Using an IPv6 Prefix Delegation with FreeBSD

So I switched ISP and discovered that my new ISP provides IPv6 even though their sales team doesn't seem to know it. Their technical support team let me know that the static IP address is only available with PPPoE and IPv6 is only available as IPoE along with IPv4 DHCP and they didn't think it was possible to have both IPv6 and use the static IP. It seemed to me that it should be, these are packeted protocols after all, designed to nest and travel independently of other protocols on the same line.

Turns out I was right, it is possible and much of what I learned by doing it applies to simpler setups too.

Disclaimer

This is not a cut and paste and watch it work guide, to use it you will have to understand what is going on and know how to do some things for yourself. It contains the essential information I gleaned while getting this working for myself.

It should be complete enough to cover any pitfalls you may encounter setting up a similar environment as well as helping you avoid any number of dead ends.

Environment

FTTP ISP(1) terminates on a gigabit ethernet and provides:

FreeBSD based router with

I'm using the static IPv4 address and not requesting a dynamic one.

[1] The Irish ISP and Telco Eir.

[sic] That means that renew works properly

Goals

Overview

Set up a VLAN (id 10) on the physical WAN interface then run PPPoE, IPv6 router solicitation and DHCPv6 for delegation over that VLAN.

Provide an RFC-1918 IPv4 network via DHCP on all internal interfaces and route it to the internet via NAT over the PPPoE interface.

Provide a routed /64 network on each internal interface taken from the /56 delegated by the ISP.

Provide DNS that includes dynamically assigned hosts as much as possible.

Prevent anything else from happening except responses to outgoing packets and any deliberately configured IPv4 port forwardings or IPv6 openings.

Basic configuration (rc.conf) gets the VLAN set up and the interfaces in the right states and mpd5 provides the PPPoE connection and presents it on ng0 (mpd5 uses netgraph under the hood so it present a netgraph based interface).

