Fix WireGuard relay preemption with UDP probing, allow VM bridges, and categorize status addresses
When remote peers communicate across networks with mixed connectivity (e.g.
a remote Linux node, a local Linux host, and a Windows VM guest under KVM):
- Previously, `WireguardTransport` immediately created and installed
unverified direct links into `RelayHub` in `notify_peer_capability` and
`open()`. When a remote peer had only unroutable private addresses
(e.g. 192.168.122.151 on a KVM NAT bridge), this dead direct link
preempted the working 2-hop relay, causing packet drops, failed handshakes,
and REKEY_TIMEOUT errors.
- Host-guest VM bridges (`virbr*`) were previously filtered out, preventing
the VM guest from discovering the host's bridge IP (192.168.122.1) and
forcing it to attempt cross-interface connections to host Wi-Fi/LAN.
- Concurrent activation of multiple networks sharing one `tsun0` interface
raced in `sync_addresses`, resulting in netlink `File exists (os error 17)`
and `The object already exists` on Windows.
Key fixes:
- In `crates/tsunagi-wireguard/src/transport.rs`:
- Implement bidirectional UDP probing with `TSUNAGI_WG_PROBE_V1` PING/PONG.
- In `open()`, probe candidate endpoints with retransmission up to 1200ms.
If unreachable, return `TransportError::Unreachable`, preventing dead
links from preempting valid relay routes in `RelayHub`.
- In `notify_peer_capability`, send hole-punching probes without prematurely
installing unverified links.
- In `accept_loop`, reply to PING probes with PONG and install confirmed
inbound links targeting the verified source address.
- In `WireguardLink::send`, inspect byte 74 of enveloped packets to detect
handshake initiations, ensuring confirmed links route data packets only
to the active target.
- In `crates/tsunagi-wireguard/src/plugin.rs`:
- Permit VM bridges (`virbr*`) as candidate interfaces (priority 10).
- Add `prioritize_local_subnet` to prioritize candidates sharing a local
IPv4 subnet with the receiver (e.g. 192.168.122.0/24).
- In `crates/tsunagi/src/overlay/interface.rs` & provisioners:
- Add async mutex `sync_lock` to serialize concurrent address syncs across networks.
- Handle netlink EEXIST (os error 17) and Windows already-exists errors
idempotently as success.
- In `crates/tsunagi-cli/src/main.rs`:
- Split `local addresses` in `tsunagi status` into `announced` (candidate
addresses in priority order with interface and rank), `not announced`
(container bridges/internal addresses with interface name), and `loopback`.
- Tests:
- Add `test_wireguard_transport_unreachable_probe_times_out` and
`test_matches_subnet` unit tests.
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
c791102ad9
commit
35acc4d231
8 files changed
+479
-103
No files matched your search
@@ -93,11 +93,10 @@ pub(crate) fn is_candidate_interface(iface: &netdev::Interface) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip container, VM, and virtual bridge interfaces
|
||||
// Skip container interfaces and virtual bridges purely internal to container engines
|
||||
if name.starts_with("docker")
|
||||
|| name.starts_with("br-")
|
||||
|| name.starts_with("veth")
|
||||
|| name.starts_with("virbr")
|
||||
|| name.starts_with("cni")
|
||||
|| name.starts_with("flannel")
|
||||
|| name.starts_with("cali")
|
||||
@@ -105,12 +104,7 @@ pub(crate) fn is_candidate_interface(iface: &netdev::Interface) -> bool {
|
||||
|| name.starts_with("dummy")
|
||||
|| name.starts_with("lxc")
|
||||
|| name.starts_with("podman")
|
||||
|| friendly.contains("vethernet")
|
||||
|| friendly.contains("vboxnet")
|
||||
|| friendly.contains("vmnet")
|
||||
|| desc.contains("virtualbox")
|
||||
|| desc.contains("hyper-v")
|
||||
|| desc.contains("vmware")
|
||||
|| friendly.contains("wsl")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -118,6 +112,43 @@ pub(crate) fn is_candidate_interface(iface: &netdev::Interface) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Reorders candidate addresses so that candidates on the same local subnet as one of our
|
||||
/// active network interfaces appear first.
|
||||
pub fn prioritize_local_subnet(candidates: &mut [SocketAddr]) {
|
||||
let local_subnets: Vec<(Ipv4Addr, u8)> = netdev::get_interfaces()
|
||||
.into_iter()
|
||||
.filter(|iface| iface.is_up() && !iface.is_loopback())
|
||||
.flat_map(|iface| {
|
||||
iface
|
||||
.ipv4
|
||||
.into_iter()
|
||||
.map(|net| (net.addr(), net.prefix_len()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
candidates.sort_by_key(|addr| {
|
||||
let is_local = match addr.ip() {
|
||||
IpAddr::V4(v4) => local_subnets
|
||||
.iter()
|
||||
.any(|(local_ip, prefix_len)| matches_subnet(*local_ip, *prefix_len, v4)),
|
||||
IpAddr::V6(_) => false,
|
||||
};
|
||||
if is_local { 0 } else { 1 }
|
||||
});
|
||||
}
|
||||
|
||||
fn matches_subnet(local_ip: Ipv4Addr, prefix_len: u8, target_ip: Ipv4Addr) -> bool {
|
||||
if prefix_len == 0 {
|
||||
return false;
|
||||
}
|
||||
let mask = if prefix_len >= 32 {
|
||||
!0u32
|
||||
} else {
|
||||
!0u32 << (32 - prefix_len)
|
||||
};
|
||||
(u32::from(local_ip) & mask) == (u32::from(target_ip) & mask)
|
||||
}
|
||||
|
||||
pub(crate) fn is_candidate_ipv4(addr: std::net::Ipv4Addr) -> bool {
|
||||
if addr.is_loopback() || addr.is_unspecified() || addr.is_link_local() || addr.is_broadcast() {
|
||||
return false;
|
||||
@@ -925,15 +956,15 @@ mod tests {
|
||||
iface.name = "veth12345".to_string();
|
||||
assert!(!is_candidate_interface(&iface));
|
||||
|
||||
// Libvirt NAT bridge virbr* - must be filtered
|
||||
// Libvirt NAT bridge virbr* - allowed for VM guests/hosts
|
||||
iface.name = "virbr0".to_string();
|
||||
assert!(!is_candidate_interface(&iface));
|
||||
assert!(is_candidate_interface(&iface));
|
||||
|
||||
// CNI bridge - must be filtered
|
||||
iface.name = "cni0".to_string();
|
||||
assert!(!is_candidate_interface(&iface));
|
||||
|
||||
// Hyper-V / WSL vEthernet switch on Windows - must be filtered
|
||||
// WSL vEthernet switch on Windows - must be filtered
|
||||
iface.name = "{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}".to_string();
|
||||
iface.friendly_name = Some("vEthernet (WSL)".to_string());
|
||||
iface.description = Some("Hyper-V Virtual Ethernet Adapter".to_string());
|
||||
@@ -998,4 +1029,14 @@ mod tests {
|
||||
// LAN IP with gateway should score higher than VPN IP without gateway
|
||||
assert!(p_lan > p_ts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matches_subnet() {
|
||||
let local_ip = Ipv4Addr::new(192, 168, 122, 151);
|
||||
let same_subnet = Ipv4Addr::new(192, 168, 122, 1);
|
||||
let diff_subnet = Ipv4Addr::new(192, 168, 1, 2);
|
||||
|
||||
assert!(matches_subnet(local_ip, 24, same_subnet));
|
||||
assert!(!matches_subnet(local_ip, 24, diff_subnet));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user