Update: Combined the RELATED and ESTABLISHED rules. Don’t know why I didn’t do that in the first place.
As I’ve mentioned previously, I run several IPFS nodes. I was looking to do some port forwarding for the network that one of them is on, so I took a look at the official documentation and noticed a comment that said
If your router and internet service provider (ISP) support IPv6, enabling it will mitigate some connection issues.
I found this interesting, as I’d never heard of IPv6 having any particularly interesting features besides just having a ton more addresses available. So I started doing some research. Turns out, while it’s true that that’s pretty much the main new feature, there are several others that are built on top of that that are really neat.
To summarize quickly, as it’s a tad outside the scope of this post, IPv6’s absurdly massive address space allows a global, WAN-level IP address to be assigned to every machine. This means that even machines behind a router inside a LAN can have a global IP address, meaning that, among other things, NAT is just simply completely unnecessary in the vast majority of situations. This is huge for peer-to-peer networks, as all I have to do is open up my router’s firewall to allow access to a specific port on my machine, and when my machine advertises its address, it can actually advertise an address that will reach it. Nothing special needs to be done to try to figure out what WAN address is necessary for peers outside the network to try to connect to to get to your machine.
Once I realized what a huge difference this would make to connectivity, I set about getting IPv6 enabled on my network. There were a few minor issues getting it running on my local network, but I got it working fairly quickly and it worked quite nicely.
Then I set about getting it set up on my DigitalOcean droplet. In Docker.
Turns out, IPv6 support is, much to my surprise, rather poor in Docker. It exists, and it could certainly be worse, but it has a number of very important features that are just simply missing. The main feature that it does have is the ability to assign a new IPv6 address from the host machine’s allowed block to each container, providing the same benefits that a LAN receives from a lack of NAT, but inside a single machine.
But there was a complication. Docker uses iptables to set up the various port rules and address bindings that containers are given via the -p
flag that’s passed to the run
command. The Linux kernel has long had support for iptables-style rules with IPv6, via ip6tables, but Docker provides no support for it. None. If you set up your containers to receive their own global addresses, they become completely exposed to the internet. With the default ip6tables rules, every port is just open by default.
So, how do you fix this? First of all, a quick summary of iptables at a basic level. The configuration for iptables splits rules into several different pre-defined ‘tables’. For this example, I’m only really going to deal with the filter
table, which is the default. In each table are several pre-defined ‘chains’. These chains define an ordered series of rules that are applied to packets that are being handled by the kernel. The user can create new chains for organizational purposes, but the pre-defined chains are special. In the filter
table, the pre-defined chains are INPUT
, FORWARD
, and OUTPUT
, used for packets that are being sent specifically to the machine, packets that are being handled by the machine but are not for it, and packets being sent from the machine, respectively. Here’s an example listing, as reported by ip6tables -nL --line-numbers
:
Chain INPUT (policy DROP) num target prot opt source destination 2 ACCEPT all ::/0 ::/0 state RELATED,ESTABLISHED 4 ACCEPT tcp ::/0 ::/0 tcp dpt:22 5 ACCEPT tcp ::/0 ::/0 tcp dpt:80 6 ACCEPT tcp ::/0 ::/0 tcp dpt:443 7 ACCEPT udp ::/0 ::/0 udp dpt:443 Chain FORWARD (policy DROP) num target prot opt source destination 1 ACCEPT all 2030:abcd:ef::/64 ::/0 2 ACCEPT all ::/0 ::/0 state RELATED,ESTABLISHED 5 ACCEPT tcp ::/0 ::/0 tcp dpt:4001 7 ACCEPT udp ::/0 ::/0 udp dpt:4001 Chain OUTPUT (policy ACCEPT) num target prot opt source destination
The first thing to note is the policy DROP
for the INPUT
and FORWARD
chains. This means that packets that are not matched by any rule in the chain are sent to the DROP
target. There are three built-in targets, ACCEPT
, which allows the packet through, REJECT
, which actively rejects it, resulting in an error on the remote end, and DROP
, which just completely ignores the packet.
Important: If you are SSHed in to a server where you intend to make changes, then before you make any changes to the INPUT
chain’s policy, make sure that you explicitly allow TCP port 22, or whichever port you use for SSH, or otherwise you could lose your connection. To do so, simply run
# ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT
Now, you may have noticed that port 4001, the IPFS swarm port, is not in the INPUT
chain. They’re in FORWARD
. The containers, with their own addresses, don’t count as traffic being sent to the machine itself. Because the packets are just being forwarded, there isn’t really a concept with them of input and output. Instead, the only way to differentiate is via the source and destination addresses.
Thankfully, iptables allows you to match addresses based on a mask, rather than one-by-one. For this example, I’m assuming that the Docker containers have addresses in the 2030:abcd:ef:::/64
subnet. The first thing that you’ll need to do is to allow outgoing packets from machines in that subnet to be routed through your machine:
# ip6tables -A FORWARD -s 2030:abcd:ef::/64 -j ACCEPT
Next, you’ll want to allow packets sent to those machines via established connections to get to where they’re going. Otherwise you’re going to wonder, like I did, why my attempts to test the connection by curling a web page from inside a container just hang, despite the outbound packets being allowed. To do this, simply run:
# ip6tables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT
And you’re set. Now you can open up ports more explicitly to expose them to the internet. For example, if you have an IPFS node in a container, like I do, you probably want to expose port 4001:
# ip6tables -A FORWARD -p tcp --dport 4001 -j ACCEPT # ip6tables -A FORWARD -p udp --dport 4001 -j ACCEPT
Remember, once you have the rules set up the way you want, and this is by no means a comprehensive guide to everything you might want to do, such as enabling ICMPv6 packets, if you want to expose a port to access something that’s not in a container, add it to the INPUT
chain, and to expose a port that is in a container, add it to the FORWARD
chain.
I hope you found this helpful. I know that I probably will if I need to do this again in like five years and can’t remember how. Unless, of course, Docker gets their act together and adds proper routing and firewall support to IPv6, like they have in IPv4. Maybe around the same time that DigitalOcean stops only allocating 16 addresses per droplet. No, DigitalOcean, a /124
block is not nearly big enough.
Sigh… Well, what can I expect. IPv6 has only been viable for nearly a decade. I guess I can’t really expect anyone to actually support it properly yet.