The firewall and NAT are handled by pf which puts all the NAT, IPv4 and IPv6 firewalling in one place. The dynamic prefix makes it difficult (I still don't know how to do it) to open IPv6 ports for incoming connections reliably.

The IPv6 router solicitaion, delegation request and farming out of the /64s and local addresses is all handled by dhcpcd running in ipv6only mode to keep it out of the way of the PPPoE provided IPv4 WAN connection If the IPv4 WAN connection were coming directly over the same VLAN then it would be be necessary to enable IPv4 in dhcpcd and let it handle collecting the IPv4 WAN address and related information.

The final piece of the jigsaw is dnsmasq which combines DNS server, DHCP server and IPv6 router advertising in one integrated package that makes it easy to have dynamically assigned hosts turn up in the local DNS (with both IPv6 and IPv4 addresses much of the time) and uses a very simple format (standard hosts file) for providing static name assignments.

Configuration

In general only the critical parts of config files are shown here boilerplate stuff like hostnames, other services, standard filters are omitted.

The mpd service provides the PPPoE over VLAN-10 on ng0

The internal interfaces on the router must not accept IPv6 router advertisments because they are used to provide them.

We call the VLAN interface "isp" and declare it as the ipv6_cpe_wanif whch prepares it to accept delegations as well as hosts.

    rc.conf
        gateway_enable="YES"

        ifconfig_em0="inet 192.168.143.254 netmask 255.255.255.0"
        ifconfig_em0_ipv6="inet6 -accept_rtadv"
        ifconfig_em1="up"
        ifconfig_em2_ipv6="inet6 -accept_rtadv"

        vlans_em1="isp"
        create_args_isp="vlan 10"
        mpd_enable="YES"

        ipv6_cpe_wanif="isp"
        ipv6_gateway_enable="YES"
        dhcpcd_enable="YES"
        dnsmasq_enable="YES"

        pf_enable="YES"
        pflog_enable="YES"

The necessary magic to bring up PPPoE on the VLAN interface.

    mpd.conf
        create bundle static B1
        set iface route default
        set iface enable tcpmssfix
        create link static L1 isp
        set link action bundle B1
        set link keep-alive 10 60
        set auth authname ISP_AUTHNAME
        set auth password ISP_PASSWORD
        set pppoe iface isp
        open

The firewall sets up NAT on the PPPoE interface (presented as ng0)

The VLAN interface needs to pass ipv6 ICMP and IPv6 traffic to UDP ports 546 and 547 for the SLAAC and DHCPv6 negotiations. These rules could be tightened to specify exactly which phases of the negotiation go in each direction but this is less fragile against protocol changes.

I've left out port forwarding, martian suppression, default block and similar boilerplate. This is NOT a complete pf.conf

    pf.conf
        ext_if="ng0"
        int_if="em0"
        vlan_if="isp"

        nat on $ext_if inet from !($ext_if) -> ($ext_if:0)


        pass quick on $vlan_if proto ipv6-icmp
        pass quick on $vlan_if inet6 proto udp from any to any port 546
        pass quick on $vlan_if inet6 proto udp from any to any port 547

Next in line is dhcpcd which takes care of collecting the IPv6 delegation from the ISP and splitting it around the internal interfaces.

By default IPv6 router solicitation is disabled (to everyone else this is the router).

On the isp interface perform router solicitation and DHCPv6 requests for a normal address (ia_na - not used and not provided but it failed without) and a prefix delegation (ia_pd).

We set an iaid (interface ID) of 1 to avoid clashing with the physical interface and generating warning messages all over the place.

Finally The first /64 is assigned to em0 and the second to em2, in each case the interface gets the :1 address.

    dhcpcd.conf
        duid
        noipv6rs
        waitip 6
        leasetime -1
        ipv6only

        nohook resolv.conf, yp, hostname, ntp

        option rapid_commit
        option routers

        slaac private

        # use the interface connected to WAN
        interface isp
            ipv6rs
            iaid 1
            ia_na 1
            ia_pd 2 em0/1/64 em2/2/64

The final piece of the jigsaw comes wrapped up in that swiss army knife of router software dnsmasq. DNS, DHCP, Router Advertisment all wrapped up in one and because it's integrated the DNS gets updated when DHCP or SLAAC adds hosts.

OK so enable-ra gets it to do router advertisements for IPv6, so don't run rtadvd (it wasn't in rc.conf and it shouldn't be) on the router (do run it or equivalent on everything else).

The two IPv6 dhcp-ranges (one for each interface using 'constructor' and a suffix so that we don't have to know the prefix) have the flags ra-stateless and ra-names - the first arranges that it uses SLAAC to hand out IPv6 addresses and doesn't also hand out a DHCPv6 address the second attempts to extract the MAC from the SLAAC address and match it with a known IPv4 lease to find a hostname. This rather clever hack only works if the SLAAC addresses are MAC based but is very handy for fixed hosts (privacy extensions only make sense for mobiles).

In addition to this my real one has a number of dhcp-host entries some with IP addresses that match entries in hosts.d/lan.hosts others with names where I don't care about the IP address but would like a name. For a while I had a blacklist hosts file in hosts.d but it proved more trouble than help YMMV and at least experimenting is easy.

I used the IPv6 address of the upstream DNS server because it is slightly faster from where I am, there is of course a lot of choice for upstream DNS.

    dnsmasq.conf

        log-dhcp

        interface=em0
        interface=em2

        no-resolv
        enable-ra
        expand-hosts
        domain=MY.DOMAIN.NAME
        addn-hosts=/usr/local/etc/hosts.d

        server=2606:4700:4700::1111

        dhcp-option=option:ntp-server,192.168.143.254
        dhcp-option=option:router,192.168.143.254

        dhcp-range=192.168.143.128,192.168.143.192,12h
        dhcp-range=::1,::ffff,constructor:em0,ra-stateless,ra-names,12h
        dhcp-range=::1,::ffff,constructor:em2,ra-stateless,ra-names,12h
That's it, all the original goals are achieved.