mirror of
https://github.com/Horizontal42/netsleuth.git
synced 2026-11-11 02:12:04 +03:00
fix default gateway detection picking wrong route with multiple adapters
_default_gateway() took whichever 0.0.0.0 line route print printed first, not the one Windows actually uses (lowest metric). With a VPN adapter installed (Radmin VPN, Tailscale, ...) its default-ish route can print before the real one. Verified live: was reporting Radmin VPN's 26.0.0.1 while Ethernet (the interface actually carrying traffic, confirmed via primary_interface_ip()) has gateway 192.168.3.1; now reports that correctly. Parsing extracted into pure _parse_windows_default_gateway(), tested against a captured multi-adapter route print sample.
This commit is contained in:
parent
4888862305
commit
51c01bd8c3
2 changed files with 74 additions and 8 deletions
|
|
@ -234,28 +234,48 @@ def collect_local_net() -> LocalNet:
|
|||
)
|
||||
|
||||
|
||||
def _parse_windows_default_gateway(route_print_output: str, family: int) -> str | None:
|
||||
# `route print` lists routes in table order, not metric order, and every VPN
|
||||
# adapter (Radmin, Tailscale, ...) contributes its own 0.0.0.0 row -- the row
|
||||
# Windows actually uses for outbound traffic is whichever has the lowest
|
||||
# metric, which is not necessarily the first one printed.
|
||||
needle = "::/0" if family == socket.AF_INET6 else "0.0.0.0"
|
||||
best_gateway: str | None = None
|
||||
best_metric: float | None = None
|
||||
for line in route_print_output.splitlines():
|
||||
if needle not in line:
|
||||
continue
|
||||
tokens = line.split()
|
||||
candidates = [t for t in tokens if _looks_like_ip(t)]
|
||||
if len(candidates) < 3:
|
||||
continue
|
||||
try:
|
||||
metric = float(tokens[-1])
|
||||
except ValueError:
|
||||
continue
|
||||
if best_metric is None or metric < best_metric:
|
||||
best_metric = metric
|
||||
best_gateway = candidates[2]
|
||||
return best_gateway
|
||||
|
||||
|
||||
def _default_gateway(family: int) -> str | None:
|
||||
if platform.system() == "Windows":
|
||||
args = ["route", "print", "-6" if family == socket.AF_INET6 else "-4"]
|
||||
needle = "::/0" if family == socket.AF_INET6 else "0.0.0.0"
|
||||
else:
|
||||
args = ["ip", "-6", "route", "show", "default"] if family == socket.AF_INET6 else ["ip", "route", "show", "default"]
|
||||
needle = "via"
|
||||
try:
|
||||
out = subprocess.run(
|
||||
args, capture_output=True, text=True, errors="replace", timeout=15
|
||||
).stdout
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
if platform.system() == "Windows":
|
||||
return _parse_windows_default_gateway(out, family)
|
||||
for line in out.splitlines():
|
||||
if needle not in line:
|
||||
continue
|
||||
tokens = line.split()
|
||||
if needle == "via" and "via" in tokens:
|
||||
if "via" in tokens:
|
||||
return tokens[tokens.index("via") + 1]
|
||||
candidates = [t for t in tokens if _looks_like_ip(t)]
|
||||
if len(candidates) >= 3:
|
||||
return candidates[2]
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -111,3 +111,49 @@ def test_mtu_anomaly_ignores_normal_and_unknown_values():
|
|||
|
||||
def test_mtu_anomaly_flags_unusually_small_links_generically():
|
||||
assert mtu_anomaly(1200) == "small"
|
||||
|
||||
|
||||
import socket
|
||||
|
||||
from netsleuth.netinfo import _parse_windows_default_gateway
|
||||
|
||||
_ROUTE_PRINT_WITH_A_VPN_ADAPTER = """
|
||||
===========================================================================
|
||||
Interface List
|
||||
12...00 15 5d 01 ab cd ......Ethernet
|
||||
15...00 ff 21 4a bc de ......Radmin VPN
|
||||
===========================================================================
|
||||
|
||||
IPv4 Route Table
|
||||
===========================================================================
|
||||
Active Routes:
|
||||
Network Destination Netmask Gateway Interface Metric
|
||||
0.0.0.0 0.0.0.0 26.0.0.1 26.0.0.5 35
|
||||
0.0.0.0 0.0.0.0 192.168.3.1 192.168.3.72 25
|
||||
26.0.0.0 255.0.0.0 On-link 26.0.0.5 291
|
||||
192.168.3.0 255.255.255.0 On-link 192.168.3.72 281
|
||||
===========================================================================
|
||||
Persistent Routes:
|
||||
None
|
||||
"""
|
||||
|
||||
|
||||
def test_windows_default_gateway_picks_the_lowest_metric_route_not_the_first_listed():
|
||||
# Radmin VPN's 0.0.0.0 route (metric 35) is listed before the real Ethernet
|
||||
# default (metric 25); Windows itself prefers the lower metric for outbound
|
||||
# traffic, so the parser must too, regardless of listing order.
|
||||
assert (
|
||||
_parse_windows_default_gateway(_ROUTE_PRINT_WITH_A_VPN_ADAPTER, socket.AF_INET) == "192.168.3.1"
|
||||
)
|
||||
|
||||
|
||||
def test_windows_default_gateway_returns_the_only_route_when_there_is_one():
|
||||
single = """
|
||||
Network Destination Netmask Gateway Interface Metric
|
||||
0.0.0.0 0.0.0.0 192.168.1.1 192.168.1.34 25
|
||||
"""
|
||||
assert _parse_windows_default_gateway(single, socket.AF_INET) == "192.168.1.1"
|
||||
|
||||
|
||||
def test_windows_default_gateway_returns_none_when_no_default_route_exists():
|
||||
assert _parse_windows_default_gateway("Active Routes:\nNone\n", socket.AF_INET) is None
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue