While most posts on this site usually concern Linux, I have a bit of a soft spot for OpenBSD.

OpenBSD is an operating system from the Unix lineage, started in Bell Labs many years ago, eventually giving rise to the Berkley Software Distribution (BSD). The most known versions of BSD are NetBSD (who focus on portability, running on pretty much any hardware), FreeBSD (who focus on covering as many purposes as possible) and OpenBSD (who focus on security, sometimes at the expense of performance).

OpenBSD has (in my opinion) amazing documentation, and with a combination of their man pages and example configuration (eg /etc/examples is full of configuration examples), you can get most of the included daemons working.

OpenBSD also provides quite a comprehensive suite of software already bundled in, everything from routing daemons, web servers, firewalling, mail servers and much more.

In this post, I’m going to cover how you can setup a pair of OpenBSD machines to run as a highly-available firewall pair.

Diagram

OpenBSD Firewalling

In the diagram, we have a client machine (running on Debian 10), a server (net-01) and two OpenBSD virtual machines.

Installing OpenBSD

To set up OpenBSD, you will be presented with a text-based installer with a number of options to choose from. I won’t go into details here, as the OpenBSD FAQ can cover every aspect of it. Summing up my chosen options though: -

  • Three main networks
    • hvn2 - The link between the firewalls (running in the 169.254.0.0/31 range)
    • hvn3 - The link to the server (running in the 192.0.2.0/24 range)
    • hvn4 - The link to the client (running in the 192.168.99.0/24 range)
  • Only installing a minimal file set, with no X11 dependencies, or the text-based games file sets
  • Automatic partioning

Setting up the the firewalls

To get the firewalls running, we need to setup the following daemons: -

  • pf(4) - PF, or Packet Filter, the OpenBSD packet filtering daemon
  • carp(4) - CARP, or Common Address Redundancy Protocol, for highly available IPs (like VRRP or Cisco’s HSRP)

We also need to do the following: -

  • Add the correct IPs to each interface
  • Allow IP forwarding through the firewalls
  • Enable the pfsync(4) device, to sync state between the firewalls
  • Create the carp(4) interfaces
  • Define the pf(4) ruleset

Setting up the IP interfaces

To define an interface in OpenBSD, you create a file called hostname.$INTERFACE-NAME, replacing $INTERFACE-NAME with your interfaces (eg hvn2, vio1), in the /etc directory.

To demonstrate this, see below: -

## hostname.hvn2
inet 169.254.0.1 255.255.255.254

## hostname.hvn3
inet 192.0.2.10 255.255.255.0

## hostname.hvn4
inet 192.168.99.1 255.255.255.0

You can also supply the network mask in hexadecimal format (eg a /24 would be 0xffffff00). To enable the interfaces, you can use doas sh /etc/netstart hvn2.

You should now be able to see the interfaces: -

$ ifconfig hvn2
hvn2: flags=8a43<UP,BROADCAST,RUNNING,ALLMULTI,SIMPLEX,MULTICAST> mtu 1500
        lladdr 00:15:5d:f4:91:ef
        index 3 priority 0 llprio 3
        media: Ethernet manual
        status: active
        inet 169.254.0.1 netmask 0xfffffffe

$ ifconfig hvn3 
hvn3: flags=8b43<UP,BROADCAST,RUNNING,PROMISC,ALLMULTI,SIMPLEX,MULTICAST> mtu 1500
        lladdr 00:15:5d:f4:91:f3
        index 4 priority 0 llprio 3
        media: Ethernet manual
        status: active
        inet 192.0.2.10 netmask 0xffffff00 broadcast 192.0.2.255

$ ifconfig hvn4 
hvn4: flags=8b43<UP,BROADCAST,RUNNING,PROMISC,ALLMULTI,SIMPLEX,MULTICAST> mtu 1500
        lladdr 00:15:5d:f4:91:f4
        index 5 priority 0 llprio 3
        media: Ethernet manual
        status: active
        inet 192.168.99.1 netmask 0xffffff00 broadcast 192.168.99.255

You’ll need to ensure that each firewall has a different IP on it’s interfaces (i.e. using 192.168.99.2/24 on hvn4 on the second firewall) so that they do not conflict.

Wait, what is doas?

doas(1) is a privilege escalation utility, used to temporarily elevate a user’s privileges, to allow them to perform commands that they typically cannot as a normal user. For those familiar with sudo, it is very similar. The reason for its existence is a smaller codebase than sudo to maintain, and also the configuration syntax is quite simple.

This is a good example of OpenBSD choosing tools that they can maintain and audit easily, rather than using external tools (or maintaing their own fork). For other examples, see the choice of their own httpd(8) over NGINX, or bgpd(4) over Quagga/BIRD.

To get a simple version of doas(1) running, you can look at the /etc/examples directory for a sample doas.conf file. Copy this to /etc/doas.conf, and edit it to your tastes.

## $OpenBSD: doas.conf,v 1.1 2016/09/03 11:58:32 pirofti Exp $
## Configuration sample file for doas(1).
## See doas.conf(5) for syntax and examples.

## Non-exhaustive list of variables needed to build release(8) and ports(7)
##permit nopass setenv { \
##    FTPMODE PKG_CACHE PKG_PATH SM_PATH SSH_AUTH_SOCK \
##    DESTDIR DISTDIR FETCH_CMD FLAVOR GROUP MAKE MAKECONF \
##    MULTI_PACKAGES NOMAN OKAY_FILES OWNER PKG_DBDIR \
##    PKG_DESTDIR PKG_TMPDIR PORTSDIR RELEASEDIR SHARED_ONLY \
##    SUBPACKAGE WRKOBJDIR SUDO_PORT_V1 } :wsrc

## Allow wheel by default
permit keepenv :wheel

The above does nothing more than allow the wheel group to use doas(1). I add my user into the wheel group, and from then on I can use doas(1) to my hearts content.

Setting up pfsync(4)

For truly highly available firewalls, you need something that will synchronize the state between them. This is because if you just failover to another machine, the second machine would have no idea of what existing TCP connections are active, what UDP connections have been seen recently (eg VoIP calls) and how many hits each rule has seen (important for monitoring).

OpenBSD provides the pfsync(4) utility, which does exactly that. To quote the man page for pfsync(4)

If configured with a physical synchronisation interface, pfsync will also send state changes out on that interface, and insert state changes received on that interface from other systems into the state table.

So if you have this running on an interface dedicated between two machines, they will share state information.

To make this work, you configure another hostname.$INTERFACE-NAME file, except this time it will be hostname.pfsync. The contents of the file will look something like the below: -

$ cat /etc/hostname.pfsync0                                                                                         
up syncdev hvn2

This is the same on both firewalls. If you run tcpdump(8) on hvn2 while the firewall is in operation, you will see numerous state messages passing between the two firewalls (try running tcpdump -i hvn2 during operation to see).

Allow IP Forwarding

IP Forwarding is configured within the /etc/sysctl.conf file as such: -

$ cat /etc/sysctl.conf
net.inet.ip.forwarding=1

Without this, traffic will not work through the firewalls, so make sure you enable this!

Add carp(4) for redundancy

Unless you use devices that support routing protocols, or can failover between multiple default gateways, you’ll need to target an IP that can reside on both firewalls. In the Cisco world, they use HSRP (Hot Standby Router Protocol). The RFC version of HSRP is known as VRRP (Virtual Router Redundancy Protocol). However, despite VRRP being an RFC standard, it is patent-encumbered, meaning some elements of it are covered by patents (owned by Cisco).

Due to this, the OpenBSD developers implemented CARP instead. CARP does not infringe on any Cisco patents, although it does still use a few traits of VRRP (eg the IP Protocol number and Virtual MAC Addresses). This does mean that if you have VRRP and CARP on the same network, you will need to use different VRRP Group IDs and CARP Virtual Host IDs for them to coexist.

Again, to setup a carp(4) interface on the primary firewall, you use the hostname.$INTERFACE-NAME files. To demonstrate, see below: -

## hostname.carp1
inet 192.0.2.1 255.255.255.0 192.0.2.255 vhid 1 carpdev hvn3 pass hvn3pass

## hostname carp2
inet 192.168.99.254 255.255.255.0 192.168.99.255 vhid 1 carpdev hvn4 pass hvn4pass 

The syntax is as follows: -

inet $VIRTUAL-IP $SUBNET-MASK $BROADCAST-ADDRESS vhid $CARP-VIRTUAL-HOST-ID carpdev $PHYSICAL-INTERFACE pass $PASSWORD

You need to ensure the Virtual IP is in the same network as the interface it is running on. The Physical Interface parameter is used to tie the CARP virtual interface to a physical interface.

Again, use doas /etc/netstart carpN to start the interfaces: -

$ ifconfig carp1
carp1: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
        lladdr 00:00:5e:00:01:01
        index 8 priority 15 llprio 3
        carp: MASTER carpdev hvn3 vhid 1 advbase 1 advskew 0
        groups: carp
        status: master
        inet 192.0.2.1 netmask 0xffffff00 broadcast 192.0.2.255

$ ifconfig carp2
carp2: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
        lladdr 00:00:5e:00:01:01
        index 9 priority 15 llprio 3
        carp: MASTER carpdev hvn4 vhid 1 advbase 1 advskew 0
        groups: carp
        status: master
        inet 192.168.99.254 netmask 0xffffff00 broadcast 192.168.99.255

Notice in the above that it shows the status of the interface (in this case, the MASTER).

The syntax for the hostname files on the secondary firewall differs slightly: -

## hostname.carp1
inet 192.0.2.1 255.255.255.0 192.0.2.255 vhid 1 carpdev hvn3 pass hvn3pass advskew 128

## hostname.carp2
inet 192.168.99.254 255.255.255.0 192.168.99.255 vhid 1 carpdev hvn4 pass hvn4pass advskew 128

The primary difference is the advskew parameter. This is used to set a “priority” or “weight” on the interface, higher being less preferred.

The interface output on the secondary firewall looks like this: -

$ ifconfig carp1
carp1: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
        lladdr 00:00:5e:00:01:01
        index 8 priority 15 llprio 3
        carp: BACKUP carpdev hvn3 vhid 1 advbase 1 advskew 128
        groups: carp
        status: backup
        inet 192.0.2.1 netmask 0xffffff00 broadcast 192.0.2.255

$ ifconfig carp2 
carp2: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
        lladdr 00:00:5e:00:01:01
        index 9 priority 15 llprio 3
        carp: BACKUP carpdev hvn4 vhid 1 advbase 1 advskew 128
        groups: carp
        status: backup
        inet 192.168.99.254 netmask 0xffffff00 broadcast 192.168.99.255

As you can see, the status of the interfaces are backup, meaning they will not respond on the Virtual IP, unless the master goes away (and they are promoted to master).

PF

After all of the above, you’ll now be ready to define a Packet Filter ruleset. There are a number of amazing resources on this, including The Book of PF by Peter Hansteen, The PF Tutorial (also by Peter Hansteen, along with Massimiliano Stucchi) and the OpenBSD PF FAQ.

PF is very flexible, in that you can apply macros, arrays/lists of IPs, and refer to them with as a single variable with your ruleset.

I have set up a very small ruleset on the two firewalls: -

oas cat /etc/pf.conf 
doas ([email protected]) password: 
##       $OpenBSD: pf.conf,v 1.55 2017/12/03 20:40:04 sthen Exp $
##
## See pf.conf(5) and /etc/examples/pf.conf
ext_if="hvn3"
carp_ext_if="carp1"
carp_int_if="carp2"
int_if="hvn4"
sync_if="hvn2"
internet_if="hvn0"

set skip on lo

block return    # block stateless traffic
pass            # establish keep-state

## By default, do not permit remote connections to X11
block return in on ! lo0 proto tcp to port 6000:6010

## Port build user does not need network
block return out log proto {tcp udp} user _pbuild

## Allow PFSync
pass quick on $sync_if proto pfsync 

## Allow CARP on in and out ints
pass quick on { $ext_if $int_if } proto carp

## NAT to net-01
pass out on $ext_if inet from $int_if:network to any nat-to $carp_ext_if
pass out on $ext_if inet from $carp_int_if:network to any nat-to $carp_ext_if

## NAT to the Internet
pass out on $internet_if inet from $int_if:network to any nat-to $internet_if
pass out on $internet_if inet from $carp_int_if:network to any nat-to $internet_if

A lot of the rules are from the examples, but I have also included a few extra rules: -

  • Allowing PFSync on the $sync_if interface (in my case, hvn2)
  • Allowing CARP on the interfaces they reside on (in my case, hvn3 and hvn4)
  • Any traffic that comes in from the internal interfaces (hvn4 or carp2) going out of the interface facing net-01, to NAT it to the IP of the the $carp_ext_if (in my case, carp1)
  • Any traffic that comes in from the internal interfaces (hvn4 or carp2) going out to the internet (the hvn0 interface), to NAT it to the IP of the the $internet_if (in my case, also hvn0).

The reason for not running carp(4) on the internet interface is due to these being virtual machines without a shared Internet-facing network, or shared IP space.

To apply this ruleset, you can use doas pfctl -f /etc/pf.conf. To enable pf(4) to run at boot, use rcctl enable pf (rcctl(8) being a tool to control running daemons).

One important point to note is that pfsync(4) does not synchronize configuration changes, only firewall state. You will need to manage keeping the rulesets synchronized separately using your chosen configuration management and deployment tool (eg rsync, ansible, rdist(1) or otherwise).

Verification

To show the active ruleset, you can run doas pfctl -s rules

$ doas pfctl -s rules
doas ([email protected]) password: 
block return all
pass all flags S/SA
block return in on ! lo0 proto tcp from any to any port 6000:6010
block return out log proto tcp all user = 55
block return out log proto udp all user = 55
pass quick on hvn3 proto carp all
pass quick on hvn4 proto carp all
pass quick on hvn2 proto pfsync all
pass out on hvn3 inet from 192.168.99.0/24 to any flags S/SA nat-to 192.0.2.1
pass out on hvn0 inet from 192.168.99.0/24 to any flags S/SA nat-to 172.18.22.197

To view active statistics, you can run doas pfctl -vsr

doas pfctl -vsr
doas ([email protected]) password: 
block return all
  [ Evaluations: 1169      Packets: 0         Bytes: 0           States: 0     ]
  [ Inserted: uid 0 pid 45241 State Creations: 0     ]
pass all flags S/SA
  [ Evaluations: 1169      Packets: 11829     Bytes: 3762548     States: 11    ]
  [ Inserted: uid 0 pid 45241 State Creations: 6645  ]
block return in on ! lo0 proto tcp from any to any port 6000:6010
  [ Evaluations: 1169      Packets: 0         Bytes: 0           States: 0     ]
  [ Inserted: uid 0 pid 45241 State Creations: 0     ]
block return out log proto tcp all user = 55
  [ Evaluations: 145       Packets: 0         Bytes: 0           States: 0     ]
  [ Inserted: uid 0 pid 45241 State Creations: 0     ]
block return out log proto udp all user = 55
  [ Evaluations: 1168      Packets: 0         Bytes: 0           States: 0     ]
  [ Inserted: uid 0 pid 45241 State Creations: 0     ]
pass quick on hvn3 proto carp all
  [ Evaluations: 1169      Packets: 23243     Bytes: 1301608     States: 2     ]
  [ Inserted: uid 0 pid 45241 State Creations: 2     ]
pass quick on hvn4 proto carp all
  [ Evaluations: 1161      Packets: 23243     Bytes: 1301608     States: 2     ]
  [ Inserted: uid 0 pid 45241 State Creations: 2     ]
pass quick on hvn2 proto pfsync all
  [ Evaluations: 1167      Packets: 0         Bytes: 0           States: 0     ]
  [ Inserted: uid 0 pid 45241 State Creations: 0     ]
pass out on hvn3 inet from 192.168.99.0/24 to any flags S/SA nat-to 192.0.2.1
  [ Evaluations: 1167      Packets: 0         Bytes: 0           States: 0     ]
  [ Inserted: uid 0 pid 45241 State Creations: 0     ]
pass out on hvn0 inet from 192.168.99.0/24 to any flags S/SA nat-to 172.18.22.197
  [ Evaluations: 1160      Packets: 34        Bytes: 2676        States: 0     ]
  [ Inserted: uid 0 pid 45241 State Creations: 26    ]

To view the current stats, you can run doas pfctl -ss

$ doas pfctl -ss  
doas ([email protected]) password: 
all pfsync 169.254.0.1 -> 224.0.0.240       SINGLE:NO_TRAFFIC
all pfsync 224.0.0.240 <- 169.254.0.0       NO_TRAFFIC:SINGLE
all tcp 192.168.241.7:22 <- 192.168.241.254:56448       ESTABLISHED:ESTABLISHED
all tcp 192.0.2.10:17742 -> 192.0.2.20:179       ESTABLISHED:ESTABLISHED
all tcp 192.168.241.6:22 <- 192.168.241.254:54168       ESTABLISHED:ESTABLISHED
all carp 192.0.2.10 -> 224.0.0.18       SINGLE:NO_TRAFFIC
all carp 192.168.99.1 -> 224.0.0.18       SINGLE:NO_TRAFFIC
all carp 224.0.0.18 <- 192.0.2.10       NO_TRAFFIC:SINGLE
all carp 224.0.0.18 <- 192.168.99.1       NO_TRAFFIC:SINGLE
all udp ff12::8384[21027] <- fe80::adfb:c525:9a:71b5[56280]       NO_TRAFFIC:SINGLE
all udp 192.168.241.255:21027 <- 192.168.241.1:64395       NO_TRAFFIC:SINGLE
all udp 172.18.22.207:21027 <- 172.18.22.193:64395       NO_TRAFFIC:SINGLE
all tcp 192.0.2.11:179 <- 192.0.2.20:57424       TIME_WAIT:TIME_WAIT
all udp 239.255.255.250:1900 <- 192.168.241.1:58267       NO_TRAFFIC:SINGLE
all udp 239.255.255.250:1900 <- 172.18.22.193:58273       NO_TRAFFIC:SINGLE
all udp 172.18.22.207:138 <- 172.18.22.193:138       NO_TRAFFIC:SINGLE
all udp 192.168.241.255:138 <- 192.168.241.1:138       NO_TRAFFIC:SINGLE
all udp 172.18.22.198:45469 -> 193.150.34.2:123       MULTIPLE:SINGLE
all icmp 192.0.2.1:8 <- 192.0.2.20:23408       0:0
all udp 172.18.22.197:15594 -> 162.159.200.1:123       MULTIPLE:SINGLE

State replication and failover

To show the state replication, I have set a ping going from client-01 to net-01: -

$ ping 192.0.2.20
PING 192.0.2.20 (192.0.2.20) 56(84) bytes of data.
64 bytes from 192.0.2.20: icmp_seq=1 ttl=63 time=0.750 ms
64 bytes from 192.0.2.20: icmp_seq=2 ttl=63 time=0.685 ms

Viewing the state table on openbsdfw-01 for this, I can see: -

$ doas pfctl -ss | grep -i icmp 
doas ([email protected]) password: 
all icmp 192.0.2.20:8 <- 192.168.99.50:5463       0:0
all icmp 192.0.2.1:63341 (192.168.99.50:5463) -> 192.0.2.20:8       0:0

$ doas tcpdump -i hvn4 icmp 
doas ([email protected]) password: 
tcpdump: listening on hvn4, link-type EN10MB
13:16:46.519326 192.168.99.50 > 192.0.2.20: icmp: echo request (DF)
13:16:46.519931 192.0.2.20 > 192.168.99.50: icmp: echo reply

What do we see on openbsdfw-02?

$ doas pfctl -ss | grep -i icmp
doas ([email protected]) password: 
all icmp 192.0.2.20:8 <- 192.168.99.50:5463       0:0
all icmp 192.0.2.1:63341 (192.168.99.50:5463) -> 192.0.2.20:8       0:0

$ doas tcpdump -i hvn4 icmp
doas ([email protected]) password: 
tcpdump: listening on hvn4, link-type EN10MB


So as you can see, we actually have the state (i.e. the ICMP packets going through), but the packets themselves do not traverse the secondary firewall.

What about if we shutdown the primary firewall?

$ doas shutdown -h now
doas ([email protected]) password: 
Shutdown NOW!
shutdown: [pid 52871]

*** FINAL System shutdown message from [email protected] ***    
System going down IMMEDIATELY

Did the traffic failover?

$ doas tcpdump -i hvn4 icmp 
doas ([email protected]) password: 
tcpdump: listening on hvn4, link-type EN10MB
13:20:02.105437 192.168.99.50 > 192.0.2.20: icmp: echo request (DF)
13:20:02.105823 192.0.2.20 > 192.168.99.50: icmp: echo reply

Looks like it did! Now lets check a few details on the secondary firewall: -

CARP Status

