rdomain is an interesting feature present on OpenBSD to create isolated routing domain. I rarely used this one - except for testing - but it was never deployed in production. In fact, it was pretty rare to have an OpenBSD router/firewall running outside of my own lab. A previous post was showing how to isolate a pool of workers and forward all their traffic through using onion routing using proxies. In this post, the workers traffic will be routed to a remote server using a wireguard tunnel. Both the firewall and the remote server are running on OpenBSD.

A picture is worse thousand of words, then, the infrastructure schema below should give you an idea of the network structure and the connections between the different devices. Not everything is presented though, because my network is a bit more complex than that, but this is a big picture.

Infrastructure Schema for Wireguard and rdomain on OpenBSD

Before doing anything, we should probably start to learn what is an rdomain and how to use it properly.

It's All About Isolation and Routing

rdomain appeared for the first time in OpenBSD 4.9. This goal was to offer a way to have isolated routing tables. By default, all routes are put in the rdomain 0, and it can lead to some issues when dealing with complex network. With an rdomain, an operator is able to create dynamically a new routing table isolated from the rest of the system, even if it is limited to 255 rdomain, it shold be enough for most of the use case.

Processes can be started in their own rdomain with the help of the route command and its exec subcommand without forgetting the -T flag. A way to check in which domain a process is in, is to use ps command with the rtable option. Let first checks the output of the ps command.

$ ps -o rtable,pid,ppid,%cpu,%mem,command RTABLE PID PPID %CPU %MEM COMMAND 0 59603 14917 0.0 0.0 -ksh (ksh) 0 13501 59603 0.0 0.2 tmux: client (/tmp/tmux-1000/default) (tmux) 0 68852 66599 0.0 0.0 -ksh (ksh) 0 73792 66599 0.0 0.0 -ksh (ksh) 0 75757 66599 0.0 0.0 -ksh (ksh) 0 85484 75757 0.0 0.1 ps -o rtable 0 47120 66599 0.0 0.0 -ksh (ksh) 0 86889 66599 0.0 0.0 -ksh (ksh) 

AS you can see, all processes are currently running in the default rdomain 0. To use another rdomain, the route command can be used, like it has been seen previously. Let check that by spawning a sleep in the background for 10 seconds in the rdomain 1.

$ route -T1 exec sleep 10 & [1] 96416 $ ps -o rtable,pid,ppid,%cpu,%mem,command RTABLE PID PPID %CPU %MEM COMMAND 0 59603 14917 0.0 0.0 -ksh (ksh) 0 13501 59603 0.0 0.2 tmux: client (/tmp/tmux-1000/default) (tmux) 0 68852 66599 0.0 0.0 -ksh (ksh) 0 73792 66599 0.0 0.0 -ksh (ksh) 0 46155 73792 0.0 0.0 sleep 1 0 75757 66599 0.0 0.0 -ksh (ksh) 1 96416 75757 0.0 0.0 sleep 10 0 26428 75757 0.0 0.1 ps -o rtable 0 47120 66599 0.0 0.0 -ksh (ksh) 0 86889 66599 0.0 0.0 -ksh (ksh) 

The sleep process is correctly using the rtable 1, but what kind of differences are existing between all of those routing tables? Well, the default routing table is initialized during the startup of the system, but all other rdomain should be empty. Again, the route command can be used to show that or the netstat command can be used instead, it is offering more filtering options.

$ netstat -rnT0 -finet Routing tables Internet: Destination Gateway Flags Refs Use Mtu Prio Iface default 192.168.123.254 UGS 9 10038979 - 8 re0 224/4 127.0.0.1 URS 0 0 32768 8 lo0 10.255.255.240/28 10.255.255.253 UCn 1 0 - ... 

An existing rdomain 1 is currently present on my test example. As you can see, the output returned by netstat is totally different than the one from the rdomain 1.

