Reference Architectures

Reference Architectures

Synopsis #

This chapter assembles end-to-end reference designs built from components covered earlier. It presents three canonical patterns:

  1. Redundant Internet Edge with carp(4), pfsync(4), per-uplink NAT and policy routing in pf.conf(5), managed by pfctl(8).
  2. Campus/Branch Routed Access using VLAN SVIs, Router Advertisements with rad(8), internal recursion via unbound(8), authoritative zones with nsd(8), and DHCP with dhcpd(8).
  3. Regional POP with either an MPLS LDP core via ldpd(8) and mpe(4), or an encrypted hub-and-spoke using iked(8) or wg(4). BGP policy lives in the OpenBGPD chapter.

Each pattern includes minimal, copy-pasteable configuration and verification guidance.

Design Considerations #

  • Failure domains. Keep state sync and control traffic on a private link. Avoid bridging across routers unless strictly required.
  • Configuration parity. Maintain identical PF and service configs on HA pairs; only device-specific interface addressing should differ.
  • Change management. Use atomic PF reloads and a rollback timer during first deployment. Keep a failsafe anchor that preserves management access.
  • Addressing plan. Allocate deterministic per-VLAN /24 (IPv4) and /64 (IPv6) with structured mapping to sites and roles.
  • Observability. Enable pflogd(8) and (optionally) pflow(4) toward a collector; keep clocks accurate via ntpd(8).

Pattern 1 — Redundant Internet Edge (Two Firewalls, Two ISPs) #

Scope. Two OpenBSD firewalls in HA; two WAN uplinks (ISP-A and ISP-B); deterministic outbound steering and symmetric inbound using route-to and reply-to.

Topology and Interface Assumptions #

  • em0 = ISP-A, em3 = ISP-B, em1 = LAN, em2 = SYNC
  • CARP VIPs: WAN-A 198.51.100.2/29 (vhid 10), WAN-B 203.0.113.2/29 (vhid 20), LAN 10.10.10.1/24 (vhid 30)
  • Node A preferred (advskew 0), Node B backup (advskew 100)

System and Interfaces (Node A; mirror with advskew 100 on Node B) #

# printf '%s\n' 'net.inet.carp.preempt=1' >> /etc/sysctl.conf
# sysctl net.inet.carp.preempt=1
  # Prefer the designated MASTER after repair

# cat > /etc/hostname.em2 <<'EOF'
inet 10.255.255.2 255.255.255.252
EOF
  # State-sync /30

# cat > /etc/hostname.pfsync0 <<'EOF'
up syncdev em2
EOF
  # pfsync over SYNC

# cat > /etc/hostname.em0 <<'EOF'
inet 198.51.100.3 255.255.255.248
EOF
# cat > /etc/hostname.em3 <<'EOF'
inet 203.0.113.3 255.255.255.248
EOF
# cat > /etc/hostname.em1 <<'EOF'
inet 10.10.10.2 255.255.255.0
EOF

# cat > /etc/hostname.carp0 <<'EOF'
inet 198.51.100.2 255.255.255.248 vhid 10 carpdev em0 pass sharedsecret advskew 0
EOF
# cat > /etc/hostname.carp1 <<'EOF'
inet 203.0.113.2 255.255.255.248 vhid 20 carpdev em3 pass sharedsecret advskew 0
EOF
# cat > /etc/hostname.carp2 <<'EOF'
inet 10.10.10.1 255.255.255.0 vhid 30 carpdev em1 pass sharedsecret advskew 0
EOF

# sh /etc/netstart em0 em1 em2 em3 carp0 carp1 carp2 pfsync0
  # Activate without reboot

PF Policy (both nodes) #

## /etc/pf.conf — Edge HA + Multi-WAN

set skip on { lo, pfsync0 }

# Interfaces and macros
lan   = "em1"
wan_a = "em0"
wan_b = "em3"
lan_net = "10.10.10.0/24"
gw_a = "198.51.100.1"
gw_b = "203.0.113.1"
web_svc = "10.10.10.50"

# Normalization
scrub in all max-mss 1452

# Allow HA control-plane
pass on $lan proto carp
pass on $wan_a proto carp
pass on $wan_b proto carp
pass on em2 proto pfsync

# NAT per uplink
match out on $wan_a inet from $lan_net to any nat-to ($wan_a)
match out on $wan_b inet from $lan_net to any nat-to ($wan_b)

