Files
tsunagi/crates/tsunagi-wireguard/src/transport.rs
T
Aleksandr Bogomiakov d2778f0b5a Prevent WireGuard timer loop failure spam and fix handshake broadcast
When a WireGuard tunnel handshake timed out or a rekey failed,
Inner::on_tunnel_failed closed the transport link and reported the failure
to the agent core. However:
1. The peer remained in WireguardDevice::inner.peers. Once boringtun
   declared ConnectionExpired (after 90s), self.tunn.is_expired() remained
   permanently true on all subsequent 250ms timer ticks. drive_timers
   repeatedly detected failed = true every 250ms, flooding logs with
   'WARN WireGuard tunnel failed due to rekey timeout, triggering fallback'
   and sending repeated protocol failure events to the core 4 times/sec.
2. In WireguardLink::send, is_handshake_init incorrectly expected an
   envelope header offset (msg[74] == 1) when checking for initiation
   packets, which failed on raw WireGuard UDP packets. As a result,
   handshake initiations were not broadcast to all candidate endpoints.
3. In WireguardTransport::accept_loop, packet source matching contained
   dead envelope destination matching inspecting ephemeral key bytes
   against local endpoint IDs.

Fixes:
- In crates/tsunagi-wireguard/src/device.rs:
  - Add failed: bool to FlowTunnel to latch failure state. Once a failure
    is reported, update_timers returns (TunnResult::Done, false) on all
    subsequent ticks until a new handshake is started or completed.
  - In Inner::on_tunnel_failed, remove the failed peer from inner.peers so
    drive_timers ceases ticking on the dead tunnel.
- In crates/tsunagi-wireguard/src/plugin.rs:
  - In Worker::sync, retain only active unclosed links in state.links.
- In crates/tsunagi-wireguard/src/transport.rs:
  - Check is_handshake_init directly as '!msg.is_empty() && msg[0] == 1',
    ensuring handshake initiations are broadcast to all candidate addresses.
  - Clean up matched_peer to look up links and peer candidate addresses directly.
- Tests:
  - Update test_flow_tunnel_rekey_timeout_triggers_failure to assert failure
    latching and verify no repeated failure reports on subsequent ticks.

Validation on Linux:
- cargo fmt --all -- --check passed.
- cargo clippy --locked --workspace --all-targets -- -D warnings passed.
- cargo test --locked --workspace --all-targets passed.
- cargo build --release passed.
2026-10-01 03:18:32 +01:00

797 lines
28 KiB
Rust

