mirror of
https://github.com/Horizontal42/netsleuth.git
synced 2026-11-10 21:32: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:
|
def _default_gateway(family: int) -> str | None:
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
args = ["route", "print", "-6" if family == socket.AF_INET6 else "-4"]
|
args = ["route", "print", "-6" if family == socket.AF_INET6 else "-4"]
|
||||||
needle = "::/0" if family == socket.AF_INET6 else "0.0.0.0"
|
|
||||||
else:
|
else:
|
||||||
args = ["ip", "-6", "route", "show", "default"] if family == socket.AF_INET6 else ["ip", "route", "show", "default"]
|
args = ["ip", "-6", "route", "show", "default"] if family == socket.AF_INET6 else ["ip", "route", "show", "default"]
|
||||||
needle = "via"
|
|
||||||
try:
|
try:
|
||||||
out = subprocess.run(
|
out = subprocess.run(
|
||||||
args, capture_output=True, text=True, errors="replace", timeout=15
|
args, capture_output=True, text=True, errors="replace", timeout=15
|
||||||
).stdout
|
).stdout
|
||||||
except (OSError, subprocess.SubprocessError):
|
except (OSError, subprocess.SubprocessError):
|
||||||
return None
|
return None
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
return _parse_windows_default_gateway(out, family)
|
||||||
for line in out.splitlines():
|
for line in out.splitlines():
|
||||||
if needle not in line:
|
|
||||||
continue
|
|
||||||
tokens = line.split()
|
tokens = line.split()
|
||||||
if needle == "via" and "via" in tokens:
|
if "via" in tokens:
|
||||||
return tokens[tokens.index("via") + 1]
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,3 +111,49 @@ def test_mtu_anomaly_ignores_normal_and_unknown_values():
|
||||||
|
|
||||||
def test_mtu_anomaly_flags_unusually_small_links_generically():
|
def test_mtu_anomaly_flags_unusually_small_links_generically():
|
||||||
assert mtu_anomaly(1200) == "small"
|
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