# Base policy
block all

# Inbound to firewall itself with symmetric reply
pass in on $wan_a proto { tcp udp icmp } to ($wan_a) reply-to ($wan_a $gw_a) keep state
pass in on $wan_b proto { tcp udp icmp } to ($wan_b) reply-to ($wan_b $gw_b) keep state

# Outbound policy from LAN:
# - Bulk to ISP-B
# - Default to ISP-A
pass in on $lan proto { tcp udp } from $lan_net to any port { 873, 6881:6999 } \
    route-to ($wan_b $gw_b) keep state
pass in on $lan from $lan_net to any \
    route-to ($wan_a $gw_a) keep state

# Inbound service on both ISPs (HTTPS)
rdr on $wan_a proto tcp to ($wan_a) port 443 -> $web_svc
pass in on $wan_a proto tcp to $web_svc port 443 reply-to ($wan_a $gw_a) keep state
rdr on $wan_b proto tcp to ($wan_b) port 443 -> $web_svc
pass in on $wan_b proto tcp to $web_svc port 443 reply-to ($wan_b $gw_b) keep state

# LAN to firewall services (SSH/HTTPS example)
pass in on $lan proto tcp from $lan_net to ($lan) port { 22, 443 } keep state

# Outbound from the firewall
pass out on { $wan_a, $wan_b } proto { tcp udp icmp } from (self) to any keep state

Verification (either node):

$ ifconfig carp0 carp1 carp2 | egrep 'vhid|status'
  # MASTER/BACKUP per VIP
$ pfctl -vvsr | egrep 'route-to|reply-to|proto carp|pfsync'
  # Policy pins and HA allowances
$ pfctl -vvss | head
  # Replicated states present on both nodes
$ tcpdump -ni pfsync0
  # Flow updates on SYNC

Pattern 2 — Campus/Branch Routed Access with Local Services #

Scope. Two routers provide VLAN gateways with CARP, advertise IPv6 via rad(8), and offer internal recursion (Unbound), authoritative zones (NSD), DHCP, and anycast resolver placement.

Gateways and CARP (R1/R2) #

Assume trunk em1, VLAN 10 Users 10.10.10.0/24, VLAN 20 Servers 10.20.20.0/24, VIPs .1, router unicast .2 (R1) / .3 (R2).

# /etc/hostname.vlan10 (R1)
vlandev em1
vlan 10
inet 10.10.10.2 255.255.255.0
up
# /etc/hostname.vlan20 (R1)
vlandev em1
vlan 20
inet 10.20.20.2 255.255.255.0
up
# /etc/hostname.carp0 (R1)
inet 10.10.10.1 255.255.255.0 vhid 10 carpdev vlan10 pass shared advskew 0
# /etc/hostname.carp1 (R1)
inet 10.20.20.1 255.255.255.0 vhid 20 carpdev vlan20 pass shared advskew 0

Mirror on R2 with .3 and advskew 100.

Router Advertisements and DNS on a Services Node #

rad(8) advertises /64s; Unbound serves recursion; NSD serves your zone; DHCP serves IPv4.

## /etc/rad.conf
interface vlan10
    prefix 2001:db8:10:10::/64
    rdnss 2001:db8:10:10::53
    dnssl corp.example
interface vlan20
    prefix 2001:db8:10:20::/64
    rdnss 2001:db8:10:20::53
    dnssl corp.example
## /var/unbound/etc/unbound.conf (excerpt)
server:
    interface: 10.10.10.53
    interface: 10.20.20.53
    access-control: 10.10.10.0/24 allow
    access-control: 10.20.20.0/24 allow
    auto-trust-anchor-file: "/var/unbound/db/root.key"
## /etc/nsd.conf (excerpt)
server:
    ip-address: 10.20.20.53
    zonesdir: "/var/nsd/zones"
zone:
    name: "corp.example"
    zonefile: "corp.example.zone"
## /etc/dhcpd.conf (excerpt)
subnet 10.10.10.0 netmask 255.255.255.0 {
    range 10.10.10.100 10.10.10.200;
    option routers 10.10.10.1;
    option domain-name-servers 10.10.10.53;
}
subnet 10.20.20.0 netmask 255.255.255.0 {
    range 10.20.20.100 10.20.20.200;
    option routers 10.20.20.1;
    option domain-name-servers 10.20.20.53;
}