$ netstat -rnT1 -f inet Routing tables Internet: Destination Gateway Flags Refs Use Mtu Prio Iface default 172.17.0.1 UGS 0 17202 - 8 wg0 10.255.255.240/28 10.255.255.254 UCn 1 0 - 4 pair1 10.255.255.253 fe:e1:ba:d5:62:48 UHLch 1 6 - 3 pair1 10.255.255.254 fe:e1:ba:d6:4a:44 UHLl 0 4 - 1 pair1 10.255.255.255 10.255.255.254 UHb 0 0 - 1 pair1 127.0.0.1 127.0.0.1 UHl 0 315 32768 1 lo1 ... 

The rdomain 2 does not exist though, and when trying to show its content, an error is returned by netstat. The same error is returned to any unitialized rdomains.

$ netstat -rnT2 netstat: routing table 2: No such file or directory 
$ netstat -rnT3 netstat: routing table 3: No such file or directory 

rdomains can be created when dealing with network interfaces and pseudo devices. For example, let created rdomain 2 and rdomain 3 by defining lo2 and lo3 loopback interfaces.

$ doas ifconfig lo2 create rdomain 2 alias 127.0.0.1 up  $ doas ifconfig lo2 lo2: flags=2008049<UP,LOOPBACK,RUNNING,MULTICAST,LRO> rdomain 2 mtu 32768  index 26 priority 0 llprio 3 groups: lo inet6 ::1 prefixlen 128 inet6 fe80::1%lo2 prefixlen 64 scopeid 0x1a inet 127.0.0.1 netmask 0xff000000 
$ doas ifconfig lo3 create rdomain 3 alias 127.0.0.1 up  $ doas ifconfig lo3 lo3: flags=2008049<UP,LOOPBACK,RUNNING,MULTICAST,LRO> rdomain 3 mtu 32768  index 27 priority 0 llprio 3 groups: lo inet6 ::1 prefixlen 128 inet6 fe80::1%lo3 prefixlen 64 scopeid 0x1b inet 127.0.0.1 netmask 0xff000000 

Both interfaces are totally isolated in their own address space, with its own routing table, this is why the same IP address (here 127.0.0.1) can be set. To extend the example, 2 vether interfaces can be created as well.

$ doas ifconfig vether2 create alias 10.0.0.1/24 rdomain 2 up  $ ifconfig vether2 vether2: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> rdomain 2 mtu 1500  lladdr fe:e1:ba:d9:1b:6c index 30 priority 0 llprio 3 groups: vether media: Ethernet autoselect status: active inet 10.0.0.1 netmask 0xffffff00 broadcast 10.0.0.255 
$ doas ifconfig vether3 create alias 10.0.1.1/24 rdomain 3 up  $ ifconfig vether3 vether3: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> rdomain 3 mtu 1500  lladdr fe:e1:ba:da:b5:73 index 31 priority 0 llprio 3 groups: vether media: Ethernet autoselect status: active inet 10.0.1.1 netmask 0xffffff00 broadcast 10.0.1.255 

To interconnect those networks 2 pair interfaces can be created. A small subnet will be create to route the packets between both networks.

$ doas ifconfig pair2 create alias 172.31.255.253/30 rdomain 2 up  $ ifconfig pair2 pair2: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> rdomain 2 mtu 1500  lladdr fe:e1:ba:de:a4:28 index 35 priority 0 llprio 3 groups: pair media: Ethernet autoselect inet 172.31.255.253 netmask 0xfffffffc broadcast 172.31.255.255 
$ doas ifconfig pair3 create alias 172.31.255.254/30 rdomain 3 up  $ ifconfig pair3 pair3: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> rdomain 3 mtu 1500  lladdr fe:e1:ba:df:34:e5 index 36 priority 0 llprio 3 groups: pair media: Ethernet autoselect inet 172.31.255.254 netmask 0xfffffffc broadcast 172.31.255.255 

The pair interfaces must be patched together.

