feat: port forwards are a firewall reader too, and only the WAN's

get_port_forwards was declared on ResidentialGatewayDriver alone, as if a
port forward were a home-router feature. A firewall forwards ports just the
same (OPNsense calls it destination NAT), and netOrk asks both: is this host
reachable from the internet, which CVEs are exposed. The declaration moves to
NatVpnMixin, where the two roles already overlap, and PortForwardDict next to
NATTranslationDict.

The contract now says what counts. Destination NAT between internal networks
and rules that only exempt traffic are not port forwards: callers read every
entry as "reachable from outside". "ANY" forwards every protocol and an
external port of 0 every port -- a whole host forwarded is the most exposed
case and must not fall out for lack of a port number.

Declaration only, under TYPE_CHECKING: nothing changes at runtime.
This commit is contained in:
2026-10-03 16:30:32 +02:00
parent 7b491164a2
commit 31949eca0a
5 changed files with 105 additions and 46 deletions
+39
View File
@@ -0,0 +1,39 @@
"""get_port_forwards: what the WAN side may reach inside, on any gateway.
The reader used to be declared on ``ResidentialGatewayDriver`` only, as if a
port forward were a home-router feature. A firewall forwards ports just the
same -- OPNsense calls it destination NAT -- and the two consumers that ask
(is this host reachable from the internet, which CVEs are exposed) need the
answer from both. The declaration therefore lives where the two roles overlap,
next to the NAT translations reader.
"""
from __future__ import annotations
import inspect
from napalm_device_types import FirewallDriver, ResidentialGatewayDriver
from napalm_device_types.nat_vpn import NatVpnMixin
def test_a_firewall_and_a_gateway_share_the_declaration():
assert issubclass(FirewallDriver, NatVpnMixin)
assert issubclass(ResidentialGatewayDriver, NatVpnMixin)
assert "def get_port_forwards(self) -> List[PortForwardDict]" in inspect.getsource(NatVpnMixin)
def test_it_is_declared_once():
assert "def get_port_forwards" not in inspect.getsource(ResidentialGatewayDriver)
def test_absent_until_a_driver_implements_it():
assert not hasattr(FirewallDriver, "get_port_forwards")
assert not hasattr(ResidentialGatewayDriver, "get_port_forwards")
def test_the_contract_says_what_counts():
"""A redirect between two internal networks is destination NAT too, and
would make an internal host look reachable from the internet."""
source = inspect.getsource(NatVpnMixin)
assert "from the WAN" in source
assert "between internal networks" in source