Fix overlay and virtual interface candidate leakage and WireGuard rekeying
On Windows (Wintun), iface.name in netdev is an adapter GUID, while
iface.friendly_name is 'tsun0'. Checking only iface.name failed to
identify and exclude Wintun overlay interfaces. Consequently, Windows
nodes announced their own overlay IP addresses (e.g. 10.13.37.26) as
candidate endpoints. Remote peers latched onto this overlay IP as the
WireGuard target, routing encrypted packets into tsun0 (recursive loop),
resulting in failed handshakes and subsequent HANDSHAKE(REKEY_TIMEOUT)
and CONNECTION_EXPIRED.
Similarly, Docker bridges (br-*, docker0) and virtual bridges were
announced and alphabetically sorted before physical LAN or Tailscale
interfaces, causing peers to attempt WireGuard handshakes with internal
Docker bridge addresses (e.g. 172.21.0.1).
Key changes:
- In crates/tsunagi-wireguard and crates/tsunagi-tcp-tls:
- Robustly identify overlay interfaces by inspecting iface.name,
iface.friendly_name, and iface.description for 'tsun', 'tsunagi',
and 'wintun'.
- Filter out container and virtual bridge interfaces (docker, br-,
veth, virbr, cni, flannel, cali, kube, dummy, lxc, podman, vethernet,
vboxnet, vmnet).
- Prioritize candidates by physical interface score: interfaces with
a default gateway (physical LAN/WAN) and Tailscale (100.64.0.0/10)
score highest and sort to candidate #0.
- In on_peer_capability, filter out any announced address falling inside
any active overlay IPv4 range (defense in depth on receiver).
- In crates/tsunagi-wireguard/src/transport.rs:
- In WireguardLink::send, broadcast Handshake Initiation packets (type 1)
to all candidate addresses so initial handshakes and rekeys probe all
candidate paths and recover if an endpoint roamed or NAT expired.
- In accept_loop, prioritize exact socket address matches before falling
back to candidate IP matches for NAT port roaming.
- In crates/tsunagi/src/net.rs:
- Update iroh AddrFilter to inspect friendly_name and description,
preventing Wintun overlay IPs from leaking into iroh discovery endpoints.
- Tests:
- Add unit tests for candidate interface filtering, IP validation, and
priority scoring in tsunagi-wireguard and tsunagi-tcp-tls.
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:
1 parent
f80841da63
commit
c791102ad9
4 files changed
+603
-46
No files matched your search
@@ -71,7 +71,8 @@ impl WireguardLink {
|
||||
if msg.len() > MAX_UDP_DATAGRAM_SIZE {
|
||||
continue;
|
||||
}
|
||||
if confirmed_r.load(Ordering::Acquire) {
|
||||
let is_handshake_init = msg.first() == Some(&1);
|
||||
if confirmed_r.load(Ordering::Acquire) && !is_handshake_init {
|
||||
let target = match target_r.read() {
|
||||
Ok(g) => *g,
|
||||
Err(p) => *p.into_inner(),
|
||||
@@ -127,16 +128,21 @@ impl WireguardLink {
|
||||
/// 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);
|
||||
}
|
||||
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
|
||||
@@ -278,7 +284,7 @@ impl WireguardTransport {
|
||||
|
||||
/// Records an observed UDP target address for a peer.
|
||||
pub fn set_peer_addr(&self, peer: EndpointId, addr: SocketAddr) {
|
||||
if !addr.is_ipv4() {
|
||||
if !addr.is_ipv4() || addr.ip().is_unspecified() {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut guard) = self.peer_addresses.write() {
|
||||
@@ -293,7 +299,7 @@ impl WireguardTransport {
|
||||
if let Ok(mut guard) = self.peer_addresses.write() {
|
||||
let entry = guard.entry(peer).or_default();
|
||||
for addr in addrs {
|
||||
if addr.is_ipv4() && !entry.contains(&addr) {
|
||||
if addr.is_ipv4() && !addr.ip().is_unspecified() && !entry.contains(&addr) {
|
||||
entry.push(addr);
|
||||
}
|
||||
}
|
||||
@@ -380,6 +386,9 @@ impl WireguardTransport {
|
||||
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;
|
||||
}
|
||||
let datagram = Bytes::copy_from_slice(&buf[..len]);
|
||||
|
||||
// 1. Identify which peer this packet comes from.
|
||||
@@ -390,9 +399,7 @@ impl WireguardTransport {
|
||||
};
|
||||
links_guard
|
||||
.values()
|
||||
.find(|link| {
|
||||
link.target_addr() == src_addr || link.target_addr().ip() == src_addr.ip()
|
||||
})
|
||||
.find(|link| link.target_addr() == src_addr)
|
||||
.map(|link| link.peer())
|
||||
.or_else(|| {
|
||||
let addrs_guard = match self.peer_addresses.read() {
|
||||
@@ -401,12 +408,22 @@ impl WireguardTransport {
|
||||
};
|
||||
addrs_guard
|
||||
.iter()
|
||||
.find(|(_, addrs)| {
|
||||
addrs
|
||||
.iter()
|
||||
.any(|a| *a == src_addr || a.ip() == src_addr.ip())
|
||||
})
|
||||
.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)
|
||||
})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user