IPv6 Networking

I wanted to setup IPv6 on my home network but I found the state of documentation and guidance rather lacking. After spending far too long closely reading various sources as well as digging into IETF RFCs and memos plus more trial-and-error than I was really planning on, I decided to coalesce what I learned for posterity.

About My Setup And Other Assumptions

For client devices I'm running Debian 9, Debian 10, Android 10, the various random Linux/*BSD VM, and of course the wife's Windows 10 laptop. I won't be covering how to setup an internal IPv6 network with predictable addresses as I'm currently still using my IPv4 network for this. I just wanted to access YouTube, Reddit, and my VPSes via IPv6. This makes SLAAC an obvious choice over DHCPv6, plus last I read Android didn't fully support DHCPv6.

For networking equipment I have a Mikrotik hAP AC running RouterOS 6.45.9 via the long-term branch. As is typical with SOHO setups the tik wears many hats: router, edge firewall, wireless AP, among others.

My layer 2 network is simple enough - I have a bridge with vLAN tagging enabled, a couple different vLANs, and my wireless radios are bridged together with the RJ-45 ports.

A Word of Caution

Before digging into the config details, I highly recommend adding some firewall rules on the edge to block ingress and egress IPv6 forwarding. If IPv4 NAT/RFC1918 subnets have always existed during your lifetime or maybe you just got really comfortable with IPv4 networking over the past 15 or so years - IPv6 is how IPv4 originally was, there is no NAT gateway to hide your hosts. Each machine can have a globally routable unicast address. If it doesn't have a host based firewall in place, the Bad Guys can do naughty things to your box. The edge firewall is used to control forwarding of traffic over the router and by default most configurations will forward all or most traffic.

Once things are in place go back and remove these blanket block rules.

Configuring the Router

To get things going we are going to need to ask our ISP nicely for an IPv6 prefix. A prefix is roughly analogous to a subnetmask, or at least the complement of using a subnetmask to mask an IPv4 address. If you ask your ISP _very_ nicely, they might even give you a very large prefix.

Now because I've chosen SLAAC as my address management scheme, it is important to note that SLAAC only works with /64 prefixes. I say _only_ because although this is an absolutely _insane_ number of unique addresses to give one customer, the snag for me is that I have multiple vLANs. I would need to have a /64 per vLAN interface in order for SLAAC to work. Fortunately, at least in my area, Comcast is handing out /60 prefixes. That lets me add a few /64 across my vLANs. Remember, these are just IPs to access the Internet, not predictable IPs for internal management.

We need to setup a DHCPv6 client on the tik that requests a prefix, create an IP pool to manage the allocated addresses, make sure Neighbor Discovery is running, and set an address on vLAN interfaces. That is it, with that the tik will be able to start handing out IPv6 addresses with SLAAC.

Create the DHCPv6 client:


/ipv6 dhcp-client add add-default-route=yes interface=eth1 pool-name=ipv6_public_pool prefix-hint=::/56 request=prefix

The prefix-hint is probably larger than it needs to be but Comcast accepts it and hands out a /60. This should automatically create the ipv6_public_pool under /pools for us.

Add an IPv6 address to a vLAN interface:


/ipv6 address add from-pool=ipv6_public_pool interface=vlan201 advertise=yes

Unfortunately you can't control what IP gets assigned here, it just grabs the first available one from the pool, usually $PREFIX::1. The advertise=yes setting is particularly important, it is what lets clients know where your router is so they can negotiate an address with SLAAC.

Make sure Neighbor Discovery is enabled under /ipv6 nd, it is an essential part of connection negotiation for clients on your network. Be sure your vLAN interfaces are included under the list of interfaces to listen on. DNS server information can be advertised here as well but I did not need to configure this since for now the DNS servers assigned by DHCPv4 are good enough.

That is it, now properly configured clients connecting to the network should be getting an IPv6 address.

Configuring the Edge Firewall

IPv6 edge firewalling was an area I was really disappointed in. There is a lot of useful information in the RFCs but they are not really meant to be read and applied directly. But that is what I ended up doing in most cases.

Before we get into the details, it is worthwhile to highlight the significance of ICMPv6. With IPv4, permitting ICMPv4 traffic over the edge is a contentious subject, although a lot of IPv4 features rely on ICMPv4 to function correctly, most sites can get by blocking it completely. My IPv4 firewall is probably a little _too_ restrictive for IPv4. With IPv6, ICMPv6 plays a _major_ role and even replaces protocols like ARP. So blanket blocking ICMPv6 is not an acceptable kludge, things just won't work if you do it.

It is helpful to recognize that the firewall in RouterOS is a thinly veiled wrapper around iptables. That means there are 3 default chains; INPUT, OUTPUT, and FORWARD. INPUT and OUTPUT chains control traffic headed TO and FROM the host running the firewall, in this case our RouterOS system. So to control traffic to our site hosts behind the router, FORWARD rules are used. That always trips people up.