$ doas ifconfig pair2 patch pair3  $ ifconfig pair2 pair2: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> rdomain 2 mtu 1500  lladdr fe:e1:ba:de:a4:28 index 35 priority 0 llprio 3 patch: pair3 groups: pair media: Ethernet autoselect status: active inet 172.31.255.253 netmask 0xfffffffc broadcast 172.31.255.255 $ ifconfig pair3 pair3: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> rdomain 3 mtu 1500  lladdr fe:e1:ba:df:34:e5 index 36 priority 0 llprio 3 patch: pair2 groups: pair media: Ethernet autoselect status: active inet 172.31.255.254 netmask 0xfffffffc broadcast 172.31.255.25 

It should now be possible to reach pair2 network from pair3 and vice versa. The best way to check that is to use the route exec command.

$ route -nT2 exec ping -c 3 172.31.255.254 PING 172.31.255.254 (172.31.255.254): 56 data bytes 64 bytes from 172.31.255.254: icmp_seq=0 ttl=255 time=0.615 ms 64 bytes from 172.31.255.254: icmp_seq=1 ttl=255 time=0.369 ms 64 bytes from 172.31.255.254: icmp_seq=2 ttl=255 time=0.382 ms --- 172.31.255.254 ping statistics --- 3 packets transmitted, 3 packets received, 0.0% packet loss round-trip min/avg/max/std-dev = 0.369/0.455/0.615/0.113 ms $ route -nT3 exec ping -c3 172.31.255.253 PING 172.31.255.253 (172.31.255.253): 56 data bytes 64 bytes from 172.31.255.253: icmp_seq=0 ttl=255 time=0.352 ms 64 bytes from 172.31.255.253: icmp_seq=1 ttl=255 time=0.364 ms 64 bytes from 172.31.255.253: icmp_seq=2 ttl=255 time=0.378 ms --- 172.31.255.253 ping statistics --- 3 packets transmitted, 3 packets received, 0.0% packet loss round-trip min/avg/max/std-dev = 0.352/0.364/0.378/0.011 ms $ route -nT3 exec ping -c1 10.0.0.1 PING 10.0.0.1 (10.0.0.1): 56 data bytes ping: sendmsg: Can't assign requested address ping: wrote 10.0.0.1 64 chars, ret=-1 

The 10.0.0.1/24 and 10.0.1.0/24 networks are not reachable yet, because no routes have been configured. Let fix that.

$ doas route -nT2 add 10.0.1.0/24 172.31.255.254 add net 10.0.1.0/24: gateway 172.31.255.254 $ netstat -rnfinet -T2 Routing tables Internet: Destination Gateway Flags Refs Use Mtu Prio Iface 10.0.0/24 10.0.0.1 UCn 0 0 - 4 vether2 10.0.0.1 fe:e1:ba:d9:1b:6c UHLl 0 8 - 1 vether2 10.0.0.255 10.0.0.1 UHb 0 0 - 1 vether2 10.0.1/24 172.31.255.254 UGS 0 0 - 8 pair2 127.0.0.1 127.0.0.1 UHl 0 0 32768 1 lo2 172.31.255.252/30 172.31.255.253 UCn 1 0 - 4 pair2 172.31.255.253 fe:e1:ba:de:a4:28 UHLl 0 10 - 1 pair2 172.31.255.254 fe:e1:ba:df:34:e5 UHLch 1 13 - 3 pair2 172.31.255.255 172.31.255.253 UHb 0 0 - 1 pair2 
$ doas route -nT3 add 10.0.0.0/24 172.31.255.253 add net 10.0.0.0/24: gateway 172.31.255.253 $ netstat -rnfinet -T3 Routing tables Internet: Destination Gateway Flags Refs Use Mtu Prio Iface 10.0.0/24 172.31.255.253 UGS 0 2 - 8 pair3 10.0.1/24 10.0.1.1 UCn 0 0 - 4 vether3 10.0.1.1 fe:e1:ba:da:b5:73 UHLl 0 0 - 1 vether3 10.0.1.255 10.0.1.1 UHb 0 0 - 1 vether3 127.0.0.1 127.0.0.1 UHl 0 0 32768 1 lo3 172.31.255.252/30 172.31.255.254 UCn 1 0 - 4 pair3 172.31.255.253 fe:e1:ba:de:a4:28 UHLch 1 7 - 3 pair3 172.31.255.254 fe:e1:ba:df:34:e5 UHLl 0 14 - 1 pair3 172.31.255.255 172.31.255.254 UHb 0 0 - 1 pair3 

