5 Commits
Author SHA1 Message Date
christianmanivong 0f172f02c0 Merge pull request 'feat(firewall): report the gateway a filter rule policy-routes to' (#7) from feat/firewall-rule-gateway into master
CI / test (3.10) (push) Successful in 30s
CI / test (3.11) (push) Successful in 30s
CI / test (3.12) (push) Successful in 33s
2026-10-05 04:47:55 +00:00
Christian Manivong fffdd95e6a feat(firewall): report the gateway a filter rule policy-routes to
CI / test (3.10) (push) Successful in 57s
CI / test (3.11) (push) Successful in 35s
CI / test (3.12) (push) Successful in 33s
CI / test (3.10) (pull_request) Successful in 33s
CI / test (3.11) (pull_request) Successful in 31s
CI / test (3.12) (pull_request) Successful in 35s
get_firewall_rules read searchRule but dropped the rule's gateway. A pass
rule with a gateway hands what it matches to that gateway, local
destinations included, so it does not reach a host on another internal
network. Without the field a caller judging reachability reads a rule meant
for internet traffic as a hole into every server: on the first real box,
"pass UDP IOT -> any" via WAN_GW made 61 hosts look reachable from the IoT
segment (netOrk #575).

The rule dict gains "gateway", the gateway's name or "". It is an extra
field like floating and interface_label; the generic diff compares a fixed
field list and ignores it.
2026-10-05 06:34:47 +02:00
christianmanivong e53cc8d402 Merge pull request 'ci: install napalm-device-types from git, so the tests run at all' (#6) from fix/ci-device-types-from-git into master
CI / test (3.10) (push) Successful in 29s
CI / test (3.11) (push) Successful in 27s
CI / test (3.12) (push) Successful in 29s
2026-10-04 00:00:36 +00:00
Christian Manivong 70fc5f7043 ci: upload-artifact@v3, the version Gitea supports
CI / test (3.10) (push) Successful in 46s
CI / test (3.11) (push) Successful in 28s
CI / test (3.12) (push) Successful in 29s
CI / test (3.10) (pull_request) Successful in 29s
CI / test (3.11) (pull_request) Successful in 27s
CI / test (3.12) (pull_request) Successful in 29s
With the install fixed, the tests ran and passed on 3.10-3.12, and the job
then failed at the last step: upload-artifact@v4 refuses to run on Gitea
(GHESNotSupportedError).
2026-10-04 01:14:24 +02:00
Christian Manivong c8d63e87e4 ci: install napalm-device-types from git, so the tests run at all
CI / test (3.10) (push) Failing after 56s
CI / test (3.11) (push) Failing after 50s
CI / test (3.12) (push) Failing after 43s
CI / test (3.10) (pull_request) Failing after 31s
CI / test (3.11) (pull_request) Failing after 33s
CI / test (3.12) (pull_request) Failing after 37s
Every CI run died at "pip install -e .[dev]": pyproject.toml asks for
napalm_device_types, which lives in git.netork.io/NAPALM rather than on PyPI,
and pip found only an unrelated 0.1.0 there. Not one test had run in CI.

The workflow now installs napalm-device-types from git first. The matrix
drops 3.8/3.9 and requires-python says >=3.10, because napalm-device-types
itself needs 3.10 -- the package never installed on anything older.

Replayed in a fresh venv: napalm-device-types 2.0.0 from git, then the
package with its dev extras, tests green.
2026-10-04 00:43:45 +02:00
4 changed files with 90 additions and 4 deletions
+6 -2
View File
@@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12"]
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -26,6 +26,9 @@ jobs:
- name: Install package with dev extras
run: |
python -m pip install --upgrade pip
# napalm-device-types lives in git.netork.io/NAPALM, not on PyPI: without this
# pip looks there, finds an unrelated 0.1.0 and the job dies before any test.
python -m pip install "napalm-device-types @ git+https://git.netork.io/NAPALM/napalm-device-types.git"
python -m pip install -e ".[dev]"
- name: Run unit tests
@@ -38,7 +41,8 @@ jobs:
python -m build
- name: Upload dist artifacts
uses: actions/upload-artifact@v4
# v4 refuses to run on Gitea ("not currently supported on GHES").
uses: actions/upload-artifact@v3
with:
name: dist-${{ matrix.python-version }}
path: dist/*
+5
View File
@@ -2444,6 +2444,10 @@ class OPNsenseDriver(OPNsensePingMixin, FirewallDriver):
* ``floating`` — bool, rule applies across all interfaces
* ``interface_label`` — human-readable interface description
* ``is_group`` — bool, interface is an interface group
* ``gateway`` — the gateway a pass rule policy-routes to, or
``""``. Such a rule sends what it matches to that gateway, local
destinations included, so it does not reach a host on another
internal network.
"""
try:
resp = self._get("/api/firewall/filter/searchRule?current=1&rowCount=-1")
@@ -2508,6 +2512,7 @@ class OPNsenseDriver(OPNsensePingMixin, FirewallDriver):
"log": str(row.get("log", "0")) == "1",
"enabled": str(row.get("enabled", "1")) == "1",
"category": category,
"gateway": row.get("gateway", "") or "",
})
return sorted(result, key=lambda x: (x["floating"], x["is_group"], x["interface"], x["sequence"]))
+1 -2
View File
@@ -8,7 +8,7 @@ version = "0.1.0"
description = "NAPALM driver for OPNsense (read-only via REST API)."
readme = "README.md"
license = { text = "Apache-2.0" }
requires-python = ">=3.9"
requires-python = ">=3.10"
authors = [
{ name = "Christian Manivong" },
]
@@ -16,7 +16,6 @@ classifiers = [
"Topic :: Utilities",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+78
View File
@@ -0,0 +1,78 @@
"""Filter rules as ``get_firewall_rules`` reports them.
A pass rule with a gateway is policy routing: OPNsense hands what it matches to
that gateway, local destinations included. A caller judging which network can
reach which host has to know that, or a rule meant for internet traffic reads
as a hole into every server (netOrk #575: on the first real box, "pass UDP
IOT -> any" via WAN_GW made 61 hosts look reachable from the IoT segment).
The row shape follows that box's ``/api/firewall/filter/searchRule``.
"""
from __future__ import annotations
from unittest.mock import patch
import pytest
from napalm_opnsense.opnsense import OPNsenseDriver
@pytest.fixture
def driver():
with patch("napalm_opnsense.opnsense.requests.Session"):
yield OPNsenseDriver(
hostname="opnsense.example.com",
username="api_key",
password="api_secret",
optional_args={"verify": False},
)
def _row(**over) -> dict:
"""One rule row as the API returns it; booleans are "0"/"1" strings."""
row = {
"uuid": "u1",
"sequence": "1",
"action": "pass",
"quick": "1",
"interface": "opt3",
"%interface": "IOT",
"direction": "in",
"ipprotocol": "inet",
"protocol": "UDP",
"%source_net": "HOME_OFFICE_IOT_NET",
"%destination_net": "any",
"destination_port": "",
"description": "reolink_udp_long_state_timeout",
"enabled": "1",
"gateway": "WAN_GW",
}
row.update(over)
return row
def _rules(driver, *rows):
def fake_get(path):
return {"rows": list(rows)} if "searchRule" in path else {"rows": []}
with patch.object(driver, "_get", side_effect=fake_get):
return driver.get_firewall_rules()
def test_a_policy_routed_rule_reports_its_gateway(driver):
[rule] = _rules(driver, _row(gateway="WAN_GW"))
assert rule["gateway"] == "WAN_GW"
def test_a_rule_that_routes_normally_reports_no_gateway(driver):
[rule] = _rules(driver, _row(gateway=""))
assert rule["gateway"] == ""
def test_a_row_without_the_field_reports_no_gateway(driver):
# Older firmware, or a rule type that never carries one.
row = _row()
del row["gateway"]
[rule] = _rules(driver, row)
assert rule["gateway"] == ""