# 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 ```bash pip install napalm-opnsense ``` Or from source: ```bash git clone https://github.com/napalm-automation-community/napalm-opnsense cd napalm-opnsense pip install -e . ``` ## Quick start ```python 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`: ```python 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. ```json [ { "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 ```python 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://` | Override the base URL | | `verify` | `True` | TLS certificate verification (path or bool) | ## Development ```bash 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](.github/workflows/ci.yml). ## License Apache 2.0 — see [LICENSE](LICENSE).