After having added these routes, both network are available.

$ route -nT3 exec ping -c3 10.0.0.1 PING 10.0.0.1 (10.0.0.1): 56 data bytes 64 bytes from 10.0.0.1: icmp_seq=0 ttl=255 time=0.476 ms 64 bytes from 10.0.0.1: icmp_seq=1 ttl=255 time=0.371 ms 64 bytes from 10.0.0.1: icmp_seq=2 ttl=255 time=0.272 ms --- 10.0.0.1 ping statistics --- 3 packets transmitted, 3 packets received, 0.0% packet loss round-trip min/avg/max/std-dev = 0.272/0.373/0.476/0.083 ms $ route -nT2 exec ping -c3 10.0.1.1 PING 10.0.1.1 (10.0.1.1): 56 data bytes 64 bytes from 10.0.1.1: icmp_seq=0 ttl=255 time=0.477 ms 64 bytes from 10.0.1.1: icmp_seq=1 ttl=255 time=0.355 ms 64 bytes from 10.0.1.1: icmp_seq=2 ttl=255 time=0.271 ms --- 10.0.1.1 ping statistics --- 3 packets transmitted, 3 packets received, 0.0% packet loss round-trip min/avg/max/std-dev = 0.271/0.368/0.477/0.085 ms 

Managing one or two rdomain can easily be done with packet-filter, but if you have more than that, it could become rapidly a complex task. While looking for more examples and use cases from the *BSD users, many authors were recommending using packet-filter anchors to make the configuration easier to understand and to control. Let check that, if you don't know, packet-filter configuration file is located in /etc/pf.conf, but before editing this file, two new pflog interfaces will be required to log the packets from these domains.

$ doas ifconfig pflog2 create up  $ doas ifconfig pflog3 create up 

The rdomain 2 ruleset will be managed by the rd2 anchor and the rdomain 3 will be managed by the rd3 anchor. Both of them will only have one rule for now, to log all packets in transit respectively to pflog2 and pflog3 pseudo interfaces.

anchor "rd2" on rdomain 2 { match log (all, to pflog2) } anchor "rd3" on rdomain 3 { match log (all, to pflog3) } 
$ doas pfctl -f /etc/pf.conf 

This is another great feature available by default with packet-filter, instead of having text logs output, the typical logs messages when using iptables or nftables on Linux systems, the packets are directly duplicated to a dedicated interfaces, compatible with tcpdump and giving us the possibility to export them in PCAP format.

$ doas tcpdump -i pflog2 tcpdump: listening on pflog2, link-type PFLOG 13:49:03.419153 172.31.255.253 > 10.0.1.1: icmp: echo request 13:49:03.419380 10.0.1.1 > 172.31.255.253: icmp: echo reply 
$ doas tcpdump -i pflog3 tcpdump: listening on pflog3, link-type PFLOG 13:49:03.419267 172.31.255.253 > 10.0.1.1: icmp: echo request 13:49:03.419330 10.0.1.1 > 172.31.255.253: icmp: echo reply 

The packets displayed in the tcpdump output have been generated by a simple ping command. As you can see, we have know a way to understand more deeply what can happen on each rdomain previously created.

$ route -nT2 exec ping -c1 10.0.1.1 PING 10.0.1.1 (10.0.1.1): 56 data bytes 64 bytes from 10.0.1.1: icmp_seq=0 ttl=255 time=0.479 ms --- 10.0.1.1 ping statistics --- 1 packets transmitted, 1 packets received, 0.0% packet loss round-trip min/avg/max/std-dev = 0.479/0.479/0.479/0.000 ms 

