Turn a config.xml into VyOS commands#
You will end up with two files: config.commands, a list of VyOS set lines in the
order a person would type them, and report.md, which says what came across, what came
across differently, and what could not come across at all. Neither file touches a router.
Applying them is a separate step — a seed during an install, or a
load at the console on a box that is already running.
The importer is tools/opnsense-import.py: Python 3
with no third-party imports, so it runs on the laptop you exported the configuration to.
On a Wheelhouse router the same script is installed as wheelhouse-opnsense-import.
Before you start#
- The OPNsense
config.xmlfrom the router you are replacing. System → Configuration → Backups → Download configuration. Do not encrypt it — the importer parses XML, not a password-wrapped blob. - A decision about which cable goes in which socket on the new box. That is what
--mapis for, and it is the one answer nothing in the file can give you. - Python 3 on whatever machine you run this on.
Step 1 — Name the ports#
FreeBSD calls a port igc0; Linux calls it eth0. Nothing in the configuration file
records which physical socket that was, so the importer either takes your mapping or
assigns ports in the order the old router listed them and says so in the report.
Map by OPNsense role, which is what most people mean:
tools/opnsense-import.py config.xml --map lan=eth0,wan=eth1 --out ./outOr by the FreeBSD device name, when the roles are confusing and the sockets are not:
tools/opnsense-import.py config.xml --map igc0=eth0,igc1=eth1,igc2=eth2 --out ./outWithout --map, the ports the old router lists — filtered to real Ethernet devices,
matched on igc, em, ix, re, bge, vtnet, eth or en followed by a digit —
become eth0, eth1, eth2 and so on in that order, and each one gets a line in the
report's Translated differently section naming the substitution.
Step 2 — Run it#
tools/opnsense-import.py config.xml --map lan=eth0,wan=eth1 --out ./outwrote ./out/config.commands (147 commands)
wrote ./out/report.md
translated 31, differently 3, not at all 9With no --out the commands go to standard output and no report is written, which is
useful for a quick look and nothing else. The four --no- switches turn off a whole
area when you would rather write it yourself:
| Flag | Effect |
|---|---|
--map lan=eth0,wan=eth1 | Interface mapping, by role or by device name |
--hostname edge | Override the host name from the file |
--out ./out | Write config.commands and report.md into this directory |
--answers ./out/answers.conf | Also write an installer answers file (see Build a seed) |
--no-firewall | Do not write a ruleset. The report records that you asked for this. |
--no-dhcp | Skip the DHCP server |
--no-dns | Skip DNS forwarding |
--no-qos | Skip the shaper |
--check-against live.txt | Compare instead of generating — see Verify a migration |
If the file is not an OPNsense configuration the importer stops before doing anything:
it checks that the root element is <opnsense> and reports the element it actually
found.
Step 3 — Read what it wrote#
config.commands is grouped by area, with a comment line before each group, in the order
the importer runs them: interfaces, system, static routes, NAT, firewall, the DHCP
server, DNS forwarding, QoS. Interfaces come first because several later sections need
the LAN address to work out a network to allow, a router to hand out, or an address to
listen on.
# ===== interfaces =====
set interfaces ethernet eth0 address 192.0.2.1/24
set interfaces ethernet eth0 description LAN
set interfaces ethernet eth1 address dhcp
set interfaces ethernet eth1 description WAN
# ===== system =====
set system host-name edge
set system domain-name example.com
set system name-server 9.9.9.9
set system time-zone Europe/Berlin
set system config-management commit-revisions 100
# ===== NAT =====
set nat source rule 100 description 'LAN to WAN'
set nat source rule 100 outbound-interface name eth1
set nat source rule 100 source address 192.0.2.0/24
set nat source rule 100 translation address masquerade
set nat destination rule 110 description 'web'
set nat destination rule 110 inbound-interface name eth1
set nat destination rule 110 protocol tcp
set nat destination rule 110 destination port 443
set nat destination rule 110 translation address 192.0.2.10Read report.md next, and read all of it. That page explains what
each of its three sections means and what to do about the entries in the third.
What the importer translates, in detail#
Interfaces#
Every enabled Ethernet port becomes set interfaces ethernet <name> address … plus a
description taken from the old router's label, or the role in upper case when there is
none. dhcp comes across as dhcp; a static address is joined with its subnet value,
defaulting to /24 when the file does not carry one.
Ports that are not Ethernet — an OpenVPN interface, a WireGuard interface, tailscale0
— are named in the report as not translated, because those are configured by their own
service and not by an interface line. Loopback is skipped silently. A port that was
disabled on the old router is skipped and reported.
System#
Host name, domain name, every <dnsserver> and the time zone, plus one line the old
router had no equivalent for:
set system config-management commit-revisions 100That is VyOS' configuration archive — the history behind the Restore buttons on the System page. A hundred revisions is what the importer asks for.
SSH comes across only if it was enabled, as service ssh port <n>. If the old router
answered SSH on the LAN interface, the importer binds it to the LAN address rather than
every interface. It never binds SSH to the WAN.
Local user accounts do not come across. Password hashes do not carry between FreeBSD and Linux, and the report says so with a count. The installer sets the console password and the web UI admin password; every other person needs an account of their own.
Static routes#
Each enabled route becomes set protocols static route <network> next-hop <address>.
OPNsense names its gateways and refers to them by name, so the importer reads
OPNsense/Gateways and resolves the name to the address behind it. A gateway with no
address of its own — a dynamic one, or one bound to an interface — cannot be resolved,
and the route is reported rather than guessed at.
NAT#
Outbound NAT comes across when the old router's outbound mode was automatic or
hybrid, as one masquerade rule:
set nat source rule 100 description 'LAN to WAN'
set nat source rule 100 outbound-interface name eth1
set nat source rule 100 source address 192.0.2.0/24
set nat source rule 100 translation address masqueradeA hand-written outbound ruleset (manual mode) is not translated. Those rules exist
because somebody had a reason, and a machine guessing at the reason is worse than a line
in the report.
Port forwards become nat destination rules numbered from 110 in steps of 10, sorted
by destination port. Sorting by port rather than by the old router's order means the
table reads sensibly and a second import of the same file produces the same rule numbers,
which is what makes --check-against useful later.
Host and port aliases are resolved: an alias of type host or network holding exactly
one value becomes that value, and a port alias becomes its list, joined with commas.
OPNsense writes port ranges with a colon; VyOS wants a hyphen, and the importer converts.
A forward whose destination interface was not the WAN is still imported, as arriving on the WAN, with a line in Translated differently naming the interface it actually matched on.
Firewall#
The importer does not translate the old ruleset. It writes a new default-drop one that admits exactly what the rest of the import needs, on both address families:
set firewall global-options state-policy established action accept
set firewall global-options state-policy related action accept
set firewall global-options state-policy invalid action drop
set firewall ipv4 input filter default-action drop
set firewall ipv4 input filter rule 10 action accept # from the LAN
set firewall ipv4 input filter rule 20 action accept # from lo
set firewall ipv4 input filter rule 30 action accept # udp/68, the DHCP client
set firewall ipv4 input filter rule 40 action accept # icmp
set firewall ipv4 forward filter default-action drop
set firewall ipv4 forward filter rule 10 action accept # LAN out
set firewall ipv4 forward filter rule 20 action accept # to a forwarded host
set firewall ipv4 forward filter rule 21 action accept # to the next one
set firewall ipv6 input filter default-action drop
set firewall ipv6 input filter rule 10 action accept # from the LAN
set firewall ipv6 input filter rule 20 action accept # from lo
set firewall ipv6 input filter rule 30 action accept # icmpv6, all of it
set firewall ipv6 input filter rule 40 action accept # udp/546, DHCPv6
set firewall ipv6 forward filter default-action drop
set firewall ipv6 forward filter rule 10 action accept # LAN out
set firewall ipv6 forward filter rule 20 action accept # icmpv6 throughFour things about that ruleset are decisions rather than translations, and the report records them.
- The old router's rules are not read. Not the IPv4 ones and not the IPv6 ones. What you get is a posture, not a port of your policy. Everything you allowed beyond a port forward — a management VLAN reaching another segment, a printer reachable from guests — you re-create yourself, from the Firewall page or with Use groups instead of literals.
- IPv6 gets the same default-deny even though nothing was read from the old configuration. A router filtered on one family and open on the other is not filtered, and it is the family nobody looks at that gets left open. If the provider brings IPv6 up later, the box is closed rather than surprised.
- All of ICMPv6 is accepted on input, not just echo. Neighbour discovery and path MTU discovery are ICMPv6; a router that drops them loses its own default route and then breaks every large packet it forwards.
- One forward-chain accept per forwarded host, numbered from 20 upwards in steps of one. The destination NAT has already rewritten the address by the time the packet is filtered, so the accept matches the inside address, not the public one. This is the same pairing the publish a service guide describes.
If no LAN interface came across, the importer refuses to write a ruleset at all and says
why: a default-drop ruleset with nothing to admit is a locked door with the key inside.
--no-firewall has the same effect and is also recorded.
Rules the old router had on an interface that is not part of this import — an OPT interface you did not map — are named in the report one by one.
DHCP#
The LAN's DHCP scope becomes a VyOS shared network called LAN:
set service dhcp-server shared-network-name LAN subnet 192.0.2.0/24 subnet-id 1
set service dhcp-server shared-network-name LAN subnet 192.0.2.0/24 option default-router 192.0.2.1
set service dhcp-server shared-network-name LAN subnet 192.0.2.0/24 option name-server 192.0.2.1
set service dhcp-server shared-network-name LAN subnet 192.0.2.0/24 option domain-name example.com
set service dhcp-server shared-network-name LAN subnet 192.0.2.0/24 lease 7200
set service dhcp-server shared-network-name LAN subnet 192.0.2.0/24 range 0 start 192.0.2.100
set service dhcp-server shared-network-name LAN subnet 192.0.2.0/24 range 0 stop 192.0.2.200
set service dhcp-server shared-network-name LAN subnet 192.0.2.0/24 static-mapping printer mac 00:00:5e:00:53:01
set service dhcp-server shared-network-name LAN subnet 192.0.2.0/24 static-mapping printer ip-address 192.0.2.10The default router falls back to the LAN address when the old scope did not name one,
the resolvers fall back to the router itself, and the lease time falls back to 7200
seconds. Domain search lists come across entry by entry. Every reservation comes across,
named from its host name (or its client identifier, or its MAC with the colons removed),
sanitised to letters, digits and hyphens, with -x appended if two reservations would
otherwise collide.
PXE comes across as option bootfile-server, option tftp-server-name and
option bootfile-name, preferring the UEFI file name (filename64) over the legacy one.
If the old router handed out a different loader per client architecture, VyOS has one per
subnet, so one file name is used for every client and the report says which. Needing the
architecture matrix is what the
dnsmasq guide is for.
Resolvers pinned to an individual reservation are dropped and reported: VyOS hands out resolvers per subnet, not per reservation.
DNS#
If the old router ran dnsmasq or Unbound, the forwarder comes across:
set service dns forwarding listen-address 192.0.2.1
set service dns forwarding allow-from 192.0.2.0/24
set service dns forwarding name-server 9.9.9.9
set service dns forwarding cache-size 10000Static host entries become system static-host-mapping host-name <fqdn> inet <address>.
Two things do not come across and both are named in the report: wildcard records,
which the VyOS forwarder does not answer, and names for DHCP clients, because the
forwarder does not register leases the way dnsmasq does. Both have the same workaround —
run dnsmasq as an app — and the second has a simpler one:
give the hosts that need names a reservation and a static host mapping.
QoS#
The upload pipe from the old traffic shaper becomes a CAKE policy on the WAN's egress:
set qos policy cake WAN-EGRESS bandwidth 70mbit
set qos policy cake WAN-EGRESS rtt 100
set qos policy cake WAN-EGRESS flow-isolation dual-src-host
set qos interface eth1 egress WAN-EGRESSThe importer picks the enabled pipe whose description mentions "up", or the first enabled pipe if none does. The download pipe is deliberately not translated, and the report says why: shaping inbound traffic on the router only drops what has already crossed the uplink. See Shape a link with CAKE for what the QoS editor can and cannot express.
Check it worked#
Nothing has been applied yet, so the check is on the files.
# The interface mapping is the first thing in the file, and the thing to get right.
sed -n '/# ===== interfaces/,/# ===== system/p' out/config.commands
# Count the port forwards against the old router's.
grep -c 'translation address' out/config.commands
# Nothing should be left unquoted that has a space in it.
grep "description '" out/config.commands | head
# And read the report, all three sections of it.
less out/report.mdIf you have a Wheelhouse router already running and want to see what the commands do before committing to them, paste them into the Config tree page's search to find the subtrees they touch, or stage a handful at a time from a shell and read the Commit Bar's diff.
Undoing it#
The importer writes files. Delete the output directory. If you have already applied the
commands to a running router, rollback 1 — from
System → History or
Undo the last commit — puts the configuration back to the
commit before them, provided they were applied as one commit.
See also#
- Read the migration report — every entry in the third section is work you still have to do
- Build a seed that installs the replacement — install the new box from these files with nobody at the keyboard
- Check a running router against the configuration it should have
- Publish a service — the shape of the rules the importer wrote for your forwards
- The Firewall page and the NAT page — where the imported rules appear afterwards
Checked against tools/opnsense-import.py ·
docs/unattended-install.md ·
ui/src/pages/Nat.tsx ·
ui/src/pages/Firewall.tsx