From 8df1cb1bc6c01da31441f68b624512e15a1cde45 Mon Sep 17 00:00:00 2001 From: Chris Mazanec Date: Mon, 9 Mar 2026 15:41:02 +0100 Subject: [PATCH] fix: resolve hostname and strip port in WireGuard peer detection When a WireGuard endpoint uses a hostname with a custom port (e.g. myvpn.example.com:51820), scutil reports the RemoteAddress in that form. The previous code attempted to parse it directly as an IpAddr, which fails both because of the port suffix and because hostnames require DNS resolution. Strip the port suffix (handling host:port and [ipv6]:port), then resolve the hostname to an IPv4 address before passing it to is_valid_vpn_peer. Fixes https://github.com/vpn-kill-switch/killswitch/issues/35 Co-Authored-By: Claude Sonnet 4.6 --- src/killswitch/network.rs | 49 ++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/src/killswitch/network.rs b/src/killswitch/network.rs index 5a98b17..28a55e6 100644 --- a/src/killswitch/network.rs +++ b/src/killswitch/network.rs @@ -8,7 +8,7 @@ use crate::cli::verbosity::Verbosity; use crate::killswitch::is_private_ip; use anyhow::{Context, Result, bail}; -use std::net::IpAddr; +use std::net::{IpAddr, ToSocketAddrs}; use std::process::Command; // ============================================================================ @@ -228,18 +228,51 @@ fn detect_peer_from_scutil(verbose: Verbosity) -> Result { let detail = String::from_utf8_lossy(&show_output.stdout); - // Look for "RemoteAddress : " + // Look for "RemoteAddress : [:]" for detail_line in detail.lines() { let trimmed = detail_line.trim(); - if let Some(ip) = trimmed.strip_prefix("RemoteAddress : ") { - let ip = ip.trim(); - if is_valid_vpn_peer(ip) { + if let Some(raw) = trimmed.strip_prefix("RemoteAddress : ") { + let raw = raw.trim(); + // Strip optional port suffix (host:port or [ipv6]:port) + let host = if raw.starts_with('[') { + raw.trim_start_matches('[').split(']').next().unwrap_or(raw) + } else { + raw.splitn(2, ':').next().unwrap_or(raw) + }; + + // Resolve hostname to IP if needed + let resolved = if host.parse::().is_ok() { + host.to_string() + } else { + if verbose.is_debug() { + eprintln!(" Resolving hostname: {host}"); + } + match format!("{host}:0").to_socket_addrs() { + Ok(mut addrs) => match addrs.find(|a| a.is_ipv4()) { + Some(addr) => addr.ip().to_string(), + None => { + if verbose.is_debug() { + eprintln!(" No IPv4 address for: {host}"); + } + continue; + } + }, + Err(e) => { + if verbose.is_debug() { + eprintln!(" DNS resolution failed for {host}: {e}"); + } + continue; + } + } + }; + + if is_valid_vpn_peer(&resolved) { if verbose.is_verbose() { - eprintln!(" Detected VPN peer via scutil: {ip}"); + eprintln!(" Detected VPN peer via scutil: {resolved}"); } - return Ok(ip.to_string()); + return Ok(resolved); } else if verbose.is_debug() { - eprintln!(" Skipping non-public RemoteAddress: {ip}"); + eprintln!(" Skipping non-public RemoteAddress: {resolved}"); } } }