One question to ask is if it is possible to forward packets from one rdomain to another via packet-filter. Indeed, the configuration support on rdomain NUMBER and rtable NUMBER parameters, so, it should be possible to achieve. To test this the pair interfaces previously created must be destroyed.

$ doas ifconfig pair2 destroy  $ doas ifconfig pair3 destroy 

Be sure the loopback interfaces (lo2 and lo3 in this example) are not skipped, the firewall must have the control on all traffic on these rdomains. When an ICMP packet is sent on rdomain 2, it will be redirected to rdomain 3.

anchor "rd2" on rdomain 2 { match log (all, to pflog2) pass out inet proto icmp \ to 10.0.0.1 \ rtable 3 \ rdr-to 10.0.1.1 } anchor "rd3" on rdomain 3 { match log (all, to pflog3) } 
$ doas pfctl -f /etc/pf.conf 
$ route -nT2 exec ping -c 1 10.0.0.1 PING 10.0.0.1 (10.0.0.1): 56 data bytes ... 
$ doas tcpdump -i pflog2 14:17:19.787826 10.0.0.1 > 10.0.0.1: icmp: echo request 
$ doas tcpdump -i pflog3 14:17:19.788009 10.0.0.1 > 10.0.1.1: icmp: echo request 

Without a route to 10.0.1.1/24, previously removed alongside the pair interfaces, it fails. Packet-filter will do the redirection anyway because it can switch from different routing tables and domains. It means the packet from 10.0.0.1 is correctly forwarded to the 10.0.1.1 address. Unfortunately, 10.0.1.1 can't reply to 10.0.0.1 because it does not have it in its own table. ICMP is perhaps not the best protocol to do those kind of redirection because it is stateless, and it can become very complex to manage the redirection. It is possible to generate an ICMP reply quite easily though, even without a correct routing table.

anchor "rd2" on rdomain 2 { match log (all, to pflog2) pass out inet proto icmp \ to 10.0.0.1 \ icmp-type echoreq \ rtable 3 \ rdr-to 10.0.1.1 } anchor "rd3" on rdomain 3 { match log (all, to pflog3) pass in inet proto icmp \ from 10.0.0.1 to 10.0.1.1 \ icmp-type echoreq \ rtable 2 \ rdr-to 10.0.0.1 } 
$ route -nT2 exec ping -c 1 10.0.0.1 PING 10.0.0.1 (10.0.0.1): 56 data bytes 
$ doas tcpdump -i pflog2 14:55:06.725461 10.0.0.1 > 10.0.0.1: icmp: echo request 
$ doas tcpdump -i pflog3 14:55:06.725640 10.0.0.1 > 10.0.1.1: icmp: echo request 14:55:06.725780 10.0.1.1 > 10.0.0.1: icmp: echo reply 

With this last example, it is possible to redirect a packet from one rdomain to another, but it is probably not the best idea to that in this way, because the rules will become quickly hard to understand. Regarding the anchors, one can also use another alternative syntax by passing the rdomain to each rules, like the code snippet below.

match log (all, to pflog2) on rdomain 2 pass out on rdomain 2 inet proto icmp \ to 10.0.0.1 \ icmp-type echoreq \ rtable 3 \ rdr-to 10.0.1.1 

VPN Server Configuration

The VPN server will act as the default router for the workers present behind the firewall they are connected to. This server is already quite busy, many services like OpenVPN, Tinc, and few others are already present and running. The private network attributed to all those services were taken randomly, and it's a small mess. To avoid confusion, only the rules for the VPN using Wireguard will be presented, in their own rdomain.

$ doas ifconfig lo1 create rdomain 1 alias 127.0.0.1 up  $ cat | doas tee /etc/hostname.lo1 < EOF rdomain 1 alias 127.0.0.1 up EOF 