Anycast Resolver Address #

Publish 10.10.255.53/32 from two resolvers and ECMP it on R1/R2.

# ifconfig lo0 alias 10.10.255.53/32
  # On each resolver

# route add -mpath -host 10.10.255.53 10.20.20.11
# route add -mpath -host 10.10.255.53 10.20.20.12
  # On each router; confirm with 'route -n show'

Verification:

$ ifconfig vlan10 vlan20 carp0 carp1 | egrep 'status|vhid'
$ drill @10.10.10.53 openbsd.org A
$ ping -n -c 3 10.10.255.53
$ ndp -an | head

Pattern 3 — Regional POP #

Two options are common:

A) MPLS LDP Core with L3 VPNs #

Enable MPLS on core links, run ldpd(8), and connect customer VRFs via mpe(4). BGP VPN policy is defined in OpenBGPD.

# ifconfig em0 mpls
# ifconfig em1 mpls
# rcctl enable ldpd ; rcctl start ldpd
## /etc/ldpd.conf (core interfaces)
router-id 192.0.2.1
address-family ipv4 {
    interface em0
    interface em1
}

Bind a customer VRF (rdomain 10) to MPLS with mpe0:

# ifconfig em2 rdomain 10 inet 10.10.10.1/24
# ifconfig mpe0 create
# ifconfig mpe0 rdomain 10 mplslabel 2000 inet 192.198.0.1/32 up

Announce customer prefixes via OpenBGPD VPN AFI/SAFI (see the OpenBGPD chapter for bgpd.conf vpn stanza and iBGP toward other PEs).

Verification:

$ ldpctl show neighbors
$ ldpctl show lib | head
$ route -T 10 -n show

B) Encrypted Hub-and-Spoke (IKEv2) #

For smaller POPs, use iked(8) to connect spokes to a regional hub.

## /etc/iked.conf — spoke to hub (selectors per site)
ikev2 "spoke-hub" active esp from 10.30.0.0/24 to 10.0.0.0/16 \
    peer 198.51.100.50 \
    ikesa enc aes-256-gcm prf sha256 group 14 \
    childsa enc aes-256-gcm \
    psk "change-me"
# rcctl enable iked ; rcctl start iked
# ikectl show sa ; ikectl show flows

POP PF skeleton (both options):

## /etc/pf.conf — POP edge (excerpt)
set skip on lo
block all
pass out on egress proto { tcp udp icmp } keep state
# Allow management from NOC
pass in on egress proto tcp from 198.51.100.0/24 to (egress) port 22 keep state
# Permit customer LANs on VRF or tunnel interfaces (adjust)
pass in on enc0 from 10.30.0.0/24 to any keep state
# Or, for MPLS VRF:
# pass in on mpe0 from 10.30.0.0/24 to any keep state

Verification #

  • HA edges: ifconfig carp*, pfctl -vvsr counters for route-to/reply-to, tcpdump -ni pfsync0.
  • Campus services: drill, unbound-control status, nsd-control stats, dhcpd leases, rad(8) advertising on correct VLANs.
  • POP: ldpctl show neighbors/lib, bgpctl show summary (when applicable), ikectl show sa/flows.
  • Telemetry: tcpdump -i pflog0, pfctl -vvsr | grep log, ntpctl -s status.

Troubleshooting #

  • Asymmetric paths at the edge. Missing reply-to on WAN rules for inbound NAT is the most common cause. Add it per uplink and flush affected states.
  • State desynchronization. Verify pfsync0 syncdev and PF allowances. Ensure both nodes run the same ruleset and NAT model.
  • IPv6 clients fail SLAAC. Confirm rad(8) advertises exactly one /64 per L2, ICMPv6 is permitted, and that only the intended routers send RAs.
  • Anycast black holes. Remove a failed next-hop from the ECMP route or automate withdrawals; ensure resolvers actually bind the anycast address.
  • POP label gaps. For MPLS, confirm core interfaces are mpls-enabled and present in ldpd.conf. For encrypted spokes, check UDP 500/4500 reachability and PSK alignment.
  • Service port conflicts. Keep authoritative (NSD) and recursive (Unbound) on distinct IPs or ports; never expose recursion to the Internet.

See Also #