We won't do anything fancy like rate limiting or trying to block ping flooding. This is a sort of bare-bones firewall that just gets the basics done.

You'll notice on the edge firewall we DROP rather than REJECT in many places. Read any opinions on the matter and you'll soon find out that in the firewall world DROP vs. REJECT is something of a holy war. My $0.02 is simply that I used to REJECT on devices routers and servers connected directly to the Internet. The overhead caused from doing a REJECT on all the port rattling was non-trivial. I saw marked latency improvement on SSH connections to my mail server and general network performance at a relative's home where I have a Mikrotik hAP lite TC deployed by switching to DROP. So as a rule of thumb I will DROP for public facing firewalls and REJECT for internal firewalls (to make my troubleshooting life a little easier).

RouterOS supports address lists to simplify rulesets, first lets create one to use in our firewall:


/ipv6 firewall address-list
add address=::1/128 comment="RFC6890 local loopback" list=not_in_internet6
add address=::/128 comment="RFC6890 unspecified address" list=not_in_internet6
add address=100::/64 comment="RFC6890 Discard only" list=not_in_internet6
add address=2001::/23 comment="RFC6890 IETF protocol assignments (may become forwardable in the future)" list=not_in_internet6
add address=2001:2::/48 comment="RFC6890 Benchmarking" list=not_in_internet6
add address=2001:db8::/32 comment="RFC6890 Documentation" list=not_in_internet6
add address=fc00::/7 comment="RFC6890 unique local" list=not_in_internet6
add address=2001:10::/28 comment="RFC6890 ORCHID" list=not_in_internet6
add address=fe80::/10 comment="RFC6890 linked scoped unicast" list=not_in_internet6

Now, lets tackle the INPUT chain:


/ipv6 firewall filter
add action=accept chain=input comment="boiler plate for allowing established connections" connection-state=established,related
add action=drop chain=input comment="don't block INVALID here, this breaks multicast ICMPv6 including SLAAC" connection-state=invalid disabled=yes
add action=accept chain=input comment="allow SSH locally" dst-port=22 in-interface-list=!WAN protocol=tcp
add action=accept chain=input comment="allow DHCPv6 answers from Comcast" dst-port=546 in-interface-list=WAN protocol=udp src-address=fe80::/10
add action=jump chain=input comment="Handle ICMPv6 in a seporate chain" jump-target=icmpv6_input protocol=icmpv6
add action=drop chain=input comment="boiler plate to drop traffic that wasn't explicitly allowed above"

add action=accept chain=icmpv6_input comment="destination unreachable" icmp-options=1 protocol=icmpv6
add action=accept chain=icmpv6_input comment="packet too big" icmp-options=2 protocol=icmpv6
add action=accept chain=icmpv6_input comment="time exceeded" icmp-options=3 protocol=icmpv6
add action=accept chain=icmpv6_input comment="parameter problem" icmp-options=4 protocol=icmpv6
add action=accept chain=icmpv6_input comment="echo request" icmp-options=128 protocol=icmpv6
add action=accept chain=icmpv6_input comment="echo response" icmp-options=129 protocol=icmpv6
add action=accept chain=icmpv6_input comment="router solicitation" icmp-options=133 protocol=icmpv6
add action=accept chain=icmpv6_input comment="router advertisement" icmp-options=134 protocol=icmpv6
add action=accept chain=icmpv6_input comment="neighbor solicitation" icmp-options=135 protocol=icmpv6
add action=accept chain=icmpv6_input comment="neighbor advertisement" icmp-options=136 protocol=icmpv6
add action=accept chain=icmpv6_input comment="inverse neighbor discovery solicitation" icmp-options=141 protocol=icmpv6
add action=accept chain=icmpv6_input comment="inverse neighbor discovery advertisement" icmp-options=142 protocol=icmpv6
add action=accept chain=icmpv6_input comment="certificate path solicitation" icmp-options=148 protocol=icmpv6
add action=accept chain=icmpv6_input comment="certificate path advertisement" icmp-options=149 protocol=icmpv6
add action=accept chain=icmpv6_input comment="listener query" icmp-options=130 protocol=icmpv6 src-address=fe80::/10
add action=accept chain=icmpv6_input comment="listener report" icmp-options=131 protocol=icmpv6 src-address=fe80::/10
add action=accept chain=icmpv6_input comment="listener done" icmp-options=132 protocol=icmpv6 src-address=fe80::/10
add action=accept chain=icmpv6_input comment="listener report version 2" icmp-options=143 protocol=icmpv6 src-address=fe80::/10
add action=accept chain=icmpv6_input comment="multicast router advertisement" icmp-options=151 protocol=icmpv6 src-address=fe80::/10
add action=accept chain=icmpv6_input comment="multicast router solicitation" icmp-options=152 protocol=icmpv6 src-address=fe80::/10
add action=accept chain=icmpv6_input comment="multicast router termination" icmp-options=153 protocol=icmpv6 src-address=fe80::/10
add action=drop chain=icmpv6_input comment="drop any other ICMPv6 input" log=yes log-prefix=":::DEFAULT INPUT ICMPv6 DROP:::" protocol=icmpv6