//! Pure WireGuard UDP packet transport and link implementation.
//!
//! Provides [`WireguardTransport`] implementing [`PacketTransport`]
//! and [`WireguardLink`] implementing [`PacketLink`].
use std::collections::{HashMap, HashSet};
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use bytes::Bytes;
use iroh::EndpointId;
use tokio::net::UdpSocket;
use tokio::sync::{Notify, mpsc, oneshot};
use tsunagi::BoxFuture;
use tsunagi::dataplane::transport::{
InboundLink, PacketLink, PacketTransport, SharedLink, TransportError,
};
use tsunagi::identity::{DeviceIdentity, NetworkId};
/// Default UDP port to bind and listen on for pure WireGuard.
pub const DEFAULT_PORT: u16 = 51820;
/// Maximum payload length for a single framed datagram over UDP.
pub const MAX_UDP_DATAGRAM_SIZE: usize = 65507;
const PROBE_MAGIC: &[u8; 19] = b"TSUNAGI_WG_PROBE_V1";
const PROBE_TYPE_PING: u8 = 1;
const PROBE_TYPE_PONG: u8 = 2;
const PROBE_LEN: usize = 19 + 1 + 32 + 32 + 32 + 8; // 124 bytes
fn encode_probe(
msg_type: u8,
sender: EndpointId,
receiver: EndpointId,
network: NetworkId,
cookie: u64,
) -> Vec<u8> {
let mut buf = Vec::with_capacity(PROBE_LEN);
buf.extend_from_slice(PROBE_MAGIC);
buf.push(msg_type);
buf.extend_from_slice(sender.as_bytes());
buf.extend_from_slice(receiver.as_bytes());
buf.extend_from_slice(network.as_bytes());
buf.extend_from_slice(&cookie.to_be_bytes());
buf
}
struct DecodedProbe {
msg_type: u8,
sender: EndpointId,
receiver: EndpointId,
network: NetworkId,
cookie: u64,
}
fn decode_probe(bytes: &[u8]) -> Option<DecodedProbe> {
if bytes.len() < PROBE_LEN || &bytes[..19] != PROBE_MAGIC {
return None;
}
let msg_type = bytes[19];
if msg_type != PROBE_TYPE_PING && msg_type != PROBE_TYPE_PONG {
return None;
}
let sender = EndpointId::from_bytes(&bytes[20..52].try_into().ok()?).ok()?;
let receiver = EndpointId::from_bytes(&bytes[52..84].try_into().ok()?).ok()?;
let network = NetworkId::from_bytes(bytes[84..116].try_into().ok()?);
let cookie = u64::from_be_bytes(bytes[116..124].try_into().ok()?);
Some(DecodedProbe {
msg_type,
sender,
receiver,
network,
cookie,
})
}
/// An authenticated datagram link carried over direct UDP.
#[derive(Debug)]
pub struct WireguardLink {
network: NetworkId,
peer: EndpointId,
target_addr: Arc<RwLock<SocketAddr>>,
candidates: Arc<RwLock<Vec<SocketAddr>>>,
confirmed: Arc<AtomicBool>,
tx: mpsc::Sender<Bytes>,
inbound_tx: mpsc::Sender<Bytes>,
rx: tokio::sync::Mutex<mpsc::Receiver<Bytes>>,
closed_notify: Arc<Notify>,
is_closed: Arc<AtomicBool>,
}
impl WireguardLink {
/// Creates a new `WireguardLink` targeting `target_addr` over `socket`.
pub fn new(
network: NetworkId,
peer: EndpointId,
target_addr: SocketAddr,
candidates: Vec<SocketAddr>,
socket: Arc<UdpSocket>,
) -> Self {
let (tx_out, mut rx_out) = mpsc::channel::<Bytes>(1024);
let (tx_in, rx_in) = mpsc::channel::<Bytes>(1024);
let is_closed = Arc::new(AtomicBool::new(false));
let confirmed = Arc::new(AtomicBool::new(false));
let closed_notify = Arc::new(Notify::new());
let target_holder = Arc::new(RwLock::new(target_addr));
let candidates_holder = Arc::new(RwLock::new(candidates));
let socket_w = Arc::clone(&socket);
let target_r = Arc::clone(&target_holder);
let candidates_r = Arc::clone(&candidates_holder);
let confirmed_r = Arc::clone(&confirmed);
let is_closed_w = Arc::clone(&is_closed);
let closed_notify_w = Arc::clone(&closed_notify);
tokio::spawn(async move {
while let Some(msg) = rx_out.recv().await {
if msg.len() > MAX_UDP_DATAGRAM_SIZE {
continue;
}
// In WireGuard protocol, type 1 is Handshake Initiation (148 bytes).
let is_handshake_init = !msg.is_empty() && msg[0] == 1;
if confirmed_r.load(Ordering::Acquire) && !is_handshake_init {
let target = match target_r.read() {
Ok(g) => *g,
Err(p) => *p.into_inner(),
};
if let Err(err) = socket_w.send_to(&msg, target).await {
tracing::trace!(%err, %target, "failed to send WireGuard UDP datagram");
}
} else {
let targets = match candidates_r.read() {
Ok(g) => g.clone(),
Err(p) => p.into_inner().clone(),
};
if targets.is_empty() {
let target = match target_r.read() {
Ok(g) => *g,
Err(p) => *p.into_inner(),
};
let _ = socket_w.send_to(&msg, target).await;
} else {
for target in targets {
let _ = socket_w.send_to(&msg, target).await;
}
}
}
}
is_closed_w.store(true, Ordering::Release);
closed_notify_w.notify_waiters();
});
Self {
network,
peer,
target_addr: target_holder,
candidates: candidates_holder,
confirmed,
tx: tx_out,
inbound_tx: tx_in,
rx: tokio::sync::Mutex::new(rx_in),
closed_notify,
is_closed,
}
}
/// Delivers an inbound packet received from UDP into this link's receive queue.
pub fn deliver_inbound(&self, payload: Bytes) -> bool {
if self.is_closed.load(Ordering::Acquire) {
return false;
}
self.confirmed.store(true, Ordering::Release);
self.inbound_tx.try_send(payload).is_ok()
}
/// Marks this link as confirmed by a probe response or verified packet.
pub fn confirm(&self) {
self.confirmed.store(true, Ordering::Release);
}
/// Whether this link has had verified two-way direct connectivity.
pub fn is_confirmed(&self) -> bool {
self.confirmed.load(Ordering::Acquire)
}
/// Updates candidate addresses for this peer.
pub fn update_candidates(&self, addrs: impl IntoIterator<Item = SocketAddr>) {
if let Ok(mut guard) = self.candidates.write() {
let new_candidates: Vec<SocketAddr> = addrs
.into_iter()
.filter(|a| !a.ip().is_unspecified())
.collect();
if !new_candidates.is_empty() {
*guard = new_candidates;
}
}
}
/// Updates the target socket address (roaming / NAT hole punch update).
pub fn update_target(&self, new_target: SocketAddr) {
if new_target.ip().is_unspecified() {
return;
}
self.confirmed.store(true, Ordering::Release);
if let Ok(mut guard) = self.target_addr.write()
&& *guard != new_target
{
tracing::debug!(peer = %self.peer.fmt_short(), old = %*guard, new = %new_target, "WireGuard endpoint roamed");
*guard = new_target;
}
}
/// Explicitly closes this link, notifying any pending readers or waiters.
pub fn close(&self) {
if !self.is_closed.swap(true, Ordering::AcqRel) {
self.closed_notify.notify_waiters();
}
}
/// Returns the current target address.
pub fn target_addr(&self) -> SocketAddr {
match self.target_addr.read() {
Ok(g) => *g,
Err(p) => *p.into_inner(),
}
}
}
impl PacketLink for WireguardLink {
fn network(&self) -> NetworkId {
self.network
}
fn peer(&self) -> EndpointId {
self.peer
}
fn max_datagram_size(&self) -> usize {
MAX_UDP_DATAGRAM_SIZE
}
fn send(&self, payload: Bytes) -> Result<(), TransportError> {
if payload.len() > MAX_UDP_DATAGRAM_SIZE {
return Err(TransportError::TooLarge {
size: payload.len(),
limit: MAX_UDP_DATAGRAM_SIZE,
});
}
if self.is_closed.load(Ordering::Acquire) {
return Err(TransportError::Closed);
}
self.tx
.try_send(payload)
.map_err(|_| TransportError::Closed)
}
fn recv(&self) -> BoxFuture<'_, Option<Bytes>> {
Box::pin(async move {
if self.is_closed.load(Ordering::Acquire) {
return None;
}
tokio::select! {
biased;
_ = self.closed_notify.notified() => None,
msg = async {
let mut guard = self.rx.lock().await;
guard.recv().await
} => msg,
}
})
}
fn closed(&self) -> BoxFuture<'_, ()> {
Box::pin(async move {
if self.is_closed.load(Ordering::Acquire) {
return;
}
self.closed_notify.notified().await;
})
}
fn is_closed(&self) -> bool {
self.is_closed.load(Ordering::Acquire)
}
fn path_description(&self) -> String {
format!("wireguard via {}", self.target_addr())
}
}
type InboundCallback = Box<dyn Fn(InboundLink) + Send + Sync + 'static>;
type PendingProbes = Arc<Mutex<HashMap<(NetworkId, EndpointId, u64), oneshot::Sender<SocketAddr>>>>;
/// Direct UDP WireGuard packet transport.
pub struct WireguardTransport {
local_id: EndpointId,
listen_port: u16,
socket: Arc<UdpSocket>,
peer_addresses: RwLock<HashMap<EndpointId, Vec<SocketAddr>>>,
peer_networks: RwLock<HashMap<EndpointId, HashSet<NetworkId>>>,
links: RwLock<HashMap<(NetworkId, EndpointId), Arc<WireguardLink>>>,
inbound_cb: Arc<Mutex<Option<InboundCallback>>>,
pending_probes: PendingProbes,
}
impl std::fmt::Debug for WireguardTransport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WireguardTransport")
.field("local_id", &self.local_id.fmt_short().to_string())
.field("listen_port", &self.listen_port)
.finish()
}
}
impl WireguardTransport {
/// Binds a UDP socket for pure WireGuard transport.
///
/// Attempts `requested_port` (default 51820). If that fails, falls back to an ephemeral port.
pub async fn bind(
identity: &DeviceIdentity,
requested_port: Option<u16>,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let target_port = requested_port.unwrap_or(DEFAULT_PORT);
let bind_addr = SocketAddr::from(([0, 0, 0, 0], target_port));
let socket = match UdpSocket::bind(bind_addr).await {
Ok(s) => s,
Err(err) if target_port > 0 => {
tracing::warn!(%err, port = target_port, "failed to bind WireGuard UDP port, falling back to ephemeral port");
UdpSocket::bind(SocketAddr::from(([0, 0, 0, 0], 0))).await?
}
Err(err) => return Err(Box::new(err)),
};
let bound_port = socket.local_addr()?.port();
tracing::info!(port = bound_port, "WireGuard UDP transport bound");
Ok(Self {
local_id: identity.endpoint_id(),
listen_port: bound_port,
socket: Arc::new(socket),
peer_addresses: RwLock::new(HashMap::new()),
peer_networks: RwLock::new(HashMap::new()),
links: RwLock::new(HashMap::new()),
inbound_cb: Arc::new(Mutex::new(None)),
pending_probes: Arc::new(Mutex::new(HashMap::new())),
})
}
/// The local port this transport is listening on.
pub fn bound_port(&self) -> u16 {
self.listen_port
}
/// Associates a peer with a network ID.
pub fn set_peer_network(&self, peer: EndpointId, network: NetworkId) {
if let Ok(mut guard) = self.peer_networks.write() {
guard.entry(peer).or_default().insert(network);
}
}
/// Records an observed UDP target address for a peer.
pub fn set_peer_addr(&self, peer: EndpointId, addr: SocketAddr) {
if !addr.is_ipv4() || addr.ip().is_unspecified() {
return;
}
if let Ok(mut guard) = self.peer_addresses.write() {
let addrs = guard.entry(peer).or_default();
addrs.retain(|a| *a != addr);
addrs.insert(0, addr);
}
}
/// Records multiple observed UDP target addresses for a peer.
pub fn set_peer_addrs(&self, peer: EndpointId, addrs: impl IntoIterator<Item = SocketAddr>) {
if let Ok(mut guard) = self.peer_addresses.write() {
let entry = guard.entry(peer).or_default();
for addr in addrs {
if addr.is_ipv4() && !addr.ip().is_unspecified() && !entry.contains(&addr) {
entry.push(addr);
}
}
}
}
/// Returns candidate socket addresses known for a peer.
pub fn peer_addrs(&self, peer: EndpointId) -> Vec<SocketAddr> {
let guard = match self.peer_addresses.read() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
guard.get(&peer).cloned().unwrap_or_default()
}
/// Explicitly closes any active direct link for the given network and peer.
pub fn close_link(&self, network: NetworkId, peer: EndpointId) {
let link = {
let guard = match self.links.read() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
guard.get(&(network, peer)).cloned()
};
if let Some(link) = link {
link.close();
}
}
/// Notifies the transport that a peer's capability was announced.
///
/// When `we_accept` is true, sends a hole-punch probe to candidate addresses
/// to open stateful NAT firewalls, but does NOT create an unverified direct link
/// in the hub before packets actually flow.
pub fn notify_peer_capability(
&self,
network: NetworkId,
peer: EndpointId,
addrs: &[SocketAddr],
we_accept: bool,
) {
self.set_peer_network(peer, network);
let ipv4_addrs: Vec<SocketAddr> = addrs.iter().copied().filter(|a| a.is_ipv4()).collect();
self.set_peer_addrs(peer, ipv4_addrs.iter().copied());
// Update candidate addresses on any existing link for this peer
if let Ok(guard) = self.links.read()
&& let Some(link) = guard.get(&(network, peer))
{
link.update_candidates(ipv4_addrs.iter().copied());
}
// Send hole punching probe if we accept and have candidate addresses.
if we_accept && !ipv4_addrs.is_empty() {
let socket = Arc::clone(&self.socket);
let local_id = self.local_id;
tokio::spawn(async move {
let cookie: u64 = rand::random();
let probe = encode_probe(PROBE_TYPE_PING, local_id, peer, network, cookie);
for addr in ipv4_addrs {
let _ = socket.send_to(&probe, addr).await;
}
});
}
}
/// Runs the receive loop for incoming UDP datagrams.
pub async fn accept_loop<F>(&self, on_inbound: F)
where
F: Fn(InboundLink) + Send + Sync + 'static,
{
{
let mut guard = match self.inbound_cb.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
*guard = Some(Box::new(on_inbound));
}
let socket = Arc::clone(&self.socket);
let mut buf = [0u8; MAX_UDP_DATAGRAM_SIZE];
while let Ok((len, src_addr)) = socket.recv_from(&mut buf).await {
if src_addr.ip().is_unspecified() {
continue;
}
// 1. Direct UDP probe packet handling
if len >= PROBE_LEN
&& &buf[..19] == PROBE_MAGIC
&& let Some(probe) = decode_probe(&buf[..len])
{
if probe.receiver != self.local_id {
continue;
}
if probe.msg_type == PROBE_TYPE_PING {
let is_known = {
let guard = match self.peer_networks.read() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
guard
.get(&probe.sender)
.is_some_and(|nets| nets.contains(&probe.network))
};
if is_known {
let pong = encode_probe(
PROBE_TYPE_PONG,
self.local_id,
probe.sender,
probe.network,
probe.cookie,
);
let _ = socket.send_to(&pong, src_addr).await;
self.set_peer_addr(probe.sender, src_addr);
let link = {
let mut guard = match self.links.write() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
if let Some(existing) = guard.get(&(probe.network, probe.sender))
&& !existing.is_closed()
{
existing.update_target(src_addr);
existing.confirm();
None
} else {
let candidates = self.peer_addrs(probe.sender);
let link = Arc::new(WireguardLink::new(
probe.network,
probe.sender,
src_addr,
candidates,
Arc::clone(&self.socket),
));
link.confirm();
guard.insert((probe.network, probe.sender), Arc::clone(&link));
Some(link)
}
};
if let Some(link) = link {
let cb_guard = match self.inbound_cb.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
if let Some(ref cb) = *cb_guard {
cb(InboundLink {
network: probe.network,
peer: probe.sender,
protocol: crate::announcement::WG_PROTOCOL.to_string(),
link,
});
}
}
}
continue;
} else if probe.msg_type == PROBE_TYPE_PONG {
self.set_peer_addr(probe.sender, src_addr);
let waiter = {
let mut probes = match self.pending_probes.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
probes.remove(&(probe.network, probe.sender, probe.cookie))
};
if let Some(tx) = waiter {
let _ = tx.send(src_addr);
}
continue;
}
}
// 2. Data / handshake packet handling
let datagram = Bytes::copy_from_slice(&buf[..len]);
// Identify which peer this packet comes from
let matched_peer = {
let links_guard = match self.links.read() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
links_guard
.values()
.find(|link| link.target_addr() == src_addr)
.map(|link| link.peer())
.or_else(|| {
let addrs_guard = match self.peer_addresses.read() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
addrs_guard
.iter()
.find(|(_, addrs)| addrs.contains(&src_addr))
.map(|(peer, _)| *peer)
.or_else(|| {
links_guard
.values()
.find(|link| link.target_addr().ip() == src_addr.ip())
.map(|link| link.peer())
.or_else(|| {
addrs_guard
.iter()
.find(|(_, addrs)| {
addrs.iter().any(|a| a.ip() == src_addr.ip())
})
.map(|(peer, _)| *peer)
})
})
})
};
let Some(peer) = matched_peer else {
continue;
};
self.set_peer_addr(peer, src_addr);
// Deliver to ALL active links for this peer (across all networks this peer shares with us)
let peer_links: Vec<Arc<WireguardLink>> = {
let guard = match self.links.read() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
guard
.values()
.filter(|l| l.peer() == peer && !l.is_closed())
.cloned()
.collect()
};
if !peer_links.is_empty() {
for link in peer_links {
link.update_target(src_addr);
link.deliver_inbound(datagram.clone());
}
continue;
}
// If no active link exists for this peer yet, create inbound links for all networks known for this peer
let networks: Vec<NetworkId> = {
let guard = match self.peer_networks.read() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
guard
.get(&peer)
.map(|s| s.iter().copied().collect())
.unwrap_or_default()
};
for network in networks {
let link = {
let mut guard = match self.links.write() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
if let Some(existing) = guard.get(&(network, peer))
&& !existing.is_closed()
{
existing.update_target(src_addr);
existing.deliver_inbound(datagram.clone());
continue;
}
let candidates = self.peer_addrs(peer);
let link = Arc::new(WireguardLink::new(
network,
peer,
src_addr,
candidates,
Arc::clone(&self.socket),
));
link.update_target(src_addr);
link.deliver_inbound(datagram.clone());
guard.insert((network, peer), Arc::clone(&link));
link
};
let cb_guard = match self.inbound_cb.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
if let Some(ref cb) = *cb_guard {
cb(InboundLink {
network,
peer,
protocol: crate::announcement::WG_PROTOCOL.to_string(),
link,
});
}
}
}
}
}
impl PacketTransport for WireguardTransport {
fn name(&self) -> &str {
"wireguard"
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn open<'a>(
&'a self,
network: NetworkId,
peer: EndpointId,
_protocol: &'a str,
) -> BoxFuture<'a, Result<SharedLink, TransportError>> {
Box::pin(async move {
let mut addrs = self.peer_addrs(peer);
if addrs.is_empty() {
return Err(TransportError::Unreachable(format!(
"no candidate UDP address known for peer {}",
peer.fmt_short()
)));
}
crate::plugin::prioritize_local_subnet(&mut addrs);
// If an existing confirmed link is alive, reuse it
{
let guard = match self.links.read() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
if let Some(existing) = guard.get(&(network, peer))
&& !existing.is_closed()
&& existing.is_confirmed()
{
existing.update_candidates(addrs);
return Ok(Arc::clone(existing) as SharedLink);
}
}
// Probe candidate addresses with PING
let cookie: u64 = rand::random();
let (tx, rx) = oneshot::channel::<SocketAddr>();
{
let mut probes = match self.pending_probes.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
probes.insert((network, peer, cookie), tx);
}
let probe_ping = encode_probe(PROBE_TYPE_PING, self.local_id, peer, network, cookie);
let socket = Arc::clone(&self.socket);
let candidates = addrs.clone();
let probe_task = tokio::spawn(async move {
for i in 0..3 {
for target in &candidates {
let _ = socket.send_to(&probe_ping, *target).await;
}
if i < 2 {
tokio::time::sleep(std::time::Duration::from_millis(350)).await;
}
}
});
let confirmed_addr =
match tokio::time::timeout(std::time::Duration::from_millis(1200), rx).await {
Ok(Ok(addr)) => {
probe_task.abort();
addr
}
_ => {
probe_task.abort();
let mut probes = match self.pending_probes.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
probes.remove(&(network, peer, cookie));
return Err(TransportError::Unreachable(format!(
"direct UDP probe to peer {} timed out",
peer.fmt_short()
)));
}
};
let link = {
let mut guard = match self.links.write() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
if let Some(existing) = guard.get(&(network, peer))
&& !existing.is_closed()
{
existing.update_target(confirmed_addr);
existing.update_candidates(addrs);
existing.confirm();
return Ok(Arc::clone(existing) as SharedLink);
}
let link = Arc::new(WireguardLink::new(
network,
peer,
confirmed_addr,
addrs,
Arc::clone(&self.socket),
));
link.confirm();
guard.insert((network, peer), Arc::clone(&link));
link
};
Ok(link as SharedLink)
})
}
}