Fix WireGuard multi-network demux, candidate discovery, and status reporting

When a peer belongs to multiple overlay networks served by one agent (e.g.
a VM participating in both 'hexor' and 'ultradesu'), packets received on
the shared WireGuard UDP socket could not be differentiated by source IP/port
alone. Previously, `WireguardTransport::accept_loop` picked the first
matching link found in iteration order, starving other networks for that peer
and causing rekey timeouts (`WARN HANDSHAKE(REKEY_TIMEOUT)`,
`ERROR CONNECTION_EXPIRED(REKEY_ATTEMPT_TIME)`) and packet loss on the
starved network.

Additionally:
- When a peer had multiple candidate endpoints (such as LAN, VM bridge, and
  Tailscale CGNAT addresses), `WireguardLink` sent handshake initiations
  only to `addrs[0]`. If the first address was unreachable from the remote
  side (e.g. host's Tailscale IP unreachable from inside a local VM), the
  handshake never completed.
- In `build_report`, peers known to the `wg-quic` plugin were added with
  empty tunnels even if an active `wg` (pure WireGuard) tunnel existed,
  causing `tsunagi status` to falsely report 'no WireGuard handshake yet'
  for connected and pingable peers.
- `local_ip_candidates` previously included IPv6 addresses which caused
  OS error 22 (EINVAL) when passed to the IPv4-bound UDP socket.

Key fixes:
- In `crates/tsunagi-wireguard/src/transport.rs`:
  - Store `peer_networks` as `HashSet<NetworkId>` per peer.
  - In `WireguardLink`, maintain a list of candidate addresses and a
    `confirmed` flag. Before a handshake response confirms the active path,
    transmit datagrams (handshake initiations) to all candidate addresses.
    Upon receiving an inbound datagram, lock onto the confirmed address.
  - In `accept_loop`, deliver inbound datagrams to all active links
    associated with the sender's endpoint ID across all networks. Boringtun's
    cryptographic index and AEAD verification naturally demultiplexes and
    accepts the packet in the correct network while discarding it in others.
  - Filter candidate addresses to IPv4 to match the bound socket family.
- In `crates/tsunagi-wireguard/src/plugin.rs`:
  - Only collect IPv4 interfaces in `local_ip_candidates`.
- In `crates/tsunagi-cli/src/main.rs`:
  - Refactor `build_report` to evaluate all protocols per peer and prioritize
    whichever protocol has an active tunnel (`wg` > `tcp-tls` > `wg-quic`).
- In `crates/tsunagi-wireguard/tests/plugin_tests.rs`:
  - Add `test_wireguard_two_networks_same_peers` verifying simultaneous
    bidirectional packet transmission between two peers sharing two networks
    over a single `WireguardTransport`.

Validation on Linux:
- cargo fmt --all -- --check passed.
- cargo clippy --locked --workspace --all-targets -- -D warnings passed.
- cargo test --locked --workspace --all-targets passed (350+ tests passed).
This commit is contained in:
Aleksandr Bogomiakov committed 2026-10-01 01:38:44 +01:00
1 parent 7070a16104
commit f80841da63
4 files changed
+434 -185

No files matched your search

+119 -107
View File
@@ -19,7 +19,7 @@ use tsunagi::discovery::{
CompositeDiscovery, MainlineDiscovery, NetworkDiscovery, StaticBootstrap, CompositeDiscovery, MainlineDiscovery, NetworkDiscovery, StaticBootstrap,
}; };
use tsunagi::identity::{NetworkName, NetworkSecret}; use tsunagi::identity::{NetworkName, NetworkSecret};
use tsunagi::iroh_types::EndpointAddr; use tsunagi::iroh_types::{EndpointAddr, EndpointId};
use tsunagi::overlay::{MemoryTunFactory, TunFactory}; use tsunagi::overlay::{MemoryTunFactory, TunFactory};
use tsunagi::state::Ipv4Range; use tsunagi::state::Ipv4Range;
use tsunagi::{Agent, NetworkId}; use tsunagi::{Agent, NetworkId};
@@ -3864,125 +3864,137 @@ async fn build_report(
let overlay = if has_any_plugin && overlay.is_some() { let overlay = if has_any_plugin && overlay.is_some() {
let mut peers: Vec<OverlayPeerReport> = Vec::new(); let mut peers: Vec<OverlayPeerReport> = Vec::new();
// 1. Peers known to the wg-quic plugin. // Collect all unique peer IDs known in this network.
if let Some(ref view) = wg_view { let mut all_peer_ids: Vec<EndpointId> = Vec::new();
for peer in &view.peers { for member in &network.members {
let tcp_tls_match = if member.endpoint_id != status.endpoint_id
tcp_tls_reports.iter().find(|r| r.peer == peer.endpoint_id); && !all_peer_ids.contains(&member.endpoint_id)
{
if let Some(tcp_tls) = tcp_tls_match { all_peer_ids.push(member.endpoint_id);
peers.push(OverlayPeerReport {
endpoint_id: peer.endpoint_id.to_string(),
protocol: tcp_tls.protocol.clone(),
public_key: peer.public_key.to_string(),
address: peer.overlay_address_v4.map(|addr| addr.to_string()),
handshake_secs_ago: Some(tcp_tls.uptime_secs),
tx_packets: tcp_tls.tx_packets,
rx_packets: tcp_tls.rx_packets,
dropped: tcp_tls.dropped,
protocol_errors: 0,
path: tcp_tls.path.clone(),
});
} else if let Some(tunnel) = &peer.tunnel {
peers.push(OverlayPeerReport {
endpoint_id: peer.endpoint_id.to_string(),
protocol: "wg-quic".into(),
public_key: peer.public_key.to_string(),
address: peer.overlay_address_v4.map(|addr| addr.to_string()),
handshake_secs_ago: tunnel
.health
.since_handshake
.map(|s| s.as_secs()),
tx_packets: tunnel.stats.tx_packets,
rx_packets: tunnel.stats.rx_packets,
dropped: tunnel.stats.dropped_wrong_source
+ tunnel.stats.dropped_oversize,
protocol_errors: tunnel.stats.protocol_errors,
path: tunnel.path.clone(),
});
} else {
peers.push(OverlayPeerReport {
endpoint_id: peer.endpoint_id.to_string(),
protocol: "wg-quic".into(),
public_key: peer.public_key.to_string(),
address: peer.overlay_address_v4.map(|addr| addr.to_string()),
handshake_secs_ago: None,
tx_packets: 0,
rx_packets: 0,
dropped: 0,
protocol_errors: 0,
path: "no data link".into(),
});
}
} }
} }
// 2. Peers known to the pure WG plugin but not yet covered.
if let Some(ref view) = pure_wg_view { if let Some(ref view) = pure_wg_view {
for peer in &view.peers { for peer in &view.peers {
if peers if !all_peer_ids.contains(&peer.endpoint_id) {
.iter() all_peer_ids.push(peer.endpoint_id);
.any(|p| p.endpoint_id == peer.endpoint_id.to_string())
{
continue;
} }
if let Some(tunnel) = &peer.tunnel { }
peers.push(OverlayPeerReport { }
endpoint_id: peer.endpoint_id.to_string(), if let Some(ref view) = wg_view {
protocol: "wg".into(), for peer in &view.peers {
public_key: peer.public_key.to_string(), if !all_peer_ids.contains(&peer.endpoint_id) {
address: peer.overlay_address_v4.map(|addr| addr.to_string()), all_peer_ids.push(peer.endpoint_id);
handshake_secs_ago: tunnel
.health
.since_handshake
.map(|s| s.as_secs()),
tx_packets: tunnel.stats.tx_packets,
rx_packets: tunnel.stats.rx_packets,
dropped: tunnel.stats.dropped_wrong_source
+ tunnel.stats.dropped_oversize,
protocol_errors: tunnel.stats.protocol_errors,
path: tunnel.path.clone(),
});
} else {
peers.push(OverlayPeerReport {
endpoint_id: peer.endpoint_id.to_string(),
protocol: "wg".into(),
public_key: peer.public_key.to_string(),
address: peer.overlay_address_v4.map(|addr| addr.to_string()),
handshake_secs_ago: None,
tx_packets: 0,
rx_packets: 0,
dropped: 0,
protocol_errors: 0,
path: "no data link".into(),
});
} }
} }
} }
// 3. tcp-tls peers not yet covered by either WG view.
for report in &tcp_tls_reports { for report in &tcp_tls_reports {
if !peers if !all_peer_ids.contains(&report.peer) {
all_peer_ids.push(report.peer);
}
}
for peer_id in all_peer_ids {
let pure_wg_match = pure_wg_view
.as_ref()
.and_then(|v| v.peers.iter().find(|p| p.endpoint_id == peer_id));
let tcp_tls_match = tcp_tls_reports.iter().find(|r| r.peer == peer_id);
let wg_quic_match = wg_view
.as_ref()
.and_then(|v| v.peers.iter().find(|p| p.endpoint_id == peer_id));
let member_addr = network
.members
.iter() .iter()
.any(|p| p.endpoint_id == report.peer.to_string()) .find(|m| m.endpoint_id == peer_id)
.and_then(|m| m.overlay_address_v4)
.map(|a| a.to_string());
if let Some(pure_peer) = pure_wg_match
&& let Some(tunnel) = &pure_peer.tunnel
{ {
let address = network
.members
.iter()
.find(|m| m.endpoint_id == report.peer)
.and_then(|m| m.overlay_address_v4)
.map(|a| a.to_string());
peers.push(OverlayPeerReport { peers.push(OverlayPeerReport {
endpoint_id: report.peer.to_string(), endpoint_id: peer_id.to_string(),
protocol: report.protocol.clone(), protocol: "wg".into(),
public_key: String::new(), public_key: pure_peer.public_key.to_string(),
address, address: pure_peer
handshake_secs_ago: Some(report.uptime_secs), .overlay_address_v4
tx_packets: report.tx_packets, .map(|a| a.to_string())
rx_packets: report.rx_packets, .or(member_addr),
dropped: report.dropped, handshake_secs_ago: tunnel.health.since_handshake.map(|s| s.as_secs()),
tx_packets: tunnel.stats.tx_packets,
rx_packets: tunnel.stats.rx_packets,
dropped: tunnel.stats.dropped_wrong_source
+ tunnel.stats.dropped_oversize,
protocol_errors: tunnel.stats.protocol_errors,
path: tunnel.path.clone(),
});
} else if let Some(tcp_tls) = tcp_tls_match {
let pubkey = pure_wg_match
.map(|p| p.public_key.to_string())
.or_else(|| wg_quic_match.map(|p| p.public_key.to_string()))
.unwrap_or_default();
peers.push(OverlayPeerReport {
endpoint_id: peer_id.to_string(),
protocol: tcp_tls.protocol.clone(),
public_key: pubkey,
address: member_addr,
handshake_secs_ago: Some(tcp_tls.uptime_secs),
tx_packets: tcp_tls.tx_packets,
rx_packets: tcp_tls.rx_packets,
dropped: tcp_tls.dropped,
protocol_errors: 0, protocol_errors: 0,
path: report.path.clone(), path: tcp_tls.path.clone(),
});
} else if let Some(wg_peer) = wg_quic_match
&& let Some(tunnel) = &wg_peer.tunnel
{
peers.push(OverlayPeerReport {
endpoint_id: peer_id.to_string(),
protocol: "wg-quic".into(),
public_key: wg_peer.public_key.to_string(),
address: wg_peer
.overlay_address_v4
.map(|a| a.to_string())
.or(member_addr),
handshake_secs_ago: tunnel.health.since_handshake.map(|s| s.as_secs()),
tx_packets: tunnel.stats.tx_packets,
rx_packets: tunnel.stats.rx_packets,
dropped: tunnel.stats.dropped_wrong_source
+ tunnel.stats.dropped_oversize,
protocol_errors: tunnel.stats.protocol_errors,
path: tunnel.path.clone(),
});
} else if let Some(pure_peer) = pure_wg_match {
peers.push(OverlayPeerReport {
endpoint_id: peer_id.to_string(),
protocol: "wg".into(),
public_key: pure_peer.public_key.to_string(),
address: pure_peer
.overlay_address_v4
.map(|a| a.to_string())
.or(member_addr),
handshake_secs_ago: None,
tx_packets: 0,
rx_packets: 0,
dropped: 0,
protocol_errors: 0,
path: "no data link".into(),
});
} else if let Some(wg_peer) = wg_quic_match {
peers.push(OverlayPeerReport {
endpoint_id: peer_id.to_string(),
protocol: "wg-quic".into(),
public_key: wg_peer.public_key.to_string(),
address: wg_peer
.overlay_address_v4
.map(|a| a.to_string())
.or(member_addr),
handshake_secs_ago: None,
tx_packets: 0,
rx_packets: 0,
dropped: 0,
protocol_errors: 0,
path: "no data link".into(),
}); });
} }
} }
-6
View File
@@ -51,12 +51,6 @@ pub fn local_ip_candidates() -> Vec<IpAddr> {
ips.push(IpAddr::V4(addr)); ips.push(IpAddr::V4(addr));
} }
} }
for ip in iface.ipv6 {
let addr = ip.addr();
if !addr.is_loopback() && !addr.is_unspecified() {
ips.push(IpAddr::V6(addr));
}
}
} }
} }
ips ips
+143 -72
View File
@@ -3,7 +3,7 @@
//! Provides [`WireguardTransport`] implementing [`PacketTransport`] //! Provides [`WireguardTransport`] implementing [`PacketTransport`]
//! and [`WireguardLink`] implementing [`PacketLink`]. //! and [`WireguardLink`] implementing [`PacketLink`].
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
@@ -31,6 +31,8 @@ pub struct WireguardLink {
network: NetworkId, network: NetworkId,
peer: EndpointId, peer: EndpointId,
target_addr: Arc<RwLock<SocketAddr>>, target_addr: Arc<RwLock<SocketAddr>>,
candidates: Arc<RwLock<Vec<SocketAddr>>>,
confirmed: Arc<AtomicBool>,
tx: mpsc::Sender<Bytes>, tx: mpsc::Sender<Bytes>,
inbound_tx: mpsc::Sender<Bytes>, inbound_tx: mpsc::Sender<Bytes>,
rx: tokio::sync::Mutex<mpsc::Receiver<Bytes>>, rx: tokio::sync::Mutex<mpsc::Receiver<Bytes>>,
@@ -44,18 +46,23 @@ impl WireguardLink {
network: NetworkId, network: NetworkId,
peer: EndpointId, peer: EndpointId,
target_addr: SocketAddr, target_addr: SocketAddr,
candidates: Vec<SocketAddr>,
socket: Arc<UdpSocket>, socket: Arc<UdpSocket>,
) -> Self { ) -> Self {
let (tx_out, mut rx_out) = mpsc::channel::<Bytes>(1024); let (tx_out, mut rx_out) = mpsc::channel::<Bytes>(1024);
let (tx_in, rx_in) = mpsc::channel::<Bytes>(1024); let (tx_in, rx_in) = mpsc::channel::<Bytes>(1024);
let is_closed = Arc::new(AtomicBool::new(false)); let is_closed = Arc::new(AtomicBool::new(false));
let confirmed = Arc::new(AtomicBool::new(false));
let closed_notify = Arc::new(Notify::new()); let closed_notify = Arc::new(Notify::new());
let target_holder = Arc::new(RwLock::new(target_addr)); let target_holder = Arc::new(RwLock::new(target_addr));
let candidates_holder = Arc::new(RwLock::new(candidates));
let socket_w = Arc::clone(&socket); let socket_w = Arc::clone(&socket);
let target_r = Arc::clone(&target_holder); 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 is_closed_w = Arc::clone(&is_closed);
let closed_notify_w = Arc::clone(&closed_notify); let closed_notify_w = Arc::clone(&closed_notify);
@@ -64,12 +71,30 @@ impl WireguardLink {
if msg.len() > MAX_UDP_DATAGRAM_SIZE { if msg.len() > MAX_UDP_DATAGRAM_SIZE {
continue; continue;
} }
let target = match target_r.read() { if confirmed_r.load(Ordering::Acquire) {
Ok(g) => *g, let target = match target_r.read() {
Err(p) => *p.into_inner(), 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"); 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); is_closed_w.store(true, Ordering::Release);
@@ -80,6 +105,8 @@ impl WireguardLink {
network, network,
peer, peer,
target_addr: target_holder, target_addr: target_holder,
candidates: candidates_holder,
confirmed,
tx: tx_out, tx: tx_out,
inbound_tx: tx_in, inbound_tx: tx_in,
rx: tokio::sync::Mutex::new(rx_in), rx: tokio::sync::Mutex::new(rx_in),
@@ -93,12 +120,24 @@ impl WireguardLink {
if self.is_closed.load(Ordering::Acquire) { if self.is_closed.load(Ordering::Acquire) {
return false; return false;
} }
// If queue is full, dropping is normal for an unreliable datagram link self.confirmed.store(true, Ordering::Release);
self.inbound_tx.try_send(payload).is_ok() self.inbound_tx.try_send(payload).is_ok()
} }
/// Updates candidate addresses for this peer.
pub fn update_candidates(&self, addrs: impl IntoIterator<Item = SocketAddr>) {
if let Ok(mut guard) = self.candidates.write() {
for addr in addrs {
if !guard.contains(&addr) {
guard.push(addr);
}
}
}
}
/// Updates the target socket address (roaming / NAT hole punch update). /// Updates the target socket address (roaming / NAT hole punch update).
pub fn update_target(&self, new_target: SocketAddr) { pub fn update_target(&self, new_target: SocketAddr) {
self.confirmed.store(true, Ordering::Release);
if let Ok(mut guard) = self.target_addr.write() if let Ok(mut guard) = self.target_addr.write()
&& *guard != new_target && *guard != new_target
{ {
@@ -177,7 +216,7 @@ pub struct WireguardTransport {
listen_port: u16, listen_port: u16,
socket: Arc<UdpSocket>, socket: Arc<UdpSocket>,
peer_addresses: RwLock<HashMap<EndpointId, Vec<SocketAddr>>>, peer_addresses: RwLock<HashMap<EndpointId, Vec<SocketAddr>>>,
peer_networks: RwLock<HashMap<EndpointId, NetworkId>>, peer_networks: RwLock<HashMap<EndpointId, HashSet<NetworkId>>>,
links: RwLock<HashMap<(NetworkId, EndpointId), Arc<WireguardLink>>>, links: RwLock<HashMap<(NetworkId, EndpointId), Arc<WireguardLink>>>,
inbound_cb: Arc<Mutex<Option<InboundCallback>>>, inbound_cb: Arc<Mutex<Option<InboundCallback>>>,
} }
@@ -233,12 +272,15 @@ impl WireguardTransport {
/// Associates a peer with a network ID. /// Associates a peer with a network ID.
pub fn set_peer_network(&self, peer: EndpointId, network: NetworkId) { pub fn set_peer_network(&self, peer: EndpointId, network: NetworkId) {
if let Ok(mut guard) = self.peer_networks.write() { if let Ok(mut guard) = self.peer_networks.write() {
guard.insert(peer, network); guard.entry(peer).or_default().insert(network);
} }
} }
/// Records an observed UDP target address for a peer. /// Records an observed UDP target address for a peer.
pub fn set_peer_addr(&self, peer: EndpointId, addr: SocketAddr) { pub fn set_peer_addr(&self, peer: EndpointId, addr: SocketAddr) {
if !addr.is_ipv4() {
return;
}
if let Ok(mut guard) = self.peer_addresses.write() { if let Ok(mut guard) = self.peer_addresses.write() {
let addrs = guard.entry(peer).or_default(); let addrs = guard.entry(peer).or_default();
addrs.retain(|a| *a != addr); addrs.retain(|a| *a != addr);
@@ -251,7 +293,7 @@ impl WireguardTransport {
if let Ok(mut guard) = self.peer_addresses.write() { if let Ok(mut guard) = self.peer_addresses.write() {
let entry = guard.entry(peer).or_default(); let entry = guard.entry(peer).or_default();
for addr in addrs { for addr in addrs {
if !entry.contains(&addr) { if addr.is_ipv4() && !entry.contains(&addr) {
entry.push(addr); entry.push(addr);
} }
} }
@@ -278,12 +320,18 @@ impl WireguardTransport {
addrs: &[SocketAddr], addrs: &[SocketAddr],
we_accept: bool, we_accept: bool,
) { ) {
if let Ok(mut guard) = self.peer_networks.write() { self.set_peer_network(peer, network);
guard.insert(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());
self.set_peer_addrs(peer, addrs.iter().copied());
if we_accept && !addrs.is_empty() { // 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());
}
if we_accept && !ipv4_addrs.is_empty() {
let mut guard = match self.links.write() { let mut guard = match self.links.write() {
Ok(g) => g, Ok(g) => g,
Err(p) => p.into_inner(), Err(p) => p.into_inner(),
@@ -292,7 +340,8 @@ impl WireguardTransport {
let link = Arc::new(WireguardLink::new( let link = Arc::new(WireguardLink::new(
network, network,
peer, peer,
addrs[0], ipv4_addrs[0],
ipv4_addrs,
Arc::clone(&self.socket), Arc::clone(&self.socket),
)); ));
entry.insert(Arc::clone(&link)); entry.insert(Arc::clone(&link));
@@ -333,89 +382,109 @@ impl WireguardTransport {
while let Ok((len, src_addr)) = socket.recv_from(&mut buf).await { while let Ok((len, src_addr)) = socket.recv_from(&mut buf).await {
let datagram = Bytes::copy_from_slice(&buf[..len]); let datagram = Bytes::copy_from_slice(&buf[..len]);
// 1. Check if an active link exists matching this peer / target // 1. Identify which peer this packet comes from.
let matched_link = { 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 || link.target_addr().ip() == src_addr.ip()
})
.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
.iter()
.any(|a| *a == src_addr || a.ip() == src_addr.ip())
})
.map(|(peer, _)| *peer)
})
};
let Some(peer) = matched_peer else {
continue;
};
// 2. 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() { let guard = match self.links.read() {
Ok(g) => g, Ok(g) => g,
Err(p) => p.into_inner(), Err(p) => p.into_inner(),
}; };
guard guard
.values() .values()
.find(|link| { .filter(|l| l.peer() == peer && !l.is_closed())
link.target_addr() == src_addr || link.target_addr().ip() == src_addr.ip()
})
.cloned() .cloned()
.collect()
}; };
if let Some(link) = matched_link { if !peer_links.is_empty() {
link.update_target(src_addr); for link in peer_links {
link.deliver_inbound(datagram); link.update_target(src_addr);
link.deliver_inbound(datagram.clone());
}
continue; continue;
} }
// 2. Check if a peer has candidate addresses matching src_addr // 3. If no active link exists for this peer yet, create inbound links for all networks known for this peer.
let matched_peer = { let networks: Vec<NetworkId> = {
let guard = match self.peer_addresses.read() { let guard = match self.peer_networks.read() {
Ok(g) => g, Ok(g) => g,
Err(p) => p.into_inner(), Err(p) => p.into_inner(),
}; };
guard guard
.iter() .get(&peer)
.find(|(_, addrs)| { .map(|s| s.iter().copied().collect())
addrs .unwrap_or_default()
.iter()
.any(|a| *a == src_addr || a.ip() == src_addr.ip())
})
.map(|(peer, _)| *peer)
}; };
if let Some(peer) = matched_peer { for network in networks {
let network = { let link = {
let guard = match self.peer_networks.read() { let mut guard = match self.links.write() {
Ok(g) => g, Ok(g) => g,
Err(p) => p.into_inner(), Err(p) => p.into_inner(),
}; };
guard.get(&peer).copied().or_else(|| { if let Some(existing) = guard.get(&(network, peer))
if guard.len() == 1 { && !existing.is_closed()
guard.values().next().copied() {
} else { existing.update_target(src_addr);
match self.links.read() { existing.deliver_inbound(datagram.clone());
Ok(g) => g.keys().next().map(|(net, _)| *net), continue;
Err(p) => p.into_inner().keys().next().map(|(net, _)| *net), }
} let candidates = self.peer_addrs(peer);
}
})
};
if let Some(network) = network {
let link = Arc::new(WireguardLink::new( let link = Arc::new(WireguardLink::new(
network, network,
peer, peer,
src_addr, src_addr,
candidates,
Arc::clone(&self.socket), Arc::clone(&self.socket),
)); ));
link.deliver_inbound(datagram); 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() {
let mut guard = match self.links.write() { Ok(g) => g,
Ok(g) => g, Err(p) => p.into_inner(),
Err(p) => p.into_inner(), };
}; if let Some(ref cb) = *cb_guard {
guard.insert((network, peer), Arc::clone(&link)); cb(InboundLink {
} network,
peer,
let cb_guard = match self.inbound_cb.lock() { protocol: crate::announcement::WG_PROTOCOL.to_string(),
Ok(g) => g, link,
Err(p) => p.into_inner(), });
};
if let Some(ref cb) = *cb_guard {
cb(InboundLink {
network,
peer,
protocol: crate::announcement::WG_PROTOCOL.to_string(),
link,
});
}
} }
} }
} }
@@ -454,12 +523,14 @@ impl PacketTransport for WireguardTransport {
if let Some(existing) = guard.get(&(network, peer)) if let Some(existing) = guard.get(&(network, peer))
&& !existing.is_closed() && !existing.is_closed()
{ {
existing.update_candidates(addrs);
return Ok(Arc::clone(existing) as SharedLink); return Ok(Arc::clone(existing) as SharedLink);
} }
let link = Arc::new(WireguardLink::new( let link = Arc::new(WireguardLink::new(
network, network,
peer, peer,
target, target,
addrs,
Arc::clone(&self.socket), Arc::clone(&self.socket),
)); ));
guard.insert((network, peer), Arc::clone(&link)); guard.insert((network, peer), Arc::clone(&link));
@@ -204,3 +204,175 @@ async fn test_wireguard_plugin_end_to_end() {
assert_eq!(received_a.1, peer_b); assert_eq!(received_a.1, peer_b);
assert_eq!(received_a.2, packet_b_to_a); assert_eq!(received_a.2, packet_b_to_a);
} }
#[tokio::test]
async fn test_wireguard_two_networks_same_peers() {
let identity_a = DeviceIdentity::generate();
let peer_a = identity_a.endpoint_id();
let identity_b = DeviceIdentity::generate();
let peer_b = identity_b.endpoint_id();
let net1 = NetworkId::from_bytes([11u8; 32]);
let net2 = NetworkId::from_bytes([22u8; 32]);
let tmp_a = tempfile::tempdir().expect("tmp A");
let tmp_b = tempfile::tempdir().expect("tmp B");
let transport_b = Arc::new(
WireguardTransport::bind(&identity_b, Some(0))
.await
.expect("bind B"),
);
let port_b = transport_b.bound_port();
let transport_a = Arc::new(
WireguardTransport::bind(&identity_a, Some(0))
.await
.expect("bind A"),
);
let port_a = transport_a.bound_port();
let plugin_a =
WireguardPlugin::open(WireguardConfig::new(tmp_a.path()), Arc::clone(&transport_a))
.await
.expect("open plugin A");
let plugin_b =
WireguardPlugin::open(WireguardConfig::new(tmp_b.path()), Arc::clone(&transport_b))
.await
.expect("open plugin B");
let (sink_a_tx, mut sink_a_rx) = mpsc::channel(20);
plugin_a.attach(PluginContext::with_sink(Arc::new(TestSink {
delivered: sink_a_tx,
})));
let (sink_b_tx, mut sink_b_rx) = mpsc::channel(20);
plugin_b.attach(PluginContext::with_sink(Arc::new(TestSink {
delivered: sink_b_tx,
})));
// Activate both networks
for net in [net1, net2] {
plugin_a.on_network_activated(net);
plugin_b.on_network_activated(net);
}
// Exchange capabilities for both networks
for net in [net1, net2] {
let cap_a = loop {
if let Some(cap) = plugin_a.local_capability(net).expect("cap A") {
break cap;
}
tokio::time::sleep(Duration::from_millis(20)).await;
};
let cap_b = loop {
if let Some(cap) = plugin_b.local_capability(net).expect("cap B") {
break cap;
}
tokio::time::sleep(Duration::from_millis(20)).await;
};
plugin_a
.on_peer_capability(net, peer_b, &cap_b)
.expect("A on cap B");
plugin_b
.on_peer_capability(net, peer_a, &cap_a)
.expect("B on cap A");
}
let addr_b: SocketAddr = format!("127.0.0.1:{port_b}").parse().unwrap();
transport_a.set_peer_addr(peer_b, addr_b);
let addr_a: SocketAddr = format!("127.0.0.1:{port_a}").parse().unwrap();
transport_b.set_peer_addr(peer_a, addr_a);
// Accept loops
let transport_a_clone = Arc::clone(&transport_a);
tokio::spawn(async move {
transport_a_clone.accept_loop(|_| {}).await;
});
let (inbound_tx, mut inbound_rx) = mpsc::channel::<InboundLink>(10);
let transport_b_clone = Arc::clone(&transport_b);
tokio::spawn(async move {
transport_b_clone
.accept_loop(move |inbound| {
let _ = inbound_tx.try_send(inbound);
})
.await;
});
// Open links for both networks from A to B
for net in [net1, net2] {
let link_a = transport_a
.open(net, peer_b, "wg")
.await
.expect("open A link");
plugin_a.on_peer_link(net, peer_b, link_a);
}
// Deliver inbound links to B
for _ in 0..2 {
let inbound = tokio::time::timeout(Duration::from_secs(5), inbound_rx.recv())
.await
.expect("inbound timeout")
.expect("inbound option");
plugin_b.on_peer_link(inbound.network, peer_a, inbound.link);
}
// Wait for both handshakes to complete on both networks
for net in [net1, net2] {
let mut up = false;
for _ in 0..50 {
if let Some(overview) = plugin_a.overview(net)
&& overview.peers.iter().any(|p| p.is_up())
{
up = true;
break;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
assert!(up, "handshake on {net:?} should complete");
}
// Send on net1
let pkt1 = sample_ipv4_packet();
assert!(plugin_a.carry(net1, peer_b, pkt1.clone()));
let recv1 = tokio::time::timeout(Duration::from_secs(5), sink_b_rx.recv())
.await
.expect("recv1 timeout")
.expect("recv1 option");
assert_eq!(recv1.0, net1);
assert_eq!(recv1.2, pkt1);
// Send on net2 from A to B
let mut pkt2_bytes = sample_ipv4_packet().to_vec();
pkt2_bytes[15] = 99; // different byte
let pkt2 = Bytes::from(pkt2_bytes);
assert!(plugin_a.carry(net2, peer_b, pkt2.clone()));
let recv2 = tokio::time::timeout(Duration::from_secs(5), sink_b_rx.recv())
.await
.expect("recv2 timeout")
.expect("recv2 option");
assert_eq!(recv2.0, net2);
assert_eq!(recv2.2, pkt2);
// Send reply on net1 from B to A
assert!(plugin_b.carry(net1, peer_a, pkt1.clone()));
let reply1 = tokio::time::timeout(Duration::from_secs(5), sink_a_rx.recv())
.await
.expect("reply1 timeout")
.expect("reply1 option");
assert_eq!(reply1.0, net1);
assert_eq!(reply1.2, pkt1);
// Send reply on net2 from B to A
assert!(plugin_b.carry(net2, peer_a, pkt2.clone()));
let reply2 = tokio::time::timeout(Duration::from_secs(5), sink_a_rx.recv())
.await
.expect("reply2 timeout")
.expect("reply2 option");
assert_eq!(reply2.0, net2);
assert_eq!(reply2.2, pkt2);
}