Skip to content

Commit 7d3c28a

Browse files
committed
fix #35 thanks @chrismazanec
1 parent da86b2d commit 7d3c28a

File tree

1 file changed

+97
-8
lines changed

1 file changed

+97
-8
lines changed

src/killswitch/network.rs

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use crate::cli::verbosity::Verbosity;
99
use crate::killswitch::is_private_ip;
1010
use anyhow::{Context, Result, bail};
11-
use std::net::IpAddr;
11+
use std::net::{IpAddr, ToSocketAddrs};
1212
use std::process::Command;
1313

1414
// ============================================================================
@@ -228,18 +228,30 @@ fn detect_peer_from_scutil(verbose: Verbosity) -> Result<String> {
228228

229229
let detail = String::from_utf8_lossy(&show_output.stdout);
230230

231-
// Look for "RemoteAddress : <ip>"
231+
// Look for "RemoteAddress : <host>[:<port>]"
232232
for detail_line in detail.lines() {
233233
let trimmed = detail_line.trim();
234-
if let Some(ip) = trimmed.strip_prefix("RemoteAddress : ") {
235-
let ip = ip.trim();
236-
if is_valid_vpn_peer(ip) {
234+
if let Some(raw) = trimmed.strip_prefix("RemoteAddress : ") {
235+
let raw = raw.trim();
236+
let host = strip_port(raw);
237+
238+
// Resolve hostname to IP if needed
239+
let resolved = if host.parse::<IpAddr>().is_ok() {
240+
host.to_string()
241+
} else {
242+
match resolve_hostname_v4(host, verbose) {
243+
Some(ip) => ip,
244+
None => continue,
245+
}
246+
};
247+
248+
if is_valid_vpn_peer(&resolved) {
237249
if verbose.is_verbose() {
238-
eprintln!(" Detected VPN peer via scutil: {ip}");
250+
eprintln!(" Detected VPN peer via scutil: {resolved}");
239251
}
240-
return Ok(ip.to_string());
252+
return Ok(resolved);
241253
} else if verbose.is_debug() {
242-
eprintln!(" Skipping non-public RemoteAddress: {ip}");
254+
eprintln!(" Skipping non-public RemoteAddress: {resolved}");
243255
}
244256
}
245257
}
@@ -248,6 +260,48 @@ fn detect_peer_from_scutil(verbose: Verbosity) -> Result<String> {
248260
bail!("No VPN peer found via scutil")
249261
}
250262

263+
/// Strip optional port suffix from a remote address.
264+
///
265+
/// Handles `host:port`, `ip:port`, and `[ipv6]:port` forms.
266+
/// Returns the bare host/IP.
267+
fn strip_port(raw: &str) -> &str {
268+
if let Some(rest) = raw.strip_prefix('[') {
269+
// [ipv6]:port — extract content between brackets
270+
rest.split(']').next().unwrap_or(raw)
271+
} else if raw.matches(':').count() == 1 {
272+
// host:port or ipv4:port — split on the single colon
273+
raw.split(':').next().unwrap_or(raw)
274+
} else {
275+
// bare IP, bare hostname, or bare IPv6 (multiple colons, no brackets)
276+
raw
277+
}
278+
}
279+
280+
/// Resolve a hostname to its first IPv4 address.
281+
fn resolve_hostname_v4(host: &str, verbose: Verbosity) -> Option<String> {
282+
if verbose.is_debug() {
283+
eprintln!(" Resolving hostname: {host}");
284+
}
285+
match format!("{host}:0").to_socket_addrs() {
286+
Ok(addrs) => {
287+
if let Some(addr) = addrs.into_iter().find(std::net::SocketAddr::is_ipv4) {
288+
Some(addr.ip().to_string())
289+
} else {
290+
if verbose.is_debug() {
291+
eprintln!(" No IPv4 address for: {host}");
292+
}
293+
None
294+
}
295+
}
296+
Err(e) => {
297+
if verbose.is_debug() {
298+
eprintln!(" DNS resolution failed for {host}: {e}");
299+
}
300+
None
301+
}
302+
}
303+
}
304+
251305
/// Extract destination IP from netstat routing table line.
252306
///
253307
/// Format: "Destination Gateway Flags Netif Expire"
@@ -592,4 +646,39 @@ mod tests {
592646
assert_eq!(hex_to_cidr("ffffff00"), None); // Missing 0x prefix
593647
assert_eq!(hex_to_cidr(""), None);
594648
}
649+
650+
// -------------------------------------------------------------------------
651+
// Port stripping tests
652+
// -------------------------------------------------------------------------
653+
654+
#[test]
655+
fn test_strip_port_bare_ipv4() {
656+
assert_eq!(strip_port("1.2.3.4"), "1.2.3.4");
657+
}
658+
659+
#[test]
660+
fn test_strip_port_ipv4_with_port() {
661+
assert_eq!(strip_port("1.2.3.4:51820"), "1.2.3.4");
662+
}
663+
664+
#[test]
665+
fn test_strip_port_hostname_with_port() {
666+
assert_eq!(strip_port("myvpn.example.com:51820"), "myvpn.example.com");
667+
}
668+
669+
#[test]
670+
fn test_strip_port_bare_hostname() {
671+
assert_eq!(strip_port("myvpn.example.com"), "myvpn.example.com");
672+
}
673+
674+
#[test]
675+
fn test_strip_port_ipv6_bracketed_with_port() {
676+
assert_eq!(strip_port("[::1]:51820"), "::1");
677+
}
678+
679+
#[test]
680+
fn test_strip_port_bare_ipv6() {
681+
// Bare IPv6 has multiple colons, no brackets — returned as-is
682+
assert_eq!(strip_port("2001:db8::1"), "2001:db8::1");
683+
}
595684
}

0 commit comments

Comments
 (0)