mirror of
https://github.com/Horizontal42/netsleuth.git
synced 2026-11-10 23:52:04 +03:00
122 lines
4.9 KiB
Python
122 lines
4.9 KiB
Python
from __future__ import annotations
|
|
|
|
from netsleuth.config import Thresholds
|
|
from netsleuth.interpret import cgnat_findings, dual_stack_findings, path_findings
|
|
from netsleuth.ip_geo import nat64_prefix_from_aaaa
|
|
from netsleuth.models import LocalNet, TraceHop, TraceResult
|
|
from netsleuth.netinfo import is_cgnat
|
|
|
|
|
|
def test_is_cgnat_boundaries():
|
|
assert is_cgnat("100.63.255.255") is False
|
|
assert is_cgnat("100.64.0.0") is True
|
|
assert is_cgnat("100.127.255.255") is True
|
|
assert is_cgnat("100.128.0.0") is False
|
|
assert is_cgnat(None) is False
|
|
assert is_cgnat("not-an-ip") is False
|
|
assert is_cgnat("2001:db8::1") is False
|
|
|
|
|
|
def test_nat64_prefix_from_aaaa_recognises_the_well_known_prefix():
|
|
assert nat64_prefix_from_aaaa(["64:ff9b::c000:aa"]) == "64:ff9b::/96"
|
|
|
|
|
|
def test_nat64_prefix_from_aaaa_recognises_an_isp_specific_prefix():
|
|
assert nat64_prefix_from_aaaa(["2001:db8:64::c000:ab"]) == "2001:db8:64::/96"
|
|
|
|
|
|
def test_nat64_prefix_from_aaaa_ignores_a_non_synthesized_answer():
|
|
assert nat64_prefix_from_aaaa(["2606:4700:4700::1111"]) is None
|
|
|
|
|
|
def test_nat64_prefix_from_aaaa_handles_empty_and_garbage_input():
|
|
assert nat64_prefix_from_aaaa([]) is None
|
|
assert nat64_prefix_from_aaaa(["not-an-address"]) is None
|
|
|
|
|
|
def test_nat64_prefix_from_aaaa_value_error():
|
|
from unittest.mock import patch
|
|
|
|
with patch("netsleuth.ip_geo.ipaddress.IPv6Address", side_effect=ValueError("mocked error")):
|
|
assert nat64_prefix_from_aaaa(["2001:db8::1"]) is None
|
|
|
|
|
|
def test_cgnat_findings_fires_on_local_ip_evidence():
|
|
local = LocalNet(local_ipv4="100.64.5.5", cgnat=True, cgnat_evidence="local address 100.64.5.5 is in 100.64.0.0/10")
|
|
findings = cgnat_findings(local, [])
|
|
assert [f.id for f in findings] == ["net.cgnat"]
|
|
assert findings[0].severity == "warn"
|
|
|
|
|
|
def test_cgnat_findings_fires_on_gateway_evidence():
|
|
local = LocalNet(default_gateway_v4="100.70.0.1", cgnat=True, cgnat_evidence="default gateway 100.70.0.1 is in 100.64.0.0/10")
|
|
assert [f.id for f in cgnat_findings(local, [])] == ["net.cgnat"]
|
|
|
|
|
|
def test_cgnat_findings_fires_on_traceroute_hop_evidence():
|
|
local = LocalNet(local_ipv4="192.168.1.34", default_gateway_v4="192.168.1.1")
|
|
trace = TraceResult(
|
|
target="1.1.1.1",
|
|
hops=[
|
|
TraceHop(ttl=1, ip="192.168.1.1", loss_pct=0.0, avg_ms=1.0),
|
|
TraceHop(ttl=2, ip="100.65.1.1", loss_pct=0.0, avg_ms=8.0),
|
|
],
|
|
)
|
|
findings = cgnat_findings(local, [trace])
|
|
assert [f.id for f in findings] == ["net.cgnat"]
|
|
assert "hop 2" in findings[0].detail
|
|
|
|
|
|
def test_cgnat_findings_silent_on_a_clean_private_network():
|
|
local = LocalNet(local_ipv4="192.168.1.34", default_gateway_v4="192.168.1.1")
|
|
trace = TraceResult(target="1.1.1.1", hops=[TraceHop(ttl=1, ip="192.168.1.1", loss_pct=0.0, avg_ms=1.0)])
|
|
assert cgnat_findings(local, [trace]) == []
|
|
|
|
|
|
def test_dual_stack_findings_nat64_warns_on_a_v6_only_host():
|
|
local = LocalNet(local_ipv6="2001:db8::1", is_dual_stack=False)
|
|
findings = dual_stack_findings("2001:db8:64::/96", local)
|
|
assert [f.id for f in findings] == ["net.nat64"]
|
|
assert findings[0].severity == "warn"
|
|
|
|
|
|
def test_dual_stack_findings_nat64_is_informational_on_a_dual_stack_host():
|
|
local = LocalNet(local_ipv4="203.0.113.5", local_ipv6="2001:db8::1", is_dual_stack=True)
|
|
findings = dual_stack_findings("2001:db8:64::/96", local)
|
|
assert findings[0].severity == "info"
|
|
|
|
|
|
def test_dual_stack_findings_silent_when_no_nat64_prefix():
|
|
assert dual_stack_findings(None, LocalNet()) == []
|
|
|
|
|
|
def _trace(hop0: TraceHop) -> TraceResult:
|
|
return TraceResult(target="1.1.1.1", resolved_ip="1.1.1.1", backend="icmplib", hops=[hop0], completed=True)
|
|
|
|
|
|
def test_path_findings_flags_loss_at_the_gateway_hop():
|
|
local = LocalNet(default_gateway_v4="192.168.1.1")
|
|
trace = _trace(TraceHop(ttl=1, ip="192.168.1.1", loss_pct=25.0, avg_ms=1.0))
|
|
findings = path_findings(trace, local, Thresholds())
|
|
ids = [f.id for f in findings]
|
|
assert "path.first_hop_loss" in ids
|
|
assert [f for f in findings if f.id == "path.first_hop_loss"][0].severity == "crit"
|
|
assert "path.first_hop_unexpected" not in ids
|
|
|
|
|
|
def test_path_findings_flags_an_unexpected_first_hop():
|
|
local = LocalNet(default_gateway_v4="192.168.1.1")
|
|
trace = _trace(TraceHop(ttl=1, ip="10.0.0.1", loss_pct=0.0, avg_ms=1.0))
|
|
findings = path_findings(trace, local, Thresholds())
|
|
assert [f.id for f in findings] == ["path.first_hop_unexpected"]
|
|
|
|
|
|
def test_path_findings_without_local_param_is_unchanged():
|
|
trace = _trace(TraceHop(ttl=1, ip="192.168.1.1", loss_pct=30.0, avg_ms=1.0))
|
|
findings = path_findings(trace)
|
|
assert all(not f.id.startswith("path.first_hop") for f in findings)
|
|
|
|
|
|
def test_path_findings_first_hop_empty_hops_still_reports_incomplete():
|
|
trace = TraceResult(target="1.1.1.1", resolved_ip="1.1.1.1", backend="icmplib", hops=[], completed=False)
|
|
assert [f.id for f in path_findings(trace, LocalNet())] == ["path.incomplete"]
|