Next, the OUTPUT chain:


/ipv6 firewall filter
add action=accept chain=output comment="boiler plate to allow router to reply to established or related connections" connection-state=established,related
add action=accept chain=output comment="permit any outbound ICMPv6 traffic" protocol=icmpv6
add action=accept chain=output comment="DHCPv6 requests to ISP" dst-address=ff02::1:2/128 dst-port=547 out-interface-list=WAN protocol=udp src-address=fe80::/10 src-port=546

By default, we allow all outbound traffic, once we've gotten things tightened down and configured, it would be a good idea to come back here and drop any traffic not explicitly allowed out. For purpose built hosts like routers and servers, this is feasible to do. For client workstations, it is a lot harder to block outbound traffic.

Finally, the FORWARD chain:


/ipv6 firewall filter
add action=accept chain=forward comment="boiler plate to accept established and related connections" connection-state=established,related
add action=drop chain=forward comment="boiler plate to drop invalid traffic" connection-state=invalid
add action=drop chain=forward comment="drop bogons headed in" in-interface-list=WAN log=yes log-prefix=":::DROP BOGONS::: comment=drop ingress bogons" src-address-list=not_in_internet6
add action=drop chain=forward comment="drop bogons headed out" log=yes log-prefix=":::DROP BOGONS::: comment=drop egress bogons" out-interface-list=WAN src-address-list=not_in_internet6
add action=jump chain=forward comment="ICMPv6 filtering based on RFC 4890 recommendations for routers" jump-target=icmpv6_forwarding protocol=icmpv6
add action=accept chain=forward comment="allow connections originating from the inside" in-interface-list=!WAN
add action=drop chain=forward comment="boiler plate to drop anything not explicitly allowed above" log-prefix=":::DEFAULT FORWARD DROP:::"

add action=accept chain=icmpv6_forwarding comment="destination unreachable" icmp-options=1 protocol=icmpv6
add action=accept chain=icmpv6_forwarding comment="packet too large" icmp-options=2 protocol=icmpv6
add action=accept chain=icmpv6_forwarding comment="time exceeded" icmp-options=3 protocol=icmpv6
add action=accept chain=icmpv6_forwarding comment="parameter problem" icmp-options=4 protocol=icmpv6
add action=accept chain=icmpv6_forwarding comment="echo request" icmp-options=128 protocol=icmpv6
add action=accept chain=icmpv6_forwarding comment="echo response" icmp-options=129 protocol=icmpv6
add action=accept chain=icmpv6_forwarding comment="home agent address discovery request" icmp-options=144 protocol=icmpv6
add action=accept chain=icmpv6_forwarding comment="home agent address discovery response" icmp-options=145 protocol=icmpv6
add action=accept chain=icmpv6_forwarding comment="mobile prefix solicitation" icmp-options=146 protocol=icmpv6
add action=accept chain=icmpv6_forwarding comment="mobile prefix advertisement" icmp-options=147 protocol=icmpv6
add action=drop chain=icmpv6_forwarding log=yes log-prefix=":::DEFAULT FORWARD ICMPv6 DROP:::"

Multicast Over WiFi - Feels Bad Man

This had me stumped for a couple days. I ran into a problem when I started using wireless more often than my wired clients. Basically I would intermittently experience loss of access to the IPv6 Internet. I noticed that after a while my default gateway would disappear. But the funny thing was I could continue using IPv4 over the same PHY just fine. And somewhat more puzzling was that I was getting echo-replies from the router while pinging the router's fe80::/10 link-local address.

After reaching out for help on Reddit, fixing a few non-related problems, and also chasing a couple red herrings; I finally figured out what was going on.

Multicast has a laundry list of issue which you can read about here. At the base of the issue is how SLAAC works and the fact that I was using 802.11 now more frequently, frequently enough to notice this problem I've had for years. If you look in the Neighbor Discovery menu of RouterOS for IPv6 you'll see two settings: ra-interval and ra-lifetime. ra-interval indicates how often the router will send Router Advertisements to the ff02::1 multicast address. And ra-lifetime indicates the lifetime of the advertised route. In an ideal world, you would get at least one new Router Advertisement before the route expired, refreshing its lifetime. The trouble with multicast is that the sender doesn't necessarily know who all is supposed to receive a packet so by definition ACKs are basically impossible without additional protocol overhead. While a wired connection is no problem it turns out that even a well oiled wireless connection is still too turbulous and multicast Router Advertisement were getting lost in transit, leaving my default gateway to expire!