$ ifconfig carp1
carp1: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 00:00:5e:00:01:01
	index 8 priority 15 llprio 3
	carp: MASTER carpdev hvn3 vhid 1 advbase 1 advskew 128
	groups: carp
	status: master
	inet 192.0.2.1 netmask 0xffffff00 broadcast 192.0.2.255

$ ifconfig carp2 
carp2: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 00:00:5e:00:01:01
	index 9 priority 15 llprio 3
	carp: MASTER carpdev hvn4 vhid 1 advbase 1 advskew 128
	groups: carp
	status: master
	inet 192.168.99.254 netmask 0xffffff00 broadcast 192.168.99.255

Looks like we failed over, what about the firewall statistics and states?

$ doas pfctl -ss | grep -i icmp
doas ([email protected]) password: 
all icmp 192.0.2.20:8 <- 192.168.99.50:5463       0:0
all icmp 192.0.2.1:63341 (192.168.99.50:5463) -> 192.0.2.20:8       0:0

$ doas pfctl -vsr               
doas ([email protected]) password: 
block return all
  [ Evaluations: 1445      Packets: 0         Bytes: 0           States: 0     ]
  [ Inserted: uid 0 pid 97984 State Creations: 0     ]
pass all flags S/SA
  [ Evaluations: 1445      Packets: 14751     Bytes: 4848439     States: 16    ]
  [ Inserted: uid 0 pid 97984 State Creations: 6888  ]
block return in on ! lo0 proto tcp from any to any port 6000:6010
  [ Evaluations: 1445      Packets: 0         Bytes: 0           States: 0     ]
  [ Inserted: uid 0 pid 97984 State Creations: 0     ]
block return out log proto tcp all user = 55
  [ Evaluations: 391       Packets: 0         Bytes: 0           States: 0     ]
  [ Inserted: uid 0 pid 97984 State Creations: 0     ]
block return out log proto udp all user = 55
  [ Evaluations: 1259      Packets: 0         Bytes: 0           States: 0     ]
  [ Inserted: uid 0 pid 97984 State Creations: 0     ]
pass quick on hvn3 proto carp all
  [ Evaluations: 1445      Packets: 47713     Bytes: 2671928     States: 1     ]
  [ Inserted: uid 0 pid 97984 State Creations: 4     ]
pass quick on hvn4 proto carp all
  [ Evaluations: 1257      Packets: 47713     Bytes: 2671928     States: 1     ]
  [ Inserted: uid 0 pid 97984 State Creations: 4     ]
pass quick on hvn2 proto pfsync all
  [ Evaluations: 1441      Packets: 0         Bytes: 0           States: 0     ]
  [ Inserted: uid 0 pid 97984 State Creations: 0     ]
pass out on hvn3 inet from 192.168.99.0/24 to any flags S/SA nat-to 192.0.2.1
  [ Evaluations: 1441      Packets: 434       Bytes: 36456       States: 1     ]
  [ Inserted: uid 0 pid 97984 State Creations: 3     ]
pass out on hvn0 inet from 192.168.99.0/24 to any flags S/SA nat-to 172.18.22.198
  [ Evaluations: 1255      Packets: 842       Bytes: 807671      States: 0     ]
  [ Inserted: uid 0 pid 97984 State Creations: 27    ]

There we go, we’re passing traffic. How many packets did we lose during all of this?

[...]
--- 192.0.2.20 ping statistics ---
500 packets transmitted, 500 received, 0% packet loss, time 675ms
rtt min/avg/max/mdev = 0.424/0.856/3.406/0.270 ms

Well then, not a single packet lost. Looks like we saw high delay on one of them, but no packets actually dropped. If we set the timeout within the ping utility, I’m sure we would have seen a few drop. By default the time between packets is 1 second, which is more than enough to see in most practical situations that many people wouldn’t notice the failover.

Summary

OpenBSD is a very versatile operating system, with very sane defaults, and a high security focus. It also ships with a lot of software that means you may never need to add anything from external repositories/ports (everything from bgpd(4), snmpd(8), relayd(8) and much more) to have a fully functional system, especially for security and network functions.

I am just a beginner when it comes to OpenBSD. If you want to know more about OpenBSD, I would urge you to visit: -

I’m missing out on many more, but the above is a good place to start! I would highly recommend going through the Events page and watching some talks regarding OpenBSD.