├── .gitignore ├── Cargo.toml ├── LICENSE ├── examples └── ping.rs ├── .circleci └── config.yml ├── README.md └── src ├── ping.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fastping-rs" 3 | version = "0.2.4" 4 | authors = ["bparli "] 5 | license = "MIT" 6 | homepage = "https://github.com/bparli/fastping-rs" 7 | repository = "https://github.com/bparli/fastping-rs" 8 | description = " ICMP ping library for quickly sending and measuring batches of ICMP ECHO REQUEST packets." 9 | readme = "README.md" 10 | 11 | [dependencies] 12 | pnet = "0.34" 13 | pnet_macros_support = "0.34" 14 | log = "0.4" 15 | rand = "0.8" 16 | 17 | [dev-dependencies] 18 | pretty_env_logger = "0.5" 19 | 20 | [[example]] 21 | name = "ping" 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ben Parli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/ping.rs: -------------------------------------------------------------------------------- 1 | extern crate fastping_rs; 2 | extern crate pretty_env_logger; 3 | #[macro_use] 4 | extern crate log; 5 | 6 | use fastping_rs::PingResult::{Idle, Receive}; 7 | use fastping_rs::Pinger; 8 | 9 | fn main() { 10 | pretty_env_logger::init(); 11 | let (pinger, results) = match Pinger::new(None, Some(64)) { 12 | Ok((pinger, results)) => (pinger, results), 13 | Err(e) => panic!("Error creating pinger: {}", e), 14 | }; 15 | 16 | pinger.add_ipaddr("8.8.8.8"); 17 | pinger.add_ipaddr("1.1.1.1"); 18 | pinger.add_ipaddr("7.7.7.7"); 19 | pinger.add_ipaddr("2001:4860:4860::8888"); 20 | pinger.run_pinger(); 21 | 22 | loop { 23 | match results.recv() { 24 | Ok(result) => match result { 25 | Idle { addr } => { 26 | error!("Idle Address {}.", addr); 27 | } 28 | Receive { addr, rtt } => { 29 | info!("Receive from Address {} in {:?}.", addr, rtt); 30 | } 31 | }, 32 | Err(_) => panic!("Worker threads disconnected before the solution was found!"), 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | lint: 5 | docker: 6 | - image: rust 7 | steps: 8 | - checkout 9 | - run: 10 | name: Install cargo fmt 11 | command: rustup component add rustfmt 12 | - run: 13 | name: Run lint 14 | command: cargo fmt -- --check 15 | 16 | clippy: 17 | docker: 18 | - image: rust 19 | steps: 20 | - checkout 21 | - run: 22 | name: Install cargo clippy 23 | command: rustup component add clippy 24 | - run: 25 | name: Run Clippy 26 | command: cargo clippy -- -W clippy::pedantic 27 | 28 | build_and_test: 29 | parameters: 30 | toolchain: 31 | description: rust toolchain 32 | type: string 33 | machine: true 34 | steps: 35 | - checkout 36 | - run: > 37 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | 38 | sh -s -- -v -y --profile minimal --default-toolchain <> 39 | - run: $HOME/.cargo/bin/cargo build --release 40 | - run: sudo $HOME/.cargo/bin/cargo test 41 | 42 | workflows: 43 | version: 2.1 44 | 45 | build: 46 | jobs: 47 | - lint 48 | - clippy 49 | - build_and_test: 50 | matrix: 51 | parameters: 52 | toolchain: ["stable"] 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastping-rs 2 | ICMP ping library in Rust inspired by go-fastping and AnyEvent::FastPing Perl module 3 | 4 | fastping-rs is a Rust ICMP ping library, inspired by [go-fastping](https://github.com/tatsushid/go-fastping) and the [AnyEvent::FastPing Perl module](http://search.cpan.org/~mlehmann/AnyEvent-FastPing-2.01/), for quickly sending and measuring batches of ICMP ECHO REQUEST packets. 5 | 6 | ## Usage 7 | `Pinger::new` returns a tuple containing the actual pinger, and the channel to listen for ping results on. The ping results will either be a `PingResult::Receive` (if the ping response was received prior to the maximum allowed roud trip time) or a `PingResult::Idle` (if the response was not in time). 8 | 9 | ### run with example 10 | ```shell 11 | git clone https://github.com/bparli/fastping-rs 12 | cd fastping-rs 13 | sudo RUST_LOG=info cargo run --example ping 14 | ``` 15 | 16 | 17 | # Example 18 | Add some crate to your dependencies: 19 | ```toml 20 | log = "0.4" 21 | pretty_env_logger = "0.4" 22 | fastping-rs = "0.2" 23 | ``` 24 | 25 | And then get started in your `main.rs`: 26 | ```rust 27 | extern crate pretty_env_logger; 28 | #[macro_use] 29 | extern crate log; 30 | 31 | use fastping_rs::PingResult::{Idle, Receive}; 32 | use fastping_rs::Pinger; 33 | 34 | fn main() { 35 | pretty_env_logger::init(); 36 | let (pinger, results) = match Pinger::new(None, Some(56)) { 37 | Ok((pinger, results)) => (pinger, results), 38 | Err(e) => panic!("Error creating pinger: {}", e), 39 | }; 40 | 41 | pinger.add_ipaddr("8.8.8.8"); 42 | pinger.add_ipaddr("1.1.1.1"); 43 | pinger.add_ipaddr("7.7.7.7"); 44 | pinger.add_ipaddr("2001:4860:4860::8888"); 45 | pinger.run_pinger(); 46 | 47 | loop { 48 | match results.recv() { 49 | Ok(result) => match result { 50 | Idle { addr } => { 51 | error!("Idle Address {}.", addr); 52 | } 53 | Receive { addr, rtt } => { 54 | info!("Receive from Address {} in {:?}.", addr, rtt); 55 | } 56 | }, 57 | Err(_) => panic!("Worker threads disconnected before the solution was found!"), 58 | } 59 | } 60 | } 61 | 62 | ``` 63 | 64 | Note a Pinger is initialized with two arguments: the maximum round trip time before an address is considered "idle" (2 seconds by default) and the size of the ping data packet (16 bytes by default). 65 | To explicitly set these values Pinger would be initialized like so: 66 | ```rust 67 | Pinger::new(Some(3000 as u64), Some(24 as usize)) 68 | ``` 69 | 70 | The public functions `stop_pinger()` to stop the continuous pinger and `ping_once()` to only run one round of pinging are also available. 71 | 72 | ## Additional Notes 73 | This library requires the ability to create raw sockets. Either explicitly set for your program (`sudo setcap cap_net_raw=eip /usr/bin/testping` for example) or run as root. 74 | 75 | Only supported on linux and osx for now (Windows will likely not work). 76 | -------------------------------------------------------------------------------- /src/ping.rs: -------------------------------------------------------------------------------- 1 | use pnet::packet::Packet; 2 | use pnet::packet::{icmp, icmpv6}; 3 | use pnet::transport::TransportSender; 4 | use pnet::util; 5 | use rand::random; 6 | use std::collections::BTreeMap; 7 | use std::net::IpAddr; 8 | use std::sync::mpsc::{Receiver, Sender}; 9 | use std::sync::{Arc, Mutex, RwLock}; 10 | use std::time::{Duration, Instant}; 11 | use PingResult; 12 | 13 | pub struct Ping { 14 | addr: IpAddr, 15 | identifier: u16, 16 | sequence_number: u16, 17 | pub seen: bool, 18 | } 19 | 20 | pub struct ReceivedPing { 21 | pub addr: IpAddr, 22 | pub identifier: u16, 23 | pub sequence_number: u16, 24 | pub rtt: Duration, 25 | } 26 | 27 | impl Ping { 28 | pub fn new(addr: IpAddr) -> Ping { 29 | Ping { 30 | addr, 31 | identifier: random::(), 32 | sequence_number: 0, 33 | seen: false, 34 | } 35 | } 36 | 37 | pub fn get_addr(&self) -> IpAddr { 38 | return self.addr; 39 | } 40 | 41 | pub fn get_identifier(&self) -> u16 { 42 | return self.identifier; 43 | } 44 | 45 | pub fn get_sequence_number(&self) -> u16 { 46 | return self.sequence_number; 47 | } 48 | 49 | pub fn increment_sequence_number(&mut self) -> u16 { 50 | self.sequence_number += 1; 51 | return self.sequence_number; 52 | } 53 | } 54 | 55 | fn send_echo( 56 | tx: &mut TransportSender, 57 | ping: &mut Ping, 58 | size: usize, 59 | ) -> Result { 60 | // Allocate enough space for a new packet 61 | let mut vec: Vec = vec![0; size]; 62 | 63 | let mut echo_packet = icmp::echo_request::MutableEchoRequestPacket::new(&mut vec[..]).unwrap(); 64 | echo_packet.set_sequence_number(ping.increment_sequence_number()); 65 | echo_packet.set_identifier(ping.get_identifier()); 66 | echo_packet.set_icmp_type(icmp::IcmpTypes::EchoRequest); 67 | 68 | let csum = util::checksum(echo_packet.packet(), 1); 69 | echo_packet.set_checksum(csum); 70 | 71 | tx.send_to(echo_packet, ping.get_addr()) 72 | } 73 | 74 | fn send_echov6( 75 | tx: &mut TransportSender, 76 | ping: &mut Ping, 77 | size: usize, 78 | ) -> Result { 79 | // Allocate enough space for a new packet 80 | let mut vec: Vec = vec![0; size]; 81 | 82 | let mut echo_packet = 83 | icmpv6::echo_request::MutableEchoRequestPacket::new(&mut vec[..]).unwrap(); 84 | echo_packet.set_sequence_number(ping.increment_sequence_number()); 85 | echo_packet.set_identifier(ping.get_identifier()); 86 | echo_packet.set_icmpv6_type(icmpv6::Icmpv6Types::EchoRequest); 87 | 88 | // Note: ICMPv6 checksum always calculated by the kernel, see RFC 3542 89 | 90 | tx.send_to(echo_packet, ping.get_addr()) 91 | } 92 | 93 | pub fn send_pings( 94 | size: usize, 95 | timer: Arc>, 96 | stop: Arc>, 97 | results_sender: Sender, 98 | thread_rx: Arc>>, 99 | tx: Arc>, 100 | txv6: Arc>, 101 | targets: Arc>>, 102 | max_rtt: Arc, 103 | ) { 104 | loop { 105 | for (addr, ping) in targets.lock().unwrap().iter_mut() { 106 | match if addr.is_ipv4() { 107 | send_echo(&mut tx.lock().unwrap(), ping, size) 108 | } else if addr.is_ipv6() { 109 | send_echov6(&mut txv6.lock().unwrap(), ping, size) 110 | } else { 111 | Ok(0) 112 | } { 113 | Err(e) => error!("Failed to send ping to {:?}: {}", *addr, e), 114 | _ => {} 115 | } 116 | ping.seen = false; 117 | } 118 | let start_time = Instant::now(); 119 | { 120 | // start the timer 121 | let mut timer = timer.write().unwrap(); 122 | *timer = start_time; 123 | } 124 | loop { 125 | // use recv_timeout so we don't cause a CPU to needlessly spin 126 | match thread_rx 127 | .lock() 128 | .unwrap() 129 | .recv_timeout(max_rtt.saturating_sub(start_time.elapsed())) 130 | { 131 | Ok(ping_result) => { 132 | // match ping_result { 133 | let ReceivedPing { 134 | addr, 135 | identifier, 136 | sequence_number, 137 | rtt: _, 138 | } = ping_result; 139 | // Update the address to the ping response being received 140 | if let Some(ping) = targets.lock().unwrap().get_mut(&addr) { 141 | if ping.get_identifier() == identifier 142 | && ping.get_sequence_number() == sequence_number 143 | { 144 | ping.seen = true; 145 | // Send the ping result over the client channel 146 | match results_sender.send(PingResult::Receive { 147 | addr: ping_result.addr, 148 | rtt: ping_result.rtt, 149 | }) { 150 | Ok(_) => {} 151 | Err(e) => { 152 | if !*stop.lock().unwrap() { 153 | error!("Error sending ping result on channel: {}", e) 154 | } 155 | } 156 | } 157 | } else { 158 | debug!("Received echo reply from target {}, but sequence_number (expected {} but got {}) and identifier (expected {} but got {}) don't match", addr, ping.get_sequence_number(), sequence_number, ping.get_identifier(), identifier); 159 | } 160 | } 161 | } 162 | Err(_) => { 163 | // Check we haven't exceeded the max rtt 164 | if start_time.elapsed() >= *max_rtt { 165 | break; 166 | } 167 | } 168 | } 169 | } 170 | // check for addresses which haven't replied 171 | for (addr, ping) in targets.lock().unwrap().iter() { 172 | if ping.seen == false { 173 | // Send the ping Idle over the client channel 174 | match results_sender.send(PingResult::Idle { addr: *addr }) { 175 | Ok(_) => {} 176 | Err(e) => { 177 | if !*stop.lock().unwrap() { 178 | error!("Error sending ping Idle result on channel: {}", e) 179 | } 180 | } 181 | } 182 | } 183 | } 184 | // check if we've received the stop signal 185 | if *stop.lock().unwrap() { 186 | return; 187 | } 188 | } 189 | } 190 | 191 | #[cfg(test)] 192 | mod tests { 193 | use super::*; 194 | 195 | #[test] 196 | fn test_ping() { 197 | let mut p = Ping::new("127.0.0.1".parse::().unwrap()); 198 | assert_eq!(p.get_sequence_number(), 0); 199 | assert!(p.get_identifier() > 0); 200 | 201 | p.increment_sequence_number(); 202 | assert_eq!(p.get_sequence_number(), 1); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate pnet; 2 | extern crate pnet_macros_support; 3 | #[macro_use] 4 | extern crate log; 5 | extern crate rand; 6 | 7 | mod ping; 8 | 9 | use ping::{send_pings, Ping, ReceivedPing}; 10 | use pnet::packet::icmp::echo_reply::EchoReplyPacket as IcmpEchoReplyPacket; 11 | use pnet::packet::icmpv6::echo_reply::EchoReplyPacket as Icmpv6EchoReplyPacket; 12 | use pnet::packet::ip::IpNextHeaderProtocols; 13 | use pnet::packet::Packet; 14 | use pnet::packet::{icmp, icmpv6}; 15 | use pnet::transport::transport_channel; 16 | use pnet::transport::TransportChannelType::Layer4; 17 | use pnet::transport::TransportProtocol::{Ipv4, Ipv6}; 18 | use pnet::transport::{icmp_packet_iter, icmpv6_packet_iter}; 19 | use pnet::transport::{TransportReceiver, TransportSender}; 20 | use std::collections::BTreeMap; 21 | use std::net::IpAddr; 22 | use std::sync::mpsc::{channel, Receiver, Sender}; 23 | use std::sync::{Arc, Mutex, RwLock}; 24 | use std::thread; 25 | use std::time::{Duration, Instant}; 26 | 27 | // result type returned by fastping_rs::Pinger::new() 28 | pub type NewPingerResult = Result<(Pinger, Receiver), String>; 29 | 30 | // ping result type. Idle represents pings that have not received a repsonse within the max_rtt. 31 | // Receive represents pings which have received a repsonse 32 | pub enum PingResult { 33 | Idle { addr: IpAddr }, 34 | Receive { addr: IpAddr, rtt: Duration }, 35 | } 36 | 37 | pub struct Pinger { 38 | // Number of milliseconds of an idle timeout. Once it passed, 39 | // the library calls an idle callback function. Default is 2000 40 | max_rtt: Arc, 41 | 42 | // map of addresses to ping on each run 43 | targets: Arc>>, 44 | 45 | // Size in bytes of the payload to send. Default is 16 bytes 46 | size: usize, 47 | 48 | // sender end of the channel for piping results to client 49 | results_sender: Sender, 50 | 51 | // sender end of libpnet icmp v4 transport channel 52 | tx: Arc>, 53 | 54 | // receiver end of libpnet icmp v4 transport channel 55 | rx: Arc>, 56 | 57 | // sender end of libpnet icmp v6 transport channel 58 | txv6: Arc>, 59 | 60 | // receiver end of libpnet icmp v6 transport channel 61 | rxv6: Arc>, 62 | 63 | // sender for internal result passing beween threads 64 | thread_tx: Sender, 65 | 66 | // receiver for internal result passing beween threads 67 | thread_rx: Arc>>, 68 | 69 | // timer for tracking round trip times 70 | timer: Arc>, 71 | 72 | // flag to stop pinging 73 | stop: Arc>, 74 | } 75 | 76 | impl Pinger { 77 | // initialize the pinger and start the icmp and icmpv6 listeners 78 | pub fn new(_max_rtt: Option, _size: Option) -> NewPingerResult { 79 | let targets = BTreeMap::new(); 80 | let (sender, receiver) = channel(); 81 | 82 | let protocol = Layer4(Ipv4(IpNextHeaderProtocols::Icmp)); 83 | let (tx, rx) = match transport_channel(4096, protocol) { 84 | Ok((tx, rx)) => (tx, rx), 85 | Err(e) => return Err(e.to_string()), 86 | }; 87 | 88 | let protocolv6 = Layer4(Ipv6(IpNextHeaderProtocols::Icmpv6)); 89 | let (txv6, rxv6) = match transport_channel(4096, protocolv6) { 90 | Ok((txv6, rxv6)) => (txv6, rxv6), 91 | Err(e) => return Err(e.to_string()), 92 | }; 93 | 94 | let (thread_tx, thread_rx) = channel(); 95 | 96 | let mut pinger = Pinger { 97 | max_rtt: Arc::new(Duration::from_millis(2000)), 98 | targets: Arc::new(Mutex::new(targets)), 99 | size: _size.unwrap_or(16), 100 | results_sender: sender, 101 | tx: Arc::new(Mutex::new(tx)), 102 | rx: Arc::new(Mutex::new(rx)), 103 | txv6: Arc::new(Mutex::new(txv6)), 104 | rxv6: Arc::new(Mutex::new(rxv6)), 105 | thread_rx: Arc::new(Mutex::new(thread_rx)), 106 | thread_tx, 107 | timer: Arc::new(RwLock::new(Instant::now())), 108 | stop: Arc::new(Mutex::new(false)), 109 | }; 110 | if let Some(rtt_value) = _max_rtt { 111 | pinger.max_rtt = Arc::new(Duration::from_millis(rtt_value)); 112 | } 113 | if let Some(size_value) = _size { 114 | pinger.size = size_value; 115 | } 116 | 117 | pinger.start_listener(); 118 | Ok((pinger, receiver)) 119 | } 120 | 121 | // add either an ipv4 or ipv6 target address for pinging 122 | pub fn add_ipaddr(&self, ipaddr: &str) { 123 | let addr = ipaddr.parse::(); 124 | match addr { 125 | Ok(valid_addr) => { 126 | debug!("Address added {}", valid_addr); 127 | let new_ping = Ping::new(valid_addr); 128 | self.targets.lock().unwrap().insert(valid_addr, new_ping); 129 | } 130 | Err(e) => { 131 | error!("Error adding ip address {}. Error: {}", ipaddr, e); 132 | } 133 | }; 134 | } 135 | 136 | // remove a previously added ipv4 or ipv6 target address 137 | pub fn remove_ipaddr(&self, ipaddr: &str) { 138 | let addr = ipaddr.parse::(); 139 | match addr { 140 | Ok(valid_addr) => { 141 | debug!("Address removed {}", valid_addr); 142 | self.targets.lock().unwrap().remove(&valid_addr); 143 | } 144 | Err(e) => { 145 | error!("Error removing ip address {}. Error: {}", ipaddr, e); 146 | } 147 | }; 148 | } 149 | 150 | // stop running the continous pinger 151 | pub fn stop_pinger(&self) { 152 | let mut stop = self.stop.lock().unwrap(); 153 | *stop = true; 154 | } 155 | 156 | // run one round of pinging and stop 157 | pub fn ping_once(&self) { 158 | self.run_pings(true) 159 | } 160 | 161 | // run the continuous pinger 162 | pub fn run_pinger(&self) { 163 | self.run_pings(false) 164 | } 165 | 166 | // run pinger either once or continuously 167 | fn run_pings(&self, run_once: bool) { 168 | let thread_rx = self.thread_rx.clone(); 169 | let tx = self.tx.clone(); 170 | let txv6 = self.txv6.clone(); 171 | let results_sender = self.results_sender.clone(); 172 | let stop = self.stop.clone(); 173 | let targets = self.targets.clone(); 174 | let timer = self.timer.clone(); 175 | let max_rtt = self.max_rtt.clone(); 176 | let size = self.size; 177 | 178 | { 179 | let mut stop = self.stop.lock().unwrap(); 180 | if run_once { 181 | debug!("Running pinger for one round"); 182 | *stop = true; 183 | } else { 184 | *stop = false; 185 | } 186 | } 187 | 188 | if run_once { 189 | send_pings( 190 | size, 191 | timer, 192 | stop, 193 | results_sender, 194 | thread_rx, 195 | tx, 196 | txv6, 197 | targets, 198 | max_rtt, 199 | ); 200 | } else { 201 | thread::spawn(move || { 202 | send_pings( 203 | size, 204 | timer, 205 | stop, 206 | results_sender, 207 | thread_rx, 208 | tx, 209 | txv6, 210 | targets, 211 | max_rtt, 212 | ); 213 | }); 214 | } 215 | } 216 | 217 | fn start_listener(&self) { 218 | // start icmp listeners in the background and use internal channels for results 219 | 220 | // setup ipv4 listener 221 | let thread_tx = self.thread_tx.clone(); 222 | let rx = self.rx.clone(); 223 | let timer = self.timer.clone(); 224 | let stop = self.stop.clone(); 225 | 226 | thread::spawn(move || { 227 | let mut receiver = rx.lock().unwrap(); 228 | let mut iter = icmp_packet_iter(&mut receiver); 229 | loop { 230 | match iter.next() { 231 | Ok((packet, addr)) => match IcmpEchoReplyPacket::new(packet.packet()) { 232 | Some(echo_reply) => { 233 | if packet.get_icmp_type() == icmp::IcmpTypes::EchoReply { 234 | let start_time = timer.read().unwrap(); 235 | match thread_tx.send(ReceivedPing { 236 | addr, 237 | identifier: echo_reply.get_identifier(), 238 | sequence_number: echo_reply.get_sequence_number(), 239 | rtt: Instant::now().duration_since(*start_time), 240 | }) { 241 | Ok(_) => {} 242 | Err(e) => { 243 | if !*stop.lock().unwrap() { 244 | error!("Error sending ping result on channel: {}", e) 245 | } else { 246 | return; 247 | } 248 | } 249 | } 250 | } else { 251 | debug!( 252 | "ICMP type other than reply (0) received from {:?}: {:?}", 253 | addr, 254 | packet.get_icmp_type() 255 | ); 256 | } 257 | } 258 | None => {} 259 | }, 260 | Err(e) => { 261 | error!("An error occurred while reading: {}", e); 262 | } 263 | } 264 | } 265 | }); 266 | 267 | // setup ipv6 listener 268 | let thread_txv6 = self.thread_tx.clone(); 269 | let rxv6 = self.rxv6.clone(); 270 | let timerv6 = self.timer.clone(); 271 | let stopv6 = self.stop.clone(); 272 | 273 | thread::spawn(move || { 274 | let mut receiver = rxv6.lock().unwrap(); 275 | let mut iter = icmpv6_packet_iter(&mut receiver); 276 | loop { 277 | match iter.next() { 278 | Ok((packet, addr)) => match Icmpv6EchoReplyPacket::new(packet.packet()) { 279 | Some(echo_reply) => { 280 | if packet.get_icmpv6_type() == icmpv6::Icmpv6Types::EchoReply { 281 | let start_time = timerv6.read().unwrap(); 282 | match thread_txv6.send(ReceivedPing { 283 | addr, 284 | identifier: echo_reply.get_identifier(), 285 | sequence_number: echo_reply.get_sequence_number(), 286 | rtt: Instant::now().duration_since(*start_time), 287 | }) { 288 | Ok(_) => {} 289 | Err(e) => { 290 | if !*stopv6.lock().unwrap() { 291 | error!("Error sending ping result on channel: {}", e) 292 | } else { 293 | return; 294 | } 295 | } 296 | } 297 | } else { 298 | debug!( 299 | "ICMPv6 type other than reply (129) received from {:?}: {:?}", 300 | addr, 301 | packet.get_icmpv6_type() 302 | ); 303 | } 304 | } 305 | None => {} 306 | }, 307 | Err(e) => { 308 | error!("An error occurred while reading: {}", e); 309 | } 310 | } 311 | } 312 | }); 313 | } 314 | } 315 | 316 | #[cfg(test)] 317 | mod tests { 318 | use super::*; 319 | 320 | #[test] 321 | fn test_newpinger() { 322 | // test we can create a new pinger with optional arguments, 323 | // test it returns the new pinger and a client channel 324 | // test we can use the client channel 325 | match Pinger::new(Some(3000 as u64), Some(24)) { 326 | Ok((test_pinger, test_channel)) => { 327 | assert_eq!(test_pinger.max_rtt, Arc::new(Duration::new(3, 0))); 328 | assert_eq!(test_pinger.size, 24); 329 | 330 | match test_pinger.results_sender.send(PingResult::Idle { 331 | addr: "127.0.0.1".parse::().unwrap(), 332 | }) { 333 | Ok(_) => match test_channel.recv() { 334 | Ok(result) => match result { 335 | PingResult::Idle { addr } => { 336 | assert_eq!(addr, "127.0.0.1".parse::().unwrap()); 337 | } 338 | _ => {} 339 | }, 340 | Err(_) => assert!(false), 341 | }, 342 | Err(_) => assert!(false), 343 | } 344 | } 345 | Err(e) => { 346 | println!("Test failed: {}", e); 347 | assert!(false) 348 | } 349 | }; 350 | } 351 | 352 | #[test] 353 | fn test_add_remove_addrs() { 354 | match Pinger::new(None, None) { 355 | Ok((test_pinger, _)) => { 356 | test_pinger.add_ipaddr("127.0.0.1"); 357 | assert_eq!(test_pinger.targets.lock().unwrap().len(), 1); 358 | assert!(test_pinger 359 | .targets 360 | .lock() 361 | .unwrap() 362 | .contains_key(&"127.0.0.1".parse::().unwrap())); 363 | 364 | test_pinger.remove_ipaddr("127.0.0.1"); 365 | assert_eq!(test_pinger.targets.lock().unwrap().len(), 0); 366 | assert_eq!( 367 | test_pinger 368 | .targets 369 | .lock() 370 | .unwrap() 371 | .contains_key(&"127.0.0.1".parse::().unwrap()), 372 | false 373 | ); 374 | } 375 | Err(e) => { 376 | println!("Test failed: {}", e); 377 | assert!(false) 378 | } 379 | } 380 | } 381 | 382 | #[test] 383 | fn test_stop() { 384 | match Pinger::new(None, None) { 385 | Ok((test_pinger, _)) => { 386 | assert_eq!(*test_pinger.stop.lock().unwrap(), false); 387 | test_pinger.stop_pinger(); 388 | assert_eq!(*test_pinger.stop.lock().unwrap(), true); 389 | } 390 | Err(e) => { 391 | println!("Test failed: {}", e); 392 | assert!(false) 393 | } 394 | } 395 | } 396 | 397 | #[test] 398 | fn test_integration() { 399 | // more comprehensive integration test 400 | match Pinger::new(None, None) { 401 | Ok((test_pinger, test_channel)) => { 402 | let test_addrs = vec!["127.0.0.1", "7.7.7.7", "::1"]; 403 | for target in test_addrs.iter() { 404 | test_pinger.add_ipaddr(target); 405 | } 406 | test_pinger.ping_once(); 407 | for _ in test_addrs.iter() { 408 | match test_channel.recv() { 409 | Ok(result) => match result { 410 | PingResult::Idle { addr } => { 411 | assert_eq!("7.7.7.7".parse::().unwrap(), addr); 412 | } 413 | PingResult::Receive { addr, rtt: _ } => { 414 | if addr == "::1".parse::().unwrap() 415 | || addr == "127.0.0.1".parse::().unwrap() 416 | { 417 | assert!(true) 418 | } else { 419 | assert!(false) 420 | } 421 | } 422 | _ => { 423 | assert!(false) 424 | } 425 | }, 426 | Err(_) => assert!(false), 427 | } 428 | } 429 | } 430 | Err(e) => { 431 | println!("Test failed: {}", e); 432 | assert!(false) 433 | } 434 | } 435 | } 436 | } 437 | --------------------------------------------------------------------------------