From c8eb53ab3195ff3e622c83470135d4ca348ce7a8 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Fri, 26 Jan 2024 13:38:31 -0800 Subject: [PATCH] feat(connection): introduce keep-alive and expose `last_seen` (#3388) We configure wireguard's keep-alive to 5 seconds and patch `boringtun` to expose the time since the last packet, which will always be < KEEP_ALIVE (5 seconds) if the connection is intact. The policy on what to do on missed keep-alives is pushed to the upper layer. Instead of acting on it, we simply expose a `stats` function that exposes the data. An upper layer can then decide on what to do in the case on missed keep-alives. Resolves: #3372. --- rust/Cargo.lock | 2 +- rust/Cargo.toml | 2 +- rust/connlib/connection/src/info.rs | 62 +++++++++++++++++++++++++++++ rust/connlib/connection/src/lib.rs | 2 + rust/connlib/connection/src/pool.rs | 28 ++++++++++++- 5 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 rust/connlib/connection/src/info.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index fc8224421..f868f0526 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -744,7 +744,7 @@ dependencies = [ [[package]] name = "boringtun" version = "0.6.0" -source = "git+https://github.com/cloudflare/boringtun?branch=master#f672bb6c1e1e371240a8d151f15854687eb740bb" +source = "git+https://github.com/thomaseizinger/boringtun?branch=feat/expose-last-seen#6fd54c027e6b78192a02de3e77d00552ec36968d" dependencies = [ "aead", "base64 0.13.1", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 2854f9e90..8a083e57e 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -47,7 +47,7 @@ firezone-tunnel = { path = "connlib/tunnel"} phoenix-channel = { path = "phoenix-channel"} [patch.crates-io] -boringtun = { git = "https://github.com/cloudflare/boringtun", branch = "master" } # Contains unreleased patches we need (bump of x25519-dalek) +boringtun = { git = "https://github.com/thomaseizinger/boringtun", branch = "feat/expose-last-seen" } webrtc = { git = "https://github.com/firezone/webrtc", branch = "expose-new-endpoint" } str0m = { git = "https://github.com/algesten/str0m", branch = "main" } diff --git a/rust/connlib/connection/src/info.rs b/rust/connlib/connection/src/info.rs new file mode 100644 index 000000000..340b29667 --- /dev/null +++ b/rust/connlib/connection/src/info.rs @@ -0,0 +1,62 @@ +use crate::pool::WIREGUARD_KEEP_ALIVE; +use std::time::Instant; + +#[derive(Debug)] +pub struct ConnectionInfo { + pub last_seen: Option, + + /// When this instance of [`ConnectionInfo`] was created. + pub generated_at: Instant, +} + +impl ConnectionInfo { + pub fn missed_keep_alives(&self) -> u64 { + let Some(last_seen) = self.last_seen else { + return 0; + }; + + let duration = self.generated_at.duration_since(last_seen); + + duration.as_secs() / WIREGUARD_KEEP_ALIVE as u64 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + #[test] + fn no_missed_keep_alives_on_none() { + let info = info(None); + + let missed_keep_alives = info.missed_keep_alives(); + + assert_eq!(missed_keep_alives, 0); + } + + #[test] + fn more_than_5_sec_one_missed_keep_alive() { + let info = info(Some(Instant::now() - Duration::from_secs(6))); + + let missed_keep_alives = info.missed_keep_alives(); + + assert_eq!(missed_keep_alives, 1); + } + + #[test] + fn more_than_10_sec_two_missed_keep_alives() { + let info = info(Some(Instant::now() - Duration::from_secs(11))); + + let missed_keep_alives = info.missed_keep_alives(); + + assert_eq!(missed_keep_alives, 2); + } + + fn info(last_seen: Option) -> ConnectionInfo { + ConnectionInfo { + last_seen, + generated_at: Instant::now(), + } + } +} diff --git a/rust/connlib/connection/src/lib.rs b/rust/connlib/connection/src/lib.rs index a41cebb9a..af97ab9b8 100644 --- a/rust/connlib/connection/src/lib.rs +++ b/rust/connlib/connection/src/lib.rs @@ -1,10 +1,12 @@ mod allocation; mod channel_data; mod index; +mod info; mod ip_packet; mod pool; mod stun_binding; +pub use info::ConnectionInfo; pub use ip_packet::IpPacket; pub use pool::{ Answer, ClientConnectionPool, ConnectionPool, Credentials, Error, Event, Offer, diff --git a/rust/connlib/connection/src/pool.rs b/rust/connlib/connection/src/pool.rs index 16aee09ec..8afdb73c0 100644 --- a/rust/connlib/connection/src/pool.rs +++ b/rust/connlib/connection/src/pool.rs @@ -21,6 +21,7 @@ use str0m::{Candidate, CandidateKind, IceConnectionState}; use crate::allocation::Allocation; use crate::index::IndexLfsr; +use crate::info::ConnectionInfo; use crate::stun_binding::StunBinding; use crate::IpPacket; use boringtun::noise::errors::WireGuardError; @@ -30,6 +31,9 @@ use stun_codec::rfc5389::attributes::{Realm, Username}; // Note: Taken from boringtun const HANDSHAKE_RATE_LIMIT: u64 = 100; +/// How often wireguard will send a keep-alive packet. +pub(crate) const WIREGUARD_KEEP_ALIVE: u16 = 5; + const MAX_UDP_SIZE: usize = (1 << 16) - 1; /// Manages a set of wireguard connections for a server. @@ -101,6 +105,19 @@ where } } + /// Lazily retrieve stats of all connections. + pub fn stats(&self) -> impl Iterator + '_ { + self.negotiated_connections.iter().map(|(id, c)| { + ( + *id, + ConnectionInfo { + last_seen: c.last_seen, + generated_at: self.last_now, + }, + ) + }) + } + pub fn add_local_interface(&mut self, local_addr: SocketAddr) { self.local_interfaces.insert(local_addr); @@ -516,7 +533,7 @@ where self.private_key.clone(), remote, Some(key), - None, + Some(WIREGUARD_KEEP_ALIVE), self.index.next(), Some(self.rate_limiter.clone()), ), @@ -525,6 +542,7 @@ where next_timer_update: self.last_now, remote_socket: None, possible_sockets: HashSet::default(), + last_seen: None, } } } @@ -961,6 +979,8 @@ struct Connection { tunnel: Tunn, next_timer_update: Instant, + last_seen: Option, + // When this is `Some`, we are connected. remote_socket: Option, // Socket addresses from which we might receive data (even before we are connected). @@ -1034,6 +1054,12 @@ impl Connection { ) -> Result>, WireGuardError> { self.agent.handle_timeout(now); + // TODO: `boringtun` is impure because it calls `Instant::now`. + self.last_seen = self + .tunnel + .time_since_last_received() + .and_then(|d| now.checked_sub(d)); + if now >= self.next_timer_update { self.next_timer_update = now + Duration::from_secs(1);