Fortunately the band-aid fix is simple, RouterOS comes with a multicast helper which is disabled by default. The multicast helper converts multicast to unicast, allowing more reliable transmission. To enable it:


/interface wireless set wlan1 multicast-helper=full

for each wireless interface you are running IPv6 over.

Debian-style ifupdown script Interface Configuration

For my Debian systems, I tend to use the ifupdown scripts. To configure an interface to use IPv6 add the following to /etc/network/interfaces, assume the interface in question is eth0:


iface eth0 inet6 auto
    # prefer RFC4941 privacy extentions
    prviext 2

Client Firewall With ip6tables

IPv6 client firewalling was another sore spot for me, there just seemed to be a lack of guidance on best practices.

Since I still need to support Debian 9 hosts and also I'm really busy, I'll be sticking with iptables syntax and leaning on nft compatibility to carry me until I can find time to port my firewall scripts to nftables.

Again, just like the edge firewall, we won't be blanket blocking ICMPv6 since it is a fundamental part of how IPv6 works, especially with SLAAC.

This is a subset of a larger firewall script I have that covers both IPv4 and IPv6, eventually I'll publish that on GitLab. We don't do anything fancy here like trying to add flood and scan protections. Just enough to get SLAAC working and keep the bad guys out.


#!/usr/bin/env sh

#    Copyright (C) 2008, 2009, 2010, 2012, 2014, 2016, 2019 Alexander Necheff
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, version 3 of the License.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>

IPT=/usr/sbin/iptables
IPT6=/usr/sbin/ip6tables

VERSION='4.2'

# flush existing rule sets
$IPT6 -F
$IPT6 -X

# set default policies,
# can't set REJECT so that will be appended later.
$IPT6 -P FORWARD DROP
$IPT6 -P OUTPUT ACCEPT
$IPT6 -P INPUT DROP

# drop invalid packets
$IPT6 -A INPUT -m conntrack --ctstate INVALID -j DROP
$IPT6 -A INPUT ! -i lo -s ::1/128 -j DROP

# allow established connections
# the default policy is ACCEPT but this makes conntrack happy and provides a nice counter in `ip6tables -nvL`
$IPT6 -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# allow outgoing connections
$IPT6 -A OUTPUT -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT

# local loopback trump card
$IPT6 -A INPUT -i lo -j ACCEPT

# ICMPv6 per RFC 4890; IPv6 relies on heavily
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 1 -j ACCEPT -m comment --comment "Destination Unreachable"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 2 -j ACCEPT -m comment --comment "Packet Too Big"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 3 -j ACCEPT -m comment --comment "Time Exceeded"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 4 -j ACCEPT -m comment --comment "Parameter Problem"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 133 -j ACCEPT -m comment --comment "Router Solicitation"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 134 -j ACCEPT -m comment --comment "Router Advertisement"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 135 -j ACCEPT -m comment --comment "Neighbor Solicitation"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 136 -j ACCEPT -m comment --comment "Neighbor Advertisment"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 141 -j ACCEPT -m comment --comment "Inverse Neighbor Discovery Solicitation"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 142 -j ACCEPT -m comment --comment "Inverse Neighbor Discovery Advertisment"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 148 -j ACCEPT -m comment --comment "Certification Path Solicitation"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 149 -j ACCEPT -m comment --comment "Certification Path Advertisment"

$IPT6 -A INPUT -p icmpv6 --icmpv6-type 130 -s fe80::/10 -j ACCEPT -m comment --comment "Multicast Listener Query"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 131 -s fe80::/10 -j ACCEPT -m comment --comment "Multicast Listener Report"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 132 -s fe80::/10 -j ACCEPT -m comment --comment "Multicast Listener Done"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 143 -s fe80::/10 -j ACCEPT -m comment --comment "Version 2 Mulicast Listener Report"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 151 -s fe80::/10 -j ACCEPT -m comment --comment "Mulitcast Router Advertisment"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 152 -s fe80::/10 -j ACCEPT -m comment --comment "Multicast Router Solicitation"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 153 -s fe80::/10 -j ACCEPT -m comment --comment "Multicast Router Termination"

# ping is allowed globally
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 128  -s 0/0 -j ACCEPT -m comment --comment "Echo Request"
$IPT6 -A INPUT -p icmpv6 --icmpv6-type 129 -s 0/0 -j ACCEPT -m comment --comment "Echo Reply"

# REJECT can't be the real default policy :-(
$IPT6 -A INPUT -j REJECT
$IPT6 -A FORWARD -j REJECT