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.
This commit is contained in:
Thomas Eizinger
2024-01-26 13:38:31 -08:00
committed by GitHub
parent 8bc46eec84
commit c8eb53ab31
5 changed files with 93 additions and 3 deletions

2
rust/Cargo.lock generated
View File

@@ -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",

View File

@@ -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" }

View File

@@ -0,0 +1,62 @@
use crate::pool::WIREGUARD_KEEP_ALIVE;
use std::time::Instant;
#[derive(Debug)]
pub struct ConnectionInfo {
pub last_seen: Option<Instant>,
/// 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<Instant>) -> ConnectionInfo {
ConnectionInfo {
last_seen,
generated_at: Instant::now(),
}
}
}

View File

@@ -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,

View File

@@ -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<Item = (TId, ConnectionInfo)> + '_ {
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<Instant>,
// When this is `Some`, we are connected.
remote_socket: Option<RemoteSocket>,
// Socket addresses from which we might receive data (even before we are connected).
@@ -1034,6 +1054,12 @@ impl Connection {
) -> Result<Option<Transmit<'static>>, 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);