OpenBSD Router : Native IPv6

How to set up native dual-stack IPv6 for an OpenBSD router

Concepts

Requirements

RFC 7084, Basic Requirements for IPv6 Customer Edge Routers, specifies how an IPv6 router should function in a residential environment. With the OpenBSD router guide as the starting point, the changes required are:

  1. On the external interface: act as a DHCPv6 client to receive a Prefix Delegation

  2. On the internal interfaces: serve Router Advertisements for local hosts, which is necessary for SLAAC

  3. Adjust the firewall ruleset to allow the Neighbor Discovery Protocol and DHCPv6 to function

Firewall

First enable IPv6 forwarding to let the host act as a router.

# echo 'net.inet6.ip6.forwarding=1' >> /etc/sysctl.conf

Then update the PF ruleset.

Add the IPv6 non-routable ranges to the “martians” table
Do not block link-local addresses (fe80::/10) since Neighbor Discovery and DHCPv6 require link-local communication
Remove the inet parameter on the main pass rules to allow both IPv4 and IPv6 traffic
Allow Neighbor and Router Discovery on the external interface
The relevant ICMPv6 types are Router Solicitation, Router Advertisement, Neighbor Solicitation and Neighbor Advertisement (types 133–136). We should never see an incoming Router Solicitation from the ISP; the other three types should be allowed in.
Allow DHCPv6 traffic from the ISP for prefix delegation
The DHCPv6 request is sent to a multicast address and the ISP router replies with its own link-local address as the source address, so state matching doesn't catch it. An explicit pass rule is required for the reply.
/etc/pf.conf
wired = "em1"
wifi = "athn0"
table <martians> {
  0.0.0.0/8 10.0.0.0/8 100.64.0.0/10            \
  127.0.0.0/8 169.254.0.0/16 172.16.0.0/12      \
  192.0.0.0/24 192.0.2.0/24 192.88.99.0/24      \
  192.168.0.0/16 198.18.0.0/15 198.51.100.0/24  \
  203.0.113.0/24 224.0.0.0/3 255.255.255.255/32 \
  ::/128 ::/96 ::1/128 ::ffff:0:0/96 100::/64   \
  2001:10::/28 2001:2::/48 2001:db8::/32        \
  3ffe::/16 fec0::/10 fc00::/7 }
set block-policy drop
set loginterface egress
set skip on lo0
match in all scrub (no-df random-id max-mss 1440)
match out on egress inet from !(egress:network) to any nat-to (egress:0)
antispoof quick for { egress $wired $wifi }
block in quick on egress from <martians> to any
block return out quick on egress from any to <martians>
block all
pass out quick
pass in on { $wired $wifi }
pass in on egress inet proto tcp from any to (egress) port { 80 443 } rdr-to 192.168.1.2
pass in on egress inet6 proto icmp6 all \
  icmp6-type { routeradv neighbrsol neighbradv }
pass in on egress inet6 proto udp \
  from fe80::/10 port dhcpv6-server \
  to fe80::/10 port dhcpv6-client \
  no state

ICMP

You can allow ping for diagnostics.

pass in on egress inet6 proto icmp6 all icmp6-type echoreq
pass in on egress inet proto icmp all icmp-type echoreq

For everything else, read RFC 4890, Recommendations for Filtering ICMPv6 Messages in Firewalls, and decide if any other packet types have a place in your network.

Other Considerations

DHCPv6

The OpenBSD base system does not have a DHCPv6 client at present.

Install the dhcpcd package for prefix delegation to obtain a chunk of public addresses for the local network.

# pkg_add dhcpcd
# rcctl enable dhcpcd

Configure dhcpcd to request a prefix from the ISP and assign prefixes to all the internal interfaces.

See dhcpcd.conf(5) and the pkg-readme for details. RFC 8415 explains the jargon.

/etc/dhcpcd.conf
ipv6only
noipv6rs
duid
persistent
option rapid_commit
require dhcp_server_identifier

script ""

allowinterfaces em0 em1 athn0
interface em0
  ipv6rs
  ia_pd 1 em1/1 athn0/2

SLAAC

The rad daemon serves Router Advertisements. Hosts on the local network use these to assign themselves IPv6 addresses, which means that you often don’t need to run a DHCPv6 server.

# echo 'interface em1' >> rad.conf
# echo 'interface athn0' >> rad.conf
# rcctl enable rad

Other nodes on your network (such as a wireless access point in bridge mode) might also be sending router advertisements. Check the output of ndp -an for nodes with the R (Router) flag set. Any misbehaving nodes might need to have forwarding sysctl’s turned off to stop them generating unnecessary traffic.

DNS

The unbound DNS resolver will function well with the same configuration as shown in the FAQ example. It will run over IPv4, but that doesn’t matter – the DNS responses will have the appropriate AAAA records which contain IPv6 addresses.

There are a few knobs you can tweak – unrelated to IPv6 – that can improve your network.

Protection from DNS Rebinding

DNS rebinding attacks subvert the same-origin policy and convert browsers into open network proxies. These attacks can circumvent firewalls to access internal documents and services […]

Networks can be protected against firewall circumvention by forbidding external host names from resolving to internal IP addresses.

Use the private-address server option in unbound.conf to protect against these attacks. Specify all possible subnets which can be used for internal networks. Any public internet domain names which resolve to the IPv4 or IPv6 subnets specified in this option are removed from the DNS response.

A future version of unbound may have this on by default.

DNS Filtering (Blocking Ads)

Choose which type of hosts you wish to block, such as from one of the lists maintained by StevenBlack.

Automate fetching your chosen blocklist, and converting it into a form that is suitable for unbound. Adapt this dnsblock.sh script and let it run regularly (such as by calling it from /etc/weekly.local). Use the include server option to insert the blocklist rules in the main unbound config file.

/var/unbound/etc/unbound.conf
server:
  interface: 192.168.1.1
  interface: 192.168.2.1
  interface: 127.0.0.1
  access-control: 192.168.1.0/24 allow
  access-control: 192.168.2.0/24 allow
  do-not-query-localhost: no
  hide-identity: yes
  hide-version: yes

  private-address: 10.0.0.0/8
  private-address: 127.0.0.0/8
  private-address: 169.254.0.0/16
  private-address: 172.16.0.0/12
  private-address: 192.168.0.0/16
  private-address: ::1/128
  private-address: ::ffff:0:0/96
  private-address: fd00::/8
  private-address: fe80::/10

  include: /usr/local/etc/blocklist.conf

forward-zone:
  name: "."
  forward-addr: 1.2.3.4  # IP of the upstream resolver

Done

Reboot and enjoy.