diff --git a/napalm_fritzbox/fritzbox.py b/napalm_fritzbox/fritzbox.py index c04ba9c..55b0cb3 100644 --- a/napalm_fritzbox/fritzbox.py +++ b/napalm_fritzbox/fritzbox.py @@ -618,12 +618,21 @@ class FritzBoxDriver(ResidentialGatewayDriver): TR-064 does not expose a live connection-tracking table; this derives static NAT-PT entries from the enabled port forwarding rules, which is the closest equivalent FritzBox provides. + + IPv6 pinholes are skipped. ``get_port_forwards`` reports them alongside + the IPv4 mappings because both are inbound rules a user configured, but + a pinhole is a firewall hole, not a translation — IPv6 does not do NAT. + Including them produced entries pairing an IPv6 host with the IPv4 WAN + address, and an ``inside_local`` of ``2001:db8::10:22`` where the last + colon is a port separator and the rest is an address. """ ext_ip = self._wan_external_ip() translations: list[NATTranslationDict] = [] for forward in self.get_port_forwards(): if not forward["enabled"]: continue + if ":" in forward["internal_ip"]: + continue remote = forward.get("remote_host") or "0.0.0.0" translations.append( { diff --git a/tests/unit/test_driver.py b/tests/unit/test_driver.py index 9f2edc4..6f1e761 100644 --- a/tests/unit/test_driver.py +++ b/tests/unit/test_driver.py @@ -480,8 +480,15 @@ class TestGetWanStatus: class TestGetPortForwards: - def test_returns_all_rules(self, driver): - assert len(driver.get_port_forwards()) == 1 + def test_returns_ipv4_mappings_and_ipv6_pinholes(self, driver): + """One IPv4 mapping plus one IPv6 pinhole. + + This asserted 1 from before pinhole support existed, which put it in + direct contradiction with TestIPv6Pinholes further down the same file. + """ + forwards = driver.get_port_forwards() + assert len(forwards) == 2 + assert {f["protocol"] for f in forwards} == {"TCP", "TCP6"} def test_rule_fields(self, driver): forward = driver.get_port_forwards()[0] @@ -497,7 +504,10 @@ class TestGetPortForwards: class TestGetNatTranslations: - def test_derived_from_enabled_port_forwards(self, driver): + def test_derived_from_enabled_ipv4_port_forwards(self, driver): + """Only the IPv4 mapping. IPv6 pinholes are firewall holes, not + translations, and IPv6 does not do NAT — including them paired an IPv6 + host with the IPv4 WAN address.""" translations = driver.get_nat_translations() assert len(translations) == 1 assert translations[0]["protocol"] == "tcp" @@ -740,6 +750,12 @@ class TestIPv6Pinholes: assert ssh6["internal_port"] == 22 assert ssh6["enabled"] is True + def test_pinholes_are_not_reported_as_nat_translations(self, driver): + """A pinhole opens the firewall for a globally routable address; there + is nothing to translate.""" + assert all(":" not in t["inside_local"].rsplit(":", 1)[0] + for t in driver.get_nat_translations()) + def test_no_pinholes_without_ipv6_service(self, driver): del driver.fc.services["WANIPv6Firewall1"] forwards = driver.get_port_forwards()