Skip to content

Commit

Permalink
feat: implement ICMP pings
Browse files Browse the repository at this point in the history
  • Loading branch information
dignifiedquire committed May 1, 2023
1 parent 1f812fd commit 6c19faa
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 23 deletions.
93 changes: 87 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ zeroize = "1.5"
bao-tree = { version = "0.1.5", features = ["tokio_io"], default-features = false }
range-collections = "0.4.0"
async-lock = { git = "https://github.com/smol-rs/async-lock", branch = "notgull/block-lock" }
surge-ping = "0.8.0"


[target.'cfg(target_os = "linux")'.dependencies]
Expand Down
15 changes: 8 additions & 7 deletions src/hp/netcheck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ const DEFAULT_ACTIVE_RETRANSMIT_TIME: Duration = Duration::from_millis(200);
/// The retransmit interval used when netcheck first runs. We have no past context to work with,
/// and we want answers relatively quickly, so it's biased slightly more aggressive than
/// [`DEFAULT_ACTIVE_RETRANSMIT_TIME`]. A few extra packets at startup is fine.

const DEFAULT_INITIAL_RETRANSMIT: Duration = Duration::from_millis(100);

const FULL_REPORT_INTERVAL: Duration = Duration::from_secs(5 * 60);

const ENOUGH_REGIONS: usize = 3;

// Chosen semi-arbitrarily
const CAPTIVE_PORTAL_DELAY: Duration = Duration::from_millis(200);

Expand Down Expand Up @@ -287,28 +287,28 @@ async fn check_captive_portal(dm: &DerpMap, preferred_derp: Option<usize>) -> Re
.build()?;

// Note: the set of valid characters in a challenge and the total
// length is limited; see isChallengeChar in cmd/derper for more
// length is limited; see is_challenge_char in bin/derper for more
// details.
let chal = format!("ts_{}", node.host_name);
let challenge = format!("ts_{}", node.host_name);

let res = client
.request(
reqwest::Method::GET,
format!("http://{}/generate_204", node.host_name),
)
.header("X-Tailscale-Challenge", &chal)
.header("X-Tailscale-Challenge", &challenge)
.send()
.await?;

let expected_response = format!("response {chal}");
let expected_response = format!("response {challenge}");
let is_valid_response = res
.headers()
.get("X-Tailscale-Response")
.map(|s| s.to_str().unwrap_or_default())
== Some(&expected_response);

info!(
"[v2] checkCaptivePortal url={} status_code={} valid_response={}",
"check_captive_portal url={} status_code={} valid_response={}",
res.url(),
res.status(),
is_valid_response,
Expand All @@ -317,6 +317,7 @@ async fn check_captive_portal(dm: &DerpMap, preferred_derp: Option<usize>) -> Re

Ok(has_captive)
}

async fn measure_icmp_latency(reg: &DerpRegion, p: &Pinger) -> Result<Duration> {
if reg.nodes.is_empty() {
anyhow::bail!(
Expand All @@ -337,7 +338,7 @@ async fn measure_icmp_latency(reg: &DerpRegion, p: &Pinger) -> Result<Duration>

// Use the unique node.name field as the packet data to reduce the
// likelihood that we get a mismatched echo response.
p.send(node_addr, node.name.as_bytes()).await
p.send(node_addr.ip(), node.name.as_bytes()).await
}

async fn get_node_addr(n: &DerpNode, proto: ProbeProto) -> Option<SocketAddr> {
Expand Down
106 changes: 96 additions & 10 deletions src/hp/ping.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,109 @@
//! Allows sending ICMP echo requests to a host in order to determine network latency.
//!
//! Based on https://github.com/tailscale/tailscale/blob/main/net/ping/ping.go.

use std::{net::SocketAddr, sync::Arc, time::Duration};
use std::{fmt::Debug, net::IpAddr, sync::Arc, time::Duration};

use anyhow::Error;
use anyhow::Result;
use surge_ping::{Client, Config, IcmpPacket, PingIdentifier, PingSequence, ICMP};
use tracing::debug;

#[derive(Debug, Clone)]
pub struct Pinger(Arc<Inner>);

#[derive(Debug)]
struct Inner {}
impl Debug for Inner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Inner").finish()
}
}

struct Inner {
client_v6: Client,
client_v4: Client,
}

const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);

impl Pinger {
pub async fn new() -> Result<Self, Error> {
Ok(Self(Arc::new(Inner {})))
pub async fn new() -> Result<Self> {
let client_v4 = Client::new(&Config::builder().kind(ICMP::V4).build())?;
let client_v6 = Client::new(&Config::builder().kind(ICMP::V6).build())?;

Ok(Self(Arc::new(Inner {
client_v4,
client_v6,
})))
}

pub async fn send(&self, addr: IpAddr, data: &[u8]) -> Result<Duration> {
let client = match addr {
IpAddr::V4(_) => &self.0.client_v4,
IpAddr::V6(_) => &self.0.client_v6,
};
let mut pinger = client.pinger(addr, PingIdentifier(rand::random())).await;
pinger.timeout(DEFAULT_TIMEOUT);
match pinger.ping(PingSequence(0), data).await? {
(IcmpPacket::V4(packet), dur) => {
debug!(
"{} bytes from {}: icmp_seq={} ttl={:?} time={:0.2?}",
packet.get_size(),
packet.get_source(),
packet.get_sequence(),
packet.get_ttl(),
dur
);
Ok(dur)
}

(IcmpPacket::V6(packet), dur) => {
debug!(
"{} bytes from {}: icmp_seq={} hlim={} time={:0.2?}",
packet.get_size(),
packet.get_source(),
packet.get_sequence(),
packet.get_max_hop_limit(),
dur
);
Ok(dur)
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;

use tracing_subscriber::{prelude::*, EnvFilter};

#[tokio::test]
async fn test_ping_google() -> Result<()> {
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
.with(EnvFilter::from_default_env())
.try_init()
.ok();

// Public DNS addrs from google based on
// https://developers.google.com/speed/public-dns/docs/using

let pinger = Pinger::new().await?;

// IPv4
let dur = pinger.send("8.8.8.8".parse()?, &[1u8; 8]).await?;
assert!(!dur.is_zero());

// IPv6
match pinger
.send("2001:4860:4860:0:0:0:0:8888".parse()?, &[1u8; 8])
.await
{
Ok(dur) => {
assert!(!dur.is_zero());
}
Err(err) => {
tracing::error!("IPv6 is not available: {:?}", err);
}
}

pub async fn send(&self, _addr: SocketAddr, _data: &[u8]) -> Result<Duration, Error> {
anyhow::bail!("icmp is not available yet");
Ok(())
}
}

0 comments on commit 6c19faa

Please sign in to comment.