Christian Manivong 3506f20606
CI / test (3.10) (push) Failing after 1m39s
CI / test (3.11) (push) Failing after 25s
CI / test (3.12) (push) Failing after 12s
CI / test (3.9) (push) Failing after 31s
CI / test (3.10) (pull_request) Failing after 13s
CI / test (3.11) (pull_request) Failing after 12s
CI / test (3.12) (pull_request) Failing after 13s
CI / test (3.9) (pull_request) Failing after 12s
feat: port forwards, read from destination NAT on the WAN
get_port_forwards reads /api/firewall/d_nat/search_rule and keeps only what
the contract asks for: rules on an interface with an upstream gateway (the
WAN, and a second uplink as well). Internal redirects, anti-lockout rules
(nordr) and rules the captive portal generates are left out -- on the first
real box (OPNsense 26.7) that was 20 of 22 rules, and each would have made an
internal host look reachable from the internet.

Targets resolve through host/network aliases, one entry per address; an
interface address or a DNS name gives no address and the rule is skipped
rather than put on a guessed host. Ports resolve as numbers, the start of a
range, port aliases or service names; no port is every port (0), and tcp/udp
is two entries. The filtering is pure, in port_forwards.py, and the driver
method does the three reads.

A box without the destination-NAT API raises instead of answering "nothing
forwarded", which nobody checked.
2026-10-03 16:30:38 +02:00
2026-05-29 09:22:10 +02:00
2026-05-29 09:22:10 +02:00
2026-05-29 09:22:10 +02:00
2026-05-29 09:22:10 +02:00

napalm-opnsense

NAPALM community driver for OPNsense firewalls (read-only, via REST API).

Tested devices

Model OPNsense Version Tested
OPNsense (virtual/bare metal) 24.7 ✅

Additional OPNsense versions should work — contributions welcome.

Requirements

Dependency Minimum version
Python 3.9
NAPALM 4.0
requests 2.28

Installation

pip install napalm-opnsense

Or from source:

git clone https://github.com/napalm-automation-community/napalm-opnsense
cd napalm-opnsense
pip install -e .

Quick start

from napalm import get_network_driver

driver = get_network_driver("opnsense")
with driver(
    "192.168.1.1",
    "my_api_key",
    "my_api_secret",
    optional_args={"verify": False},
) as device:
    facts = device.get_facts()
    print(facts)

Authentication

OPNsense uses API key/secret pairs instead of username/password. Generate a key pair in the OPNsense GUI under System → Access → Users → edit user → API keys.

Pass the credentials via optional_args:

optional_args={
    "api_key": "your-api-key",
    "api_secret": "your-api-secret",
    "verify": False,  # set to a CA bundle path or True in production
}

Alternatively, pass the key/secret as the positional username/password arguments.

Implemented getters

Getter Status OPNsense API Endpoint
get_facts ✅ GET /api/core/system/status
get_interfaces ✅ GET /api/interfaces/overview/export
get_interfaces_ip ✅ GET /api/interfaces/addresses/export
get_interfaces_counters ✅ GET /api/diagnostics/interface/get_interface_statistics
get_arp_table ✅ GET /api/diagnostics/interface/get_arp
get_ipv6_neighbors_table ✅ GET /api/diagnostics/interface/get_ndp
get_route_to ✅ GET /api/diagnostics/interface/get_routes
get_environment ✅ GET /api/diagnostics/system/system_resources + system_temperature
get_lldp_neighbors ✅ ¹ GET /api/lldpd/service/neighbor
get_lldp_neighbors_detail ✅ ¹ GET /api/lldpd/service/neighbor
get_ntp_servers ✅ GET /api/ntpd/service/status
get_config ✅ GET /api/core/backup/download/this (XML)
is_alive ✅ TCP socket check
get_bgp_neighbors ✅ ² GET /api/quagga/bgp/get + GET /api/quagga/diagnostics/bgpneighbors
get_vlans ✅ GET /api/interfaces/vlan_settings/search_item
get_mac_address_table ❌ Not applicable (firewall, no L2 switching)
ping ✅ POST /api/diagnostics/ping/set + start + search_jobs + stop/remove
ping_sweep ✅ ³ same endpoints, one batch of parallel jobs at a time

