Wheelhouse docs

Coming from OPNsense#

tools/opnsense-import.py turns an OPNsense config.xml into VyOS set commands plus a report of what it did. Nothing is applied by the importer, and the commands come out in the order a person would type them. From there you either build a seed that installs the replacement machine unattended, or apply the commands to a router you installed by hand. Afterwards the same tool compares the running router against the configuration it was supposed to get.

Six steps, in order.

1. Export the configuration#

Take a config.xml off the OPNsense box. The importer only reads it; it never talks to the router.

2. Import it#

bash
tools/opnsense-import.py config.xml --map lan=eth0,wan=eth1 --out ./out

That writes two files into ./out: config.commands, the set lines, and report.md, what came across and what did not.

Pass --map. FreeBSD calls a port igc0 and Linux calls it eth0, and nothing in the file says which cable is in which socket. Give the mapping by role (lan=eth0) or by device (igc0=eth0). Without it, ports are taken in the order the router lists them.

FlagWhat it does
--map lan=eth0,wan=eth1interface mapping, by role or by device
--hostname <name>override the host name from the file
--out <dir>write config.commands and report.md there; without it, the commands go to standard output
--answers <file>also write a Wheelhouse installer answers file
--no-firewall, --no-dhcp, --no-dns, --no-qosskip that part of the translation
--check-against <file>compare a live router's commands with the ones this import expects

What comes across#

The host name, domain, resolvers and time zone; interface addresses, static and DHCP; static routes, resolving OPNsense gateway names to their next hop; outbound NAT and every port forward, with host and port aliases resolved; a default-drop firewall that admits exactly what those forwards need; the DHCP server with its pool, options, PXE handoff and every reservation — including the resolvers and the router pinned to an individual one; DNS forwarding with its static host entries and its wildcard records; the WAN egress shaper; and the UPnP settings, which are agent settings here rather than router configuration and so are written beside the commands as upnp.json.

3. Read the report before you boot anything#

report.md names what could not come across rather than dropping it silently: FreeBSD-only plugins, NAT reflection, the old router's own firewall rules, and anything the router had disabled.

Two entries matter to most OPNsense operators:

  • UPnP is an agent setting here, not router configuration. Since 0.8.0 the agent speaks UPnP IGD and NAT-PMP itself, and every mapping it grants is a commit to this router's own configuration. So the importer writes the old router's settings to upnp.json beside the commands, with the WAN interface, the LAN interface and the client subnets filled in, and you apply it after the install under Services → UPnP or with PUT /api/upnp. Read the UPnP page in the product documentation first: it is off by default, and turning it on decides who on your LAN may open a port.
  • 1:1 NAT is made on the NAT page. The importer does not carry 1:1 mappings across. Add each one under Protect → NAT → Add 1:1 NAT after the install: Wheelhouse builds it as a pair of NAT rules, numbered ahead of the port forwards the import brought, and applies it from the Commit Bar like any other change.

4. Build a seed#

The seed is what makes the replacement machine install itself with your configuration already in it.

bash
tools/wheelhouse-seed.py --opnsense config.xml --map lan=eth0,wan=eth1 \
    --admin-password-file admin.pw --console-password-file console.pw \
    --out seed.iso

That runs the importer for you, puts config.commands on the seed, copies the report on as import-report.md, and writes a cloud-config that installs the machine and restores the configuration. It keeps the router's own host name and time zone from the imported configuration unless you override them.

It also decides who writes the firewall, and says so in the seed in as many words: if the imported configuration brings a default-drop ruleset, the installer's own firewall is turned off, because two writers numbering the same rules differently produces a configuration the router refuses. If nothing came across, the installer writes its default-deny ruleset instead.

--include-source puts the config.xml on the seed as opnsense-config.xml, so the machine can check itself against the router it replaced with nothing else to fetch.

5. Install#

Write the seed to a stick or attach it as a second disk, boot the Wheelhouse image with it present, and the machine installs itself. Installing without a keyboard has the seed model in full: where the installer looks, the once-per-boot rule, and the two guards that stop a seed erasing a disk twice.

The configuration is applied after the passwords are set, so a restored firewall can be as closed as you like without locking the installer out of the agent.

6. Check the new router against the old one#

On a running machine the importer ships as wheelhouse-opnsense-import, and --check-against compares a live router with the configuration it should have:

bash
show configuration commands > /tmp/live
wheelhouse-opnsense-import config.xml --map lan=eth0,wan=eth1 --check-against /tmp/live

It prints every command the import expects and the router is missing, and exits non-zero if there are any — which makes it a reasonable thing to run from a cron job after a migration until you trust it. Quoting is ignored, because a router quotes the values it prints, as are the settings VyOS writes for itself on every interface.

show configuration commands is a shell function of the operational CLI, so over SSH it needs the wrapper:

bash
/opt/vyatta/bin/vyatta-op-cmd-wrapper show configuration commands > /tmp/live

On a machine installed from a seed built with --include-source, the whole check is two commands with nothing to fetch:

bash
sudo mount /dev/sr1 /mnt
/opt/vyatta/bin/vyatta-op-cmd-wrapper show configuration commands > /tmp/live
sudo wheelhouse-opnsense-import /mnt/opnsense-config.xml --map lan=eth1,wan=eth0 \
    --hostname "$(hostname)" --check-against /tmp/live

What to expect to be different#

The vocabulary differs. Nothing on any page applies itself: an editor adds its change to one list of pending changes, where it reads as a sentence, and one Commit Bar applies the lot. What OPNsense calls a plugin is an app here, and installing one declares a container in the router's configuration tree — a diff you review, a commit you can roll back, and state that survives an image upgrade.

Two differences change how you work:

  • IPv6 is configured and observed on its own page. Network → IPv6 edits addressing, router advertisements and prefix delegation, and shows v6 addresses, neighbours, routes and the ip6 firewall counters; Static routes edits v6 routes. The neighbour tables still read the IPv4 table, and the uplink model has no v6 concept. The installer does write a v6 ruleset.
  • Packet capture is in the UI. Diagnostics → Capture runs a bounded tcpdump on the router and downloads the pcap. POST /api/capture answers 501 only where it cannot run: an agent that is not on the router, or a router with no tcpdump.

See also#

Updated 2026-09-18 migration opnsense importer seed