Because rdomain 1 is totally isolated from rdomain 0 where the vio0 interface is present, it is necessary to create an interconnection network between them. The best way is to create 2 pair interfaces. The subnet will be set to 10.255.255.250/30. pair0 will stay in rdomain 0, and will give knowledge to the routes available on rdomain 1.

$ doas ifconfig pair0 create rdomain 0 up  $ cat | doas tee /etc/hostname.pair0 < EOF inet 10.255.255.253/30 patch pair1 up !route add 172.17.0.0/24 10.255.255.254 !route add 172.16.0.0/24 10.255.255.254 EOF 

pair1 will be pushed in rdomain 1 and like pair0, it will announce the route available on its side.

$ doas ifconfig pair1 create rdomain 1 up  $ cat | doas tee /etc/hostname.pair1 < EOF rdomain 1 inet 10.255.255.254/30 up !route -T1 add default 10.255.255.253 EOF 

With this pseudo interface configured, it is now possible to reach the network(s) present in rdomain 1. Let configures the VPN with a wireguard pseudo interface. The following snippet hides the private information to avoid leaks, but the idea is quite simple.

  1. generate a private key using openssl from a random integer encoded in base64;

  2. use a new random port (let say it will be 12345 for the rest of the configuration) using openssl as well;

  3. add the public key from the firewall (it will be shown in the next part)

  4. configure the end-point, in our case, it will be the public address of my firewall, let say 1.2.3.4 for now;

  5. creates the wg0 interface using ifconfig containing the previously created parameters. Set also the wgaip to explicitly allow the IP address from the firewall;

  6. finally, create the /etc/hostname.wg0 file containing the very same information to recreate automatically the VPN after a reboot.

$ server_wgkey=$(openssl rand -base64 32)  $ server_wgport=$(printf "%d\n" "0x$(openssl rand -hex 2)")  $ server_wgpeer="${firewall_public_key}"  $ server_wgendpoint="${firewall_ip_address}"  $ doas ifconfig wg0 create wgport ${server_wgport} \  wgkey ${server_wgkey} \  wgpeer ${server_wgpeer} \  wgaip 172.17.0.2/32 \ wgaip 172.16.0.0/24 \  wgendpoint ${server_wgendpoint} \  172.17.0.1/24 \ rdomain 1 \ up $ cat | doas tee /etc/hostname.wg0 < EOF wgport ${server_wgport} wgkey ${server_wgkey} wgpeer ${server_wgpeer} wgaip 172.17.0.2/32 wgaip 172.16.0.0/24 wgendpoint ${server_wgendpoint} 172.17.0.1/24 rdomain 1 up EOF 

Having a dedicated pflog interface for this rdomain can also be helpful, so, one can create the pflog1 interface.

$ doas ifconfig pflog1 create up  $ cat | doas tee /etc/hostname.pflog1 < EOF up EOF 

The configuration is practically over, packet-filter configuration will be the last step. The first part of this configuration will ensure the packets from the VPN are correctly translated to the public address set on the vio0 interface. In short, addresses from the worker network will use the public address of this server.

match out on rdomain 0 \ from 10.255.255.253/28 \ nat-to (vio0) static-port match out on rdomain 1 \ from 172.17.0.0/24 \ rtable 0 \ nat-to (vio0) static-port match out on rdomain 1 \ from 172.16.0.0/24 \ rtable 0 \ nat-to (vio0) static-port 

DNS resolution can be a problem, and to avoid DNS leaks, all packets from the worker network will be redirected to a local unbound instance, in charge of doing the domain resolution. It will also be easier to see what kind of queries are sent to domain servers and to filter them in one place.

match in log (to pflog1) on rdomain 1 \ proto {tcp, udp} \ to port 53 \ rtable 0 \ rdr-to 127.0.0.1 

Packet-filter is blocking all traffic by default. Wireguard needs to have its port accessible to other nodes though, then, the port previously create will be opened.