¹ Requires the os-lldpd plugin. Returns empty dict if the plugin is not installed. ² Requires the os-frr (FRR/Quagga) plugin. Returns empty dict if the plugin is not installed or FRR is not running. ³ Overrides the generic per-host loop from napalm-device-types.

Ping

OPNsense has no synchronous ping endpoint: /api/diagnostics/ping is a job API — create, start, read statistics, stop, remove. A single ping therefore costs five requests and about a second of waiting, which makes the generic sequential sweep from napalm-device-types unusable for a whole subnet.

ping_sweep exploits what the job API does offer instead: jobs are independent and run on the firewall in parallel, and search_jobs reports all of them in one response. It creates and starts a batch (PING_SWEEP_BATCH_SIZE, default 32), waits once, reads every result with a single request, then cleans the batch up — waiting time per batch is constant rather than linear in hosts. PING_SWEEP_MAX_TARGETS (default 512) bounds the sweep as a whole; both are deliberately conservative, since this runs on production firewalls.

Results are polled rather than read once: search_jobs signals the running ping with SIGINFO and parses whatever it has written so far, so the statistics line lands in the log slightly after the request that triggered it.

ttl and vrf are accepted for NAPALM compatibility and ignored — the API has no equivalent.

Config management

OPNsense does not expose a single generic "push config" endpoint. Config is managed per-module via separate API controllers. This driver implements config management for static routes via /api/routes/routes/.

Why routes? Routes are the most common network-automation target on a firewall, and the OPNsense routes API provides full CRUD operations.

Supported methods

Method OPNsense API
load_merge_candidate(config=...) stages routes in memory
compare_config() diffs against GET /api/routes/routes/searchroute
commit_config() snapshots backup → POST /api/routes/routes/addroute × n → reconfigure
discard_config() clears staged candidate
rollback() POST /api/core/backup/revert_backup/{id} — restores the config.xml snapshot taken before the last commit

Config format

The load_merge_candidate config parameter must be a JSON array of route objects, each with network and gateway keys. gateway must be the name of an existing OPNsense gateway (as configured under System → Gateways → Configuration), not an IP address.

[
  {
    "network": "10.0.0.0/8",
    "gateway": "WAN_GW",
    "descr": "Corporate internal",
    "disabled": "0"
  },
  {
    "network": "0.0.0.0/0",
    "gateway": "WAN_GW",
    "descr": "Default route"
  }
]

Example

import json

driver = get_network_driver("opnsense")
d = driver("192.168.1.1", "user", "pass", optional_args={"api_key": "k", "api_secret": "s"})
d.open()

routes = json.dumps([{"network": "10.0.0.0/8", "gateway": "WAN_GW"}])
d.load_merge_candidate(config=routes)

print(d.compare_config())   # unified diff
d.commit_config()           # applies routes and calls reconfigure
d.rollback()                # removes the routes just added
d.close()

Limitations

  • load_replace_candidate is not supported (no XML upload endpoint in the JSON API).
  • Non-route config (firewall rules, DHCP, DNS, interfaces, …) must be managed via OPNsense module-specific controllers — outside the scope of this driver.
  • rollback() reverts the entire config.xml to the pre-commit state (not just the routes). OPNsense's backup API has no partial-restore capability.
  • Rollback uses the backup snapshot taken at commit_config() time. If no commit was made in the current session, the most recent available backup is used as a fallback.

Optional arguments

Argument Default Description
api_key username OPNsense API key
api_secret password OPNsense API secret
base_url https://<hostname> Override the base URL
verify True TLS certificate verification (path or bool)

Development

pip install -e ".[dev]"
pytest tests/

CI

This project includes a GitHub Actions workflow that:

  • Runs unit tests across Python 3.9–3.12
  • Builds sdist and wheel
  • Uploads build artifacts

See .github/workflows/ci.yml.

License

Apache 2.0 — see LICENSE.

S
Description
No description provided
Readme
203 KiB
Languages
Python 100%