A bridging IPv6 Linux firewall for a NTT FLETS internet connection

It’s easy to make an IPv4 DSL connection work in Japan if you’re using NTT FLETS — just use any PPPoE client. FreeBSD, NetBSD, Linux … it doesn’t matter, the process is well-documented.

Using that same FreeBSD/NetBSD/Linux machine to handle IPv6 simultaneously, however, is not well-documented. In fact, I wasn’t able to find this scheme written up anywhere. I think I’ve figured it out, and I’m putting my notes up to help others that might be trying to do the same thing.

NTT does things a bit differently from the rest of the world. IPv4 is handled via PPPoE, which is standard. IPv6 is bridged, not routed. Anyone with a network and/or security engineering background is probably saying “oh, <expletive deleted>” about right now.

It’s been accepted practice to firewall one’s internal network from the outside world by use of a … well, a firewall. For IPv4, this means that one has a machine with two network interfaces between the internal network and the outside. One interface is plugged into the DSL router, one interface plugged into the internal network switch. The internal interface is set to be the default gateway for the internal machines, NATs outbound connections, and screens incoming connections.

In the NTT IPv6 scheme, the router just upstream from the firewall is broadcasting IPv6 router advertisements and responding to DHCPv6 requests. This means that the internal machines need to sit on the same physical network as the firewall’s outside interface. This adds a great deal of complexity to the situation, to say nothing of the security implications.

Most off-the-shelf routers will handle this transparently (the ASUS RT-68U, for example, which is what I was using previously). However, doing it that way resulted in bad client DNS settings — the upstream router was helpfully supplying the NTT DNS servers and search domain in the DHCPv6 response packet, which kept the MacOS machines on the internal network from resolving hosts. IPv6 packet-filtering was essentially non-existent.

So here’s what I came up with:

The firewall is running Arch Linux. This should be adaptable for other distributions that use systemd (yes, I dislike systemd very much, but that’s what’s bring rammed down our throats by the distribution makers, so I’ll just have to suck it up).

I decided to use two external network interfaces — one for IPv4 (via PPPoE), one for IPv6. Separating the two into discrete physical interfaces simplifies the security model quite a bit. Two cheap USB GigE network dongles, plugged into a cheap dedicated switch that is also plugged into the DSL router. They’re enumerated by the Linux kernel as enp0s16f0u1 and enp0s16f0u2.

The third NIC is on-board. The kernel sees it as enp1s0. It sits on the internal network.

I’m going to skip the IPv4 PPPoE, NAT, and firewalling bits. That is documented in excruciating detail elsewhere. Drop me a line if you need help with that part of the configuration.

So, first thing we do is set up the IPv6 bridge between enp1s0 and enp0s16f0u1.

/etc/systemd/network/enp0s16f0u1.network:
[Match]
Name=enp0s16f0u1
[Network]
LinkLocalAddressing=no
IPv6AcceptRA=no
/etc/systemd/network/enp1s0.network:
[Match]
Name=enp1s0
/etc/systemd/network/br0.netdev:
[NetDev]
Name=br0
Kind=bridge
MACAddress=${SOME_RANDOM_MAC_ADDRESS}
/etc/systemd/network/br0.network:
[Match]
Name=br0
[Network]
DHCP=ipv6
IPv6AcceptRA=yes
Address=192.168.x.254/24
/etc/systemd/network/bind.network:
[Match]
Name=enp1s0 enp0s16f0u1
[Network]
Bridge=br0

Restart systemd-networkd. At this point you should be seeing IPv6 traffic on both bridge interfaces, and br0 should have picked up an IPv6 address.

We’re going to be using ip6tables instead of ebtables, because we need to drop bridged packets based on their layer-three characteristics. This is an egregious blurring of the OSI layers, but we’ll note that and move on.

/etc/modules-load.d/firewall.conf:
br_netfilter

Now we need to rig the IPv6 packet filter:

/etc/iptables/ip6tables.rules:

*filter 
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:DROPLOG - [0:0]
:TCP - [0:0]
:UDP - [0:0]
 
# We want to log dropped packets (except DHCPv6)
-A DROPLOG -j LOG
-A DROPLOG -p udp -j REJECT --reject-with icmp6-adm-prohibited
-A DROPLOG -p tcp -j REJECT --reject-with tcp-reset
-A DROPLOG -j REJECT --reject-with icmp6-adm-prohibited

# FORWARD chain handles everything on the bridge
# Block NTT DHCPv6 first.
-A FORWARD -m physdev --physdev-in enp1s0 --physdev-out enp0s16f0u1 -p udp --dport 547 -j DROP
-A FORWARD -m physdev --physdev-in enp0s16f0u1 --physdev-out enp1s0 -p udp --dport 546 -j DROP
-A FORWARD -m conntrack --ctstate INVALID -j DROPLOG
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -p icmpv6 -j ACCEPT
-A FORWARD -m physdev --physdev-in enp1s0 -p udp -m conntrack --ctstate NEW -j ACCEPT
-A FORWARD -m physdev --physdev-in enp1s0 -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j ACCEPT
-A FORWARD -p udp --destination ${IP6_ADDR} --dport ${SOME_PORT} -j ACCEPT
-A FORWARD -j DROPLOG

# INPUT chain affects only this host
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m physdev --physdev-in enp1s0 -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROPLOG
-A INPUT -s fe80::/10 -p ipv6-icmp -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p tcp --dport 443 -j ACCEPT
-A INPUT -p ipv6-icmp -m icmp6 --icmpv6-type 128 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -j DROPLOG
COMMIT

This should be pretty straightforward. The only non-obvious bit is that we’re using the FORWARD table to process packets on the bridge. Anything originating from the firewall itself is going to be handled by the INPUT table, so there’s duplication.

It’s very important to note that we’re dropping the DHCPv6 packets. That’s because the upstream router is also issuing IPv6 router advertisments without DNS information. The slickest way I could see to have internal machines get their IPv6 addresses without having to run a DHCPv6 daemon on the firewall was to just have the clients use those router advertisements to self-configure using SLAAC. The Linux machines happily do both, and MacOS will use SLAAC if it doesn’t see DHCPv6 response packets. Problem solved.

There are still small things that need to be ironed out. I have a gratuitous “-A FORWARD -m physdev --physdev-in enp1s0 -j ACCEPT” in the FORWARD table that really shouldn’t be there, but packets were being dropped for no good reason that I could see. Please don’t hesitate to point out the error if you can see it.

So that’s it. It isn’t perfect, but it works, and it appears to be relatively secure. I hope this helps others that are trying to figure out how to deal with NTT’s interesting IPv6 design.

About Chris Kobayashi

I'm a security systems engineer, specializing in UNIX, network, and physical security. I'm in Tokyo, and I'm mostly retired now. I'm well-versed in both electrical and software engineering, with a particular interest in old computers and game consoles. You can contact me here.
This entry was posted in Network, Security and tagged , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.