pass in on vio0 proto udp \ to port 12345 \ label "wireguard vpn" 

Finally, the last rules are only for rdomain 1. All packets are blocked and logged by default in this rdomain. ICMP and few TCP ports are allowed though, for SSH, HTTP and HTTPS.

anchor "workers" on rdomain 1 { block log (to pflog1) pass inet proto icmp pass inet proto tcp to port {22, 80, 443} keep state } 

Packet-filter configuration can be reloaded or restarted.

$ doas pfctl -nf /etc/pf.conf  $ doas pfctl -f /etc/pf.conf 

With this configuration, any nodes connected to wireguard will use the IP address of the server instead of their own (or the IP address of their routers). Especially useful if your workers are running in private place and you don't want to share your real IP address to Gitlab (or any other provider).

Fixing the right access to the hostname.if files can also be a good idea.

$ doas chmod 600 /etc/hostname.* 

Firewall Configuration

The firewall will be way easier to configure, the process will be similar than the one for the server. At first, creating a loopback interface for rdomain 1 can help.

$ cat | doas tee /etc/hostname.lo1 < EOF rdomain 1 alias 127.0.0.1 up EOF 

The main physical interface called re1 will be put in rdomain 1, and will not be configured. The IP address will be set on a vether pseudo interface created during the next step.

$ cat | doas tee /etc/hostname.re1 < EOF rdomain 1 up EOF 

vether0 is the main interface for rdomain 1. It is the gateway for the workers' network and will be put in rdomain 1.

$ cat | doas tee /etc/hostname.vether0 < EOF rdomain 1 inet 172.16.0.1/24 up EOF 

A bridge called bridge0 will be used to interconnect vether0 and re1, and will be put also in rdomain 1.

$ cat | doas tee /etc/hostname.bridge0 < EOF add vether0 add re1 rdomain 1 up EOF 

My private network should have access to the workers' network, for monitoring and maintenance purpose. The workers will not be allowed to pass there though, this rule will be enforced in packet filter. Anyway, a small interconnection network will be created and set on pair0.

$ cat | doas tee /etc/hostname.pair0 < EOF inet 10.255.255.253/30 rdomain 0 patch pair1 up !route add 172.16.0/24 10.255.255.254 EOF 

pair1 will also be created and connected to pair0. This time, pair1 will be put in rdomain 1.

$ cat | doas tee /etc/hostname.pair1 < EOF inet 10.255.255.254/30 rdomain 1 up !route -T1 add ${local_private_network} 10.255.255.253 EOF 

A wireguard wg0 speudo interface is also required to connect the firewall to the VPN. The procedure is the very same than before, except the information are created for the firewall this time, and not for the server. wgaip parameter is also set to 0.0.0.0/0 to allow any network to pass through the VPN, very useful if the VPN will be set as default gateway.

$ fw_wgkey=$(openssl rand -base64 32)  $ fw_wgport=$(printf "%d\n" "0x$(openssl rand -hex 2)")  $ fw_wgpeer="${server_public_key}"  $ fw_wgendpoint="${server_ip_address}"  $ cat | doas tee /etc/hostname.wg0 < EOF wgport ${fw_wgport} wgkey ${server_wgkey} wgpeer ${fw_wgpeer} wgaip 0.0.0.0/0 wgendpoint ${server_wgendpoint} 172.17.0.2/24 rdomain 1 up !route -T1 add default 172.17.0.1 EOF 

Finally, the packet-filter configuration present in /etc/pf.conf. The first part defines the macros. Classic.

worker_if = "{ re1, bridge0, vether0 }" worker_network = "172.16.0.0/24" worker_gateway = "172.16.0.1 

To avoid leaks, all packets from the workers' network will be dropped and not allowed to pass through the outgoing interface re0. This is probably not necessary here, because in the full configuration a block log is also present, and should block these connections.

block drop log \ on $worker_if \ from $worker_network \ to (re0) 

The connections from the private network (here s...