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:
Aleksandr Bogomiakov committed 2026-10-01 02:15:06 +01:00
1 parent f80841da63
commit c791102ad9
4 files changed
+589 -32

No files matched your search

+267 -10
View File
@@ -63,30 +63,147 @@ pub struct TcpTlsAnnouncement {
/// Discovers local network interface IPs to announce to peers. /// Discovers local network interface IPs to announce to peers.
pub fn local_ip_candidates() -> Vec<std::net::IpAddr> { pub fn local_ip_candidates() -> Vec<std::net::IpAddr> {
let mut ips = Vec::new(); let mut candidates = Vec::new();
for iface in netdev::get_interfaces() { for iface in netdev::get_interfaces() {
if iface.is_up() && !iface.is_loopback() { if !is_candidate_interface(&iface) {
let name_lower = iface.name.to_lowercase();
if name_lower.starts_with("tsun") || name_lower.contains("tsunagi") {
continue; continue;
} }
for ip in iface.ipv4 { for ip in &iface.ipv4 {
let addr = ip.addr(); let addr = ip.addr();
if !addr.is_loopback() && !addr.is_unspecified() { if is_candidate_ipv4(addr) {
ips.push(std::net::IpAddr::V4(addr)); let priority = ip_priority(&iface, std::net::IpAddr::V4(addr));
candidates.push((priority, std::net::IpAddr::V4(addr)));
} }
} }
for ip in iface.ipv6 { for ip in &iface.ipv6 {
let addr = ip.addr(); let addr = ip.addr();
if !addr.is_loopback() && !addr.is_unspecified() { if is_candidate_ip(std::net::IpAddr::V6(addr)) {
ips.push(std::net::IpAddr::V6(addr)); let priority = ip_priority(&iface, std::net::IpAddr::V6(addr));
candidates.push((priority, std::net::IpAddr::V6(addr)));
} }
} }
} }
// Sort descending by priority so best interface IP is candidate #0
candidates.sort_by_key(|b| std::cmp::Reverse(b.0));
let mut seen = std::collections::HashSet::new();
let mut ips = Vec::new();
for (_, ip) in candidates {
if seen.insert(ip) {
ips.push(ip);
}
} }
ips ips
} }
pub(crate) fn is_candidate_interface(iface: &netdev::Interface) -> bool {
if !iface.is_up() || iface.is_loopback() {
return false;
}
let name = iface.name.to_lowercase();
let friendly = iface
.friendly_name
.as_deref()
.map(str::to_lowercase)
.unwrap_or_default();
let desc = iface
.description
.as_deref()
.map(str::to_lowercase)
.unwrap_or_default();
// Skip Tsunagi / Wintun overlay interfaces
if name.starts_with("tsun")
|| name.contains("tsunagi")
|| name.contains("wintun")
|| friendly.starts_with("tsun")
|| friendly.contains("tsunagi")
|| friendly.contains("wintun")
|| desc.contains("wintun")
|| desc.contains("tsunagi")
{
return false;
}
// Skip container, VM, and virtual bridge interfaces
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")
|| name.starts_with("kube")
|| 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")
{
return false;
}
true
}
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;
}
if addr.is_documentation() {
return false;
}
true
}
pub(crate) fn is_candidate_ip(addr: std::net::IpAddr) -> bool {
match addr {
std::net::IpAddr::V4(v4) => is_candidate_ipv4(v4),
std::net::IpAddr::V6(v6) => {
if v6.is_loopback() || v6.is_unspecified() {
return false;
}
let segments = v6.segments();
// Filter link-local fe80::/10
if (segments[0] & 0xffc0) == 0xfe80 {
return false;
}
true
}
}
}
pub(crate) fn ip_priority(iface: &netdev::Interface, ip: std::net::IpAddr) -> u32 {
let has_gateway = iface.gateway.is_some();
let base = if has_gateway { 100 } else { 0 };
match ip {
std::net::IpAddr::V4(v4) => {
let octets = v4.octets();
// Tailscale: 100.64.0.0/10
let is_tailscale = octets[0] == 100 && (octets[1] & 0xc0) == 64;
let is_private = v4.is_private();
if !is_private && !is_tailscale {
base + 50
} else if has_gateway && is_private {
base + 40
} else if is_tailscale {
base + 30
} else {
base + 10
}
}
std::net::IpAddr::V6(v6) => {
let segments = v6.segments();
let is_global = (segments[0] & 0xe000) == 0x2000;
if is_global { base + 45 } else { base + 20 }
}
}
}
/// The codec implementing cryptographic transformation and announcements for `tcp-tls`. /// The codec implementing cryptographic transformation and announcements for `tcp-tls`.
pub struct TcpTlsCodec { pub struct TcpTlsCodec {
local_id: EndpointId, local_id: EndpointId,
@@ -198,9 +315,11 @@ impl TunnelCodec for TcpTlsCodec {
.map_err(|err| PluginError::Rejected(format!("invalid announcement: {err}")))?; .map_err(|err| PluginError::Rejected(format!("invalid announcement: {err}")))?;
for ip in &announcement.addrs { for ip in &announcement.addrs {
if is_candidate_ip(*ip) {
self.transport self.transport
.set_peer_addr(peer, std::net::SocketAddr::new(*ip, announcement.port)); .set_peer_addr(peer, std::net::SocketAddr::new(*ip, announcement.port));
} }
}
let peer_noise_pubkey = PublicKey::from(announcement.noise_public); let peer_noise_pubkey = PublicKey::from(announcement.noise_public);
let cipher = Arc::new(PeerCipher::new( let cipher = Arc::new(PeerCipher::new(
@@ -271,3 +390,141 @@ impl TunnelCodec for TcpTlsCodec {
/// The complete `tcp-tls` plugin. /// The complete `tcp-tls` plugin.
pub type TcpTlsPlugin = GenericTunnelPlugin<TcpTlsCodec>; pub type TcpTlsPlugin = GenericTunnelPlugin<TcpTlsCodec>;
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
#[test]
fn test_is_candidate_interface_filters_overlay_and_bridges() {
let mut iface = netdev::Interface::dummy();
iface.flags = 1; // UP
// Standard Ethernet - should pass
iface.name = "eth0".to_string();
assert!(is_candidate_interface(&iface));
// Windows Ethernet with GUID name - should pass
iface.name = "{B6149D2C-1234-5678-9ABC-DEF012345678}".to_string();
iface.friendly_name = Some("Ethernet".to_string());
iface.description = Some("Intel(R) Ethernet Connection".to_string());
assert!(is_candidate_interface(&iface));
// Linux tsun0 overlay - must be filtered
iface.name = "tsun0".to_string();
iface.friendly_name = None;
iface.description = None;
assert!(!is_candidate_interface(&iface));
// Windows Wintun overlay (GUID name with tsun0 friendly name) - must be filtered!
iface.name = "{72D85D76-F1A5-42AC-BC49-2169527CDDDF}".to_string();
iface.friendly_name = Some("tsun0".to_string());
iface.description = Some("Wintun Userspace Tunnel".to_string());
assert!(!is_candidate_interface(&iface));
// Windows Wintun overlay with description containing Wintun - must be filtered!
iface.friendly_name = Some("Local Area Connection 2".to_string());
iface.description = Some("Wintun Userspace Tunnel".to_string());
assert!(!is_candidate_interface(&iface));
// Docker bridge - must be filtered
iface.name = "docker0".to_string();
iface.friendly_name = None;
iface.description = None;
assert!(!is_candidate_interface(&iface));
// Docker network bridge br-* - must be filtered
iface.name = "br-4f6d3a9b1c2e".to_string();
assert!(!is_candidate_interface(&iface));
// Virtual ethernet veth* - must be filtered
iface.name = "veth12345".to_string();
assert!(!is_candidate_interface(&iface));
// Libvirt NAT bridge virbr* - must be filtered
iface.name = "virbr0".to_string();
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
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());
assert!(!is_candidate_interface(&iface));
// Down interface - must be filtered
let mut down_iface = netdev::Interface::dummy();
down_iface.flags = 0; // Not UP
down_iface.name = "eth0".to_string();
assert!(!is_candidate_interface(&down_iface));
}
#[test]
fn test_is_candidate_ip() {
// Valid LAN IP
assert!(is_candidate_ip(std::net::IpAddr::V4(Ipv4Addr::new(
192, 168, 1, 117
))));
// Valid Tailscale IP
assert!(is_candidate_ip(std::net::IpAddr::V4(Ipv4Addr::new(
100, 77, 155, 120
))));
// Valid Public IP
assert!(is_candidate_ip(std::net::IpAddr::V4(Ipv4Addr::new(
1, 1, 1, 1
))));
// Valid IPv6 GUA
assert!(is_candidate_ip(std::net::IpAddr::V6(Ipv6Addr::new(
0x2a01, 0x4b00, 0xb8e3, 0x4e00, 0, 0, 0, 1
))));
// Loopback - must be rejected
assert!(!is_candidate_ip(std::net::IpAddr::V4(Ipv4Addr::new(
127, 0, 0, 1
))));
assert!(!is_candidate_ip(std::net::IpAddr::V6(Ipv6Addr::LOCALHOST)));
// Unspecified - must be rejected
assert!(!is_candidate_ip(std::net::IpAddr::V4(
Ipv4Addr::UNSPECIFIED
)));
assert!(!is_candidate_ip(std::net::IpAddr::V6(
Ipv6Addr::UNSPECIFIED
)));
// Link-local - must be rejected
assert!(!is_candidate_ip(std::net::IpAddr::V4(Ipv4Addr::new(
169, 254, 1, 1
))));
assert!(!is_candidate_ip(std::net::IpAddr::V6(Ipv6Addr::new(
0xfe80, 0, 0, 0, 0, 0x1ff, 0xfe00, 1
))));
// Broadcast - must be rejected
assert!(!is_candidate_ip(std::net::IpAddr::V4(Ipv4Addr::BROADCAST)));
}
#[test]
fn test_ip_priority_ordering() {
let mut iface_lan = netdev::Interface::dummy();
iface_lan.flags = 1;
iface_lan.gateway = Some(netdev::net::device::NetworkDevice::new());
let mut iface_vpn = netdev::Interface::dummy();
iface_vpn.flags = 1;
iface_vpn.name = "tailscale0".to_string();
let lan_ip = std::net::IpAddr::V4(Ipv4Addr::new(192, 168, 1, 117));
let tailscale_ip = std::net::IpAddr::V4(Ipv4Addr::new(100, 77, 155, 120));
let p_lan = ip_priority(&iface_lan, lan_ip);
let p_ts = ip_priority(&iface_vpn, tailscale_ip);
// LAN IP with gateway should score higher than VPN IP without gateway
assert!(p_lan > p_ts);
}
}
+273 -8
View File
@@ -36,26 +36,146 @@ pub const DEFAULT_MTU: u32 = 1280;
/// WireGuard packet overhead. /// WireGuard packet overhead.
pub const WIREGUARD_OVERHEAD: u32 = 32; pub const WIREGUARD_OVERHEAD: u32 = 32;
/// Discovers local network interface IPs to announce to peers, excluding loopback and overlay interfaces. /// Discovers local network interface IPs to announce to peers, excluding loopback, overlay interfaces, and virtual bridges.
pub fn local_ip_candidates() -> Vec<IpAddr> { pub fn local_ip_candidates() -> Vec<IpAddr> {
let mut ips = Vec::new(); let mut candidates = Vec::new();
for iface in netdev::get_interfaces() { for iface in netdev::get_interfaces() {
if iface.is_up() && !iface.is_loopback() { if !is_candidate_interface(&iface) {
let name_lower = iface.name.to_lowercase();
if name_lower.starts_with("tsun") || name_lower.contains("tsunagi") {
continue; continue;
} }
for ip in iface.ipv4 { for ip in &iface.ipv4 {
let addr = ip.addr(); let addr = ip.addr();
if !addr.is_loopback() && !addr.is_unspecified() { if is_candidate_ipv4(addr) {
ips.push(IpAddr::V4(addr)); let priority = ip_priority(&iface, IpAddr::V4(addr));
candidates.push((priority, IpAddr::V4(addr)));
} }
} }
} }
// Sort descending by priority so the most viable physical / WAN / LAN IP is candidate #0
candidates.sort_by_key(|b| std::cmp::Reverse(b.0));
let mut seen = std::collections::HashSet::new();
let mut ips = Vec::new();
for (_, ip) in candidates {
if seen.insert(ip) {
ips.push(ip);
}
} }
ips ips
} }
pub(crate) fn is_candidate_interface(iface: &netdev::Interface) -> bool {
if !iface.is_up() || iface.is_loopback() {
return false;
}
let name = iface.name.to_lowercase();
let friendly = iface
.friendly_name
.as_deref()
.map(str::to_lowercase)
.unwrap_or_default();
let desc = iface
.description
.as_deref()
.map(str::to_lowercase)
.unwrap_or_default();
// Skip Tsunagi / Wintun overlay interfaces
if name.starts_with("tsun")
|| name.contains("tsunagi")
|| name.contains("wintun")
|| friendly.starts_with("tsun")
|| friendly.contains("tsunagi")
|| friendly.contains("wintun")
|| desc.contains("wintun")
|| desc.contains("tsunagi")
{
return false;
}
// Skip container, VM, and virtual bridge interfaces
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")
|| name.starts_with("kube")
|| 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")
{
return false;
}
true
}
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;
}
if addr.is_documentation() {
return false;
}
true
}
pub(crate) fn is_candidate_ip(addr: IpAddr) -> bool {
match addr {
IpAddr::V4(v4) => is_candidate_ipv4(v4),
IpAddr::V6(v6) => {
if v6.is_loopback() || v6.is_unspecified() {
return false;
}
let segments = v6.segments();
// Filter link-local fe80::/10
if (segments[0] & 0xffc0) == 0xfe80 {
return false;
}
true
}
}
}
pub(crate) fn ip_priority(iface: &netdev::Interface, ip: IpAddr) -> u32 {
let has_gateway = iface.gateway.is_some();
let base = if has_gateway { 100 } else { 0 };
match ip {
IpAddr::V4(v4) => {
let octets = v4.octets();
// Tailscale: 100.64.0.0/10
let is_tailscale = octets[0] == 100 && (octets[1] & 0xc0) == 64;
let is_private = v4.is_private();
if !is_private && !is_tailscale {
// Public / globally routable IP
base + 50
} else if has_gateway && is_private {
// Primary LAN IP with gateway (e.g. 192.168.1.117)
base + 40
} else if is_tailscale {
// Tailscale / CGNAT
base + 30
} else {
// Other private / virtual
base + 10
}
}
IpAddr::V6(v6) => {
let segments = v6.segments();
let is_global = (segments[0] & 0xe000) == 0x2000;
if is_global { base + 45 } else { base + 20 }
}
}
}
/// Configuration of the pure WireGuard plugin. /// Configuration of the pure WireGuard plugin.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct WireguardConfig { pub struct WireguardConfig {
@@ -622,9 +742,24 @@ impl IpPlugin for WireguardPlugin {
let validated = WgAnnouncement::decode_and_validate(&capability.data, network, &local_key)?; let validated = WgAnnouncement::decode_and_validate(&capability.data, network, &local_key)?;
let is_overlay_ip = |ip: &IpAddr| -> bool {
let shared = self.worker.lock_shared();
shared.networks.values().any(|s| {
if let Some(range) = s.ipv4_range {
match ip {
IpAddr::V4(v4) => range.contains(*v4),
IpAddr::V6(_) => false,
}
} else {
false
}
})
};
let socket_addrs: Vec<SocketAddr> = validated let socket_addrs: Vec<SocketAddr> = validated
.addrs .addrs
.iter() .iter()
.filter(|ip| is_candidate_ip(**ip) && !is_overlay_ip(ip))
.map(|ip| SocketAddr::new(*ip, validated.port)) .map(|ip| SocketAddr::new(*ip, validated.port))
.collect(); .collect();
@@ -734,3 +869,133 @@ impl IpPlugin for WireguardPlugin {
}) })
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
#[test]
fn test_is_candidate_interface_filters_overlay_and_bridges() {
let mut iface = netdev::Interface::dummy();
iface.flags = 1; // UP
// Standard Ethernet - should pass
iface.name = "eth0".to_string();
assert!(is_candidate_interface(&iface));
// Standard Wi-Fi - should pass
iface.name = "wlan0".to_string();
assert!(is_candidate_interface(&iface));
// Windows Ethernet with GUID name - should pass
iface.name = "{B6149D2C-1234-5678-9ABC-DEF012345678}".to_string();
iface.friendly_name = Some("Ethernet".to_string());
iface.description = Some("Intel(R) Ethernet Connection".to_string());
assert!(is_candidate_interface(&iface));
// Linux tsun0 overlay - must be filtered
iface.name = "tsun0".to_string();
iface.friendly_name = None;
iface.description = None;
assert!(!is_candidate_interface(&iface));
// Windows Wintun overlay (GUID name with tsun0 friendly name) - must be filtered!
iface.name = "{72D85D76-F1A5-42AC-BC49-2169527CDDDF}".to_string();
iface.friendly_name = Some("tsun0".to_string());
iface.description = Some("Wintun Userspace Tunnel".to_string());
assert!(!is_candidate_interface(&iface));
// Windows Wintun overlay with description containing Wintun - must be filtered!
iface.friendly_name = Some("Local Area Connection 2".to_string());
iface.description = Some("Wintun Userspace Tunnel".to_string());
assert!(!is_candidate_interface(&iface));
// Docker bridge - must be filtered
iface.name = "docker0".to_string();
iface.friendly_name = None;
iface.description = None;
assert!(!is_candidate_interface(&iface));
// Docker network bridge br-* - must be filtered
iface.name = "br-4f6d3a9b1c2e".to_string();
assert!(!is_candidate_interface(&iface));
// Virtual ethernet veth* - must be filtered
iface.name = "veth12345".to_string();
assert!(!is_candidate_interface(&iface));
// Libvirt NAT bridge virbr* - must be filtered
iface.name = "virbr0".to_string();
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
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());
assert!(!is_candidate_interface(&iface));
// Down interface - must be filtered
let mut down_iface = netdev::Interface::dummy();
down_iface.flags = 0; // Not UP
down_iface.name = "eth0".to_string();
assert!(!is_candidate_interface(&down_iface));
}
#[test]
fn test_is_candidate_ip() {
// Valid LAN IP
assert!(is_candidate_ip(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 117))));
// Valid Tailscale IP
assert!(is_candidate_ip(IpAddr::V4(Ipv4Addr::new(
100, 77, 155, 120
))));
// Valid Public IP
assert!(is_candidate_ip(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1))));
// Valid IPv6 GUA
assert!(is_candidate_ip(IpAddr::V6(Ipv6Addr::new(
0x2a01, 0x4b00, 0xb8e3, 0x4e00, 0, 0, 0, 1
))));
// Loopback - must be rejected
assert!(!is_candidate_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
assert!(!is_candidate_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)));
// Unspecified - must be rejected
assert!(!is_candidate_ip(IpAddr::V4(Ipv4Addr::UNSPECIFIED)));
assert!(!is_candidate_ip(IpAddr::V6(Ipv6Addr::UNSPECIFIED)));
// Link-local - must be rejected
assert!(!is_candidate_ip(IpAddr::V4(Ipv4Addr::new(169, 254, 1, 1))));
assert!(!is_candidate_ip(IpAddr::V6(Ipv6Addr::new(
0xfe80, 0, 0, 0, 0, 0x1ff, 0xfe00, 1
))));
// Broadcast - must be rejected
assert!(!is_candidate_ip(IpAddr::V4(Ipv4Addr::BROADCAST)));
}
#[test]
fn test_ip_priority_ordering() {
let mut iface_lan = netdev::Interface::dummy();
iface_lan.flags = 1;
iface_lan.gateway = Some(netdev::net::device::NetworkDevice::new());
let mut iface_vpn = netdev::Interface::dummy();
iface_vpn.flags = 1;
iface_vpn.name = "tailscale0".to_string();
let lan_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 117));
let tailscale_ip = IpAddr::V4(Ipv4Addr::new(100, 77, 155, 120));
let p_lan = ip_priority(&iface_lan, lan_ip);
let p_ts = ip_priority(&iface_vpn, tailscale_ip);
// LAN IP with gateway should score higher than VPN IP without gateway
assert!(p_lan > p_ts);
}
}
+30 -13
View File
@@ -71,7 +71,8 @@ impl WireguardLink {
if msg.len() > MAX_UDP_DATAGRAM_SIZE { if msg.len() > MAX_UDP_DATAGRAM_SIZE {
continue; 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() { let target = match target_r.read() {
Ok(g) => *g, Ok(g) => *g,
Err(p) => *p.into_inner(), Err(p) => *p.into_inner(),
@@ -127,16 +128,21 @@ impl WireguardLink {
/// Updates candidate addresses for this peer. /// Updates candidate addresses for this peer.
pub fn update_candidates(&self, addrs: impl IntoIterator<Item = SocketAddr>) { pub fn update_candidates(&self, addrs: impl IntoIterator<Item = SocketAddr>) {
if let Ok(mut guard) = self.candidates.write() { if let Ok(mut guard) = self.candidates.write() {
for addr in addrs { let new_candidates: Vec<SocketAddr> = addrs
if !guard.contains(&addr) { .into_iter()
guard.push(addr); .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). /// 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) {
if new_target.ip().is_unspecified() {
return;
}
self.confirmed.store(true, Ordering::Release); 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
@@ -278,7 +284,7 @@ impl WireguardTransport {
/// 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() { if !addr.is_ipv4() || addr.ip().is_unspecified() {
return; return;
} }
if let Ok(mut guard) = self.peer_addresses.write() { if let Ok(mut guard) = self.peer_addresses.write() {
@@ -293,7 +299,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 addr.is_ipv4() && !entry.contains(&addr) { if addr.is_ipv4() && !addr.ip().is_unspecified() && !entry.contains(&addr) {
entry.push(addr); entry.push(addr);
} }
} }
@@ -380,6 +386,9 @@ impl WireguardTransport {
let mut buf = [0u8; MAX_UDP_DATAGRAM_SIZE]; let mut buf = [0u8; MAX_UDP_DATAGRAM_SIZE];
while let Ok((len, src_addr)) = socket.recv_from(&mut buf).await { 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]); let datagram = Bytes::copy_from_slice(&buf[..len]);
// 1. Identify which peer this packet comes from. // 1. Identify which peer this packet comes from.
@@ -390,9 +399,7 @@ impl WireguardTransport {
}; };
links_guard links_guard
.values() .values()
.find(|link| { .find(|link| link.target_addr() == src_addr)
link.target_addr() == src_addr || link.target_addr().ip() == src_addr.ip()
})
.map(|link| link.peer()) .map(|link| link.peer())
.or_else(|| { .or_else(|| {
let addrs_guard = match self.peer_addresses.read() { let addrs_guard = match self.peer_addresses.read() {
@@ -401,13 +408,23 @@ impl WireguardTransport {
}; };
addrs_guard addrs_guard
.iter() .iter()
.find(|(_, addrs)| { .find(|(_, addrs)| addrs.contains(&src_addr))
addrs .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() .iter()
.any(|a| *a == src_addr || a.ip() == src_addr.ip()) .find(|(_, addrs)| {
addrs.iter().any(|a| a.ip() == src_addr.ip())
}) })
.map(|(peer, _)| *peer) .map(|(peer, _)| *peer)
}) })
})
})
}; };
let Some(peer) = matched_peer else { let Some(peer) = matched_peer else {
+19 -1
View File
@@ -248,7 +248,25 @@ impl EndpointAdapter {
let mut overlay_ips = std::collections::HashSet::new(); let mut overlay_ips = std::collections::HashSet::new();
for iface in netdev::get_interfaces() { for iface in netdev::get_interfaces() {
let name = iface.name.to_lowercase(); let name = iface.name.to_lowercase();
if name.starts_with("tsun") || name.contains("tsunagi") { let friendly = iface
.friendly_name
.as_deref()
.map(str::to_lowercase)
.unwrap_or_default();
let desc = iface
.description
.as_deref()
.map(str::to_lowercase)
.unwrap_or_default();
if name.starts_with("tsun")
|| name.contains("tsunagi")
|| name.contains("wintun")
|| friendly.starts_with("tsun")
|| friendly.contains("tsunagi")
|| friendly.contains("wintun")
|| desc.contains("wintun")
|| desc.contains("tsunagi")
{
for ip in iface.ipv4 { for ip in iface.ipv4 {
overlay_ips.insert(std::net::IpAddr::V4(ip.addr())); overlay_ips.insert(std::net::IpAddr::V4(ip.addr()));
} }