Skip to content

Instantly share code, notes, and snippets.

@gpshead
Last active February 2, 2024 20:16
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gpshead/f4f394593674e5f7a58e9424b4dba989 to your computer and use it in GitHub Desktop.
Save gpshead/f4f394593674e5f7a58e9424b4dba989 to your computer and use it in GitHub Desktop.
IPv6 Only Linux host HOW-TO [Ubuntu]

Making a Linux host IPv6 only (No IPv4 at all)

Notes on what I had to do it setup an IPv6 only Ubuntu bionic 20.04 host.

Revisions:

updated: 2020-04-06 (virus times, whatcha gonna do?)
updated: 2020-10-24 (still virus times...)
2022-03-22, Posted as a Gist because it always should've been...


I doubt these are the best way. Is there a best way? IPv6-only is not an out-of-the-box configuration for Linux distros. Despite many enterprises running their intranets in this manner.

My hardware? nVidia Jetson Nano running Ubuntu bionic. Any hardware will do. Cloud providers seem to finally be getting into the IPv6 routed to your VMs game? If so, consider setting up a couple of those!

TODOs

  • Produce automation for this config so it can be easily adopted and applied.
  • Update the dnscrypt-proxy section for modern distros.

Getting rid of localhost 127.0.0.1

This was quite a challenge to figure out. The Linux kernel and systemd are hardcoded to bring lo up with this address. (!)

A startup script that removes 127.0.0.1 from lo

Create a file called /etc/systemd/system/disable-ipv4-lo.service containing

[Unit]
Description=Disabling the IPv4 loopback lo interface.
After=network.target
After=network-online.target

[Service]
User=root
Type=oneshot
ExecStart=/sbin/ip -4 addr del 127.0.0.1/8 dev lo

[Install]
WantedBy=multi-user.target

Don't forget to sudo systemctl enable disable-ipv4-lo.

Obviously IPv4 localhost existed for a while during system boot, so some things will wound up having bound to that address. systemd on port 2947, sshd, rpcbind and avahi-daemon according to lsof on my system. I'll ignore that for now.

Why get rid of IPv4 on localhost?

For a true IPv6-only experience we don't want to allow software to even be able to use an IPv4 stack for anything. We need to ferret out those evil 127.0.0.1 references as being a problem.

A corporate intranet probably keeps 127.0.0.1 and the IPv4 stack around as so much of the world's software assume it exists.

How do we connect to IPv4-only servers in the real 2020 world such as pathetic Github?

Individual protocol proxy on a local network IPv6 host supporting both protocols work, use /etc/hosts to direct those hostnames there... But this is far from ideal as you must manually configure these for all required combos. I could get away with it for a buildbot's needs... but yuck.

Better, lets use NAT64 🎉

NAT64 is designed for this purpose. We need to use special DNS64 servers that return IPv6 addresses pointing within our NAT64's /96 gateway address for this to work. But after that it is a transparent proxy.

Stop the systemd 127.0.0.53 DNS server.

It by default runs a local resolver on 127.0.0.53:53, you will need to get rid of this before you make 127.0.0.1 go away if you want DNS...

https://medium.com/@niktrix/getting-rid-of-systemd-resolved-consuming-port-53-605f0234f32f

systemctl stop systemd-resolved

Edit /etc/systemd/resolved.conf to contain

[Resolve]
DNS=2001:4860:4860::6464 2001:4860:4860::64
DNSStubListener=no
DNSSEC=no

ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf

Use the DNS64 resolvers exclusively.

You'll notice above we listed two DNS64 resolvers (Google DNS64). But that isn't enough. That list is not exclusive, systemd will merge the DNS servers it hears from the IPv6 Router Advertisement and/or DHCP servers in with those. As my IPv6-only host is on a dual stack network not configured to force everyone to use DNS64 and NAT64... This is bad, it was picking up the normal DNS servers! We gotta disable that behavior:

To accomplish this, create an /etc/systemd/network/dns64.network file:

[Match]
Name=eth0

[Network]
Description=Ethernet configured for IPv6-only
DNS=2001:4860:4860::6464 2001:4860:4860::64
DNSSEC=no

[IPv6AcceptRA]
UseDNS=no

[DHCPv6]
UseDNS=no

[DHCPv4]
UseDNS=no

[route]
destination=64:ff9b::/96
scope=link

This importantly configures systemd to ignore externally provided DNS server lists on the eth0 network interface. Substitute your interface name or other matching criteria as appropriate. This also hardcodes the same DNS servers (I doubt we need them listed in both files... but that's what I've got right now).

After the above you need to make sure systemd-networkd is enabled rather than NetworkManager. (I'm on a wired host, I don't know the implications of this change for other network configuration situations)

sudo systemctl enable systemd-networkd
sudo systemctl start systemd-networkd
sudo systemctl disable NetworkManager
sudo systemctl stop NetworkManager

TODO(gps@2020-04-06): now figure out why systemd-networkd-wait-online is failing and timing out before network-online.target can be satisfied.

# I bet I need to tell it to ignore other network interfaces:
greg@jetson-nano:~$ sudo networkctl
IDX LINK             TYPE               OPERATIONAL SETUP
  1 lo               loopback           carrier     configured
  2 dummy0           ether              degraded    configured
  3 eth0             ether              routable    configured
  4 rndis0           ether              no-carrier  configuring
  5 usb0             ether              no-carrier  configuring
  6 l4tbr0           ether              no-carrier  configured

6 links listed.
greg@jetson-nano:~$ sudo /lib/systemd/systemd-networkd-wait-online
managing: eth0
ignoring: lo

Resolved: I had to edit /etc/systemd/system/network-online.target.wants/systemd-networkd-wait-online.service to add -i eth0 to the command line as the other interfaces are unimportant and were getting in the way.

Configuring your NAT64 gateway

What NAT64 gateway to use?

You need a dual-stack host on your network to run a NAT64 gateway daemon.

I chose WrapSix. There are others. An older popular one is Tayga; untested by me. Whatever you choose, you will need to provide it a unique unused IPv4 address on your LAN beyond what the host already uses. Don't let your local IPv4 DHCP server hand this one out!

The only line in my wrapsix.conf is the ipv4_address 192.168.x.y one. Depending on your system, you may also need to specify the network interface.

On your +NAT64 gateway host (not your IPv6-only host!), create an /etc/systemd/system/wrapsix-nat64.service config:

[Unit]
Description=wrapsix IPv6 NAT64 translation daemon.
After=network.target
After=network-online.target

[Service]
User=root
Type=simple
# Give it a boost as it does network routing for others.
Nice=-1
ExecStart=/usr/local/sbin/wrapsix

[Install]
WantedBy=multi-user.target

After that, fire it up:

sudo systemctl start wrapsix-nat64
sudo systemctl status wrapsix-nat64
sudo systemctl enable wrapsix-nat64

How do we route to 64:ff9b::/96?

Our IPv6-only host needs to how how to route to that reserved NAT64 network as it isn't provided by the internet at large (unless your ISP is amazing).

A static route will do the trick.

Did you see that I already added that in the IPv6-only host's dns64.network config above? If you try to do it any other way while systemd-networkd is the interface, it'll wind up being removed. Use this form of [Route] section:

[Route]
destination=64:ff9b::/96
scope=link

(DEPRECATED - doesn't work) Create a /etc/systemd/system/add-nat64-route.service file:

[Unit]
Description=Adds route to IPv6 NAT64 gateway on local lan.
After=network.target
After=network-online.target

[Service]
User=root
Type=oneshot
ExecStart=/sbin/ip -6 route add 64:ff9b::/96 dev eth0

[Install]
WantedBy=multi-user.target

Don't forget to sudo systemctl enable add-nat64-route.

Why doesn't the route get auto-configured without that?

We'd need a IPv6 Router Advertisement. I tried to get this going with radvd on the NAT64 host and had mixed success. It is possibly due to my radvd config?

Here's my radvd config on my NAT64 gateway host running wrapsix:

# For reference only. radvd was not reliable for me.
interface enp1s0 {
	AdvSendAdvert on;
        # We are NOT a default router gateway, this disables that concept.
	AdvDefaultLifetime 0;
	# Advertise reasonably often.
	MaxRtrAdvInterval 42;
	prefix 64:ff9b::/96 {
		AdvOnLink off;
		AdvAutonomous off;
		# Short lifetime
		AdvValidLifetime 300;
		AdvPreferredLifetime 120;
	};
};

Don't forget to sudo systemctl enable radvd.service and sudo service radvd start.

Misc cleanup

Comment out or remove the 127.0.0.1 lines in /etc/hosts and make sure ::1 lines for those exist.

But wait... why use someone elses DNS64 server?

In the time since I configured the above, I started playing with a DNS-molesting setting on my router (Eero "Secure" service). That, unsurprisingly, works purely by hijacking your DNS. So those traditional UDP port 53 queries to the remote Google Public DNS DNS64 servers were being intercepted and replaced with responses who's answers clearly were not coming from those servers. Result: The internet appears broken on my IPv6 box. "It's always DNS" I've long since kicked their the "Eero Secure" DNS mangling service out of my house.

To fix this and survive malicious DNS-mangling environments, my first thought was to move to DoH or DoT (DNS over HTTPS or DNS over TLS). Every good DNS service out there support those.

In the process of configuring my way around this, I found my way to dnscrypt-proxy as the software of choice. It can do way more than we need; potential configuration confusion? Maybe. It works well and is written in network server safe language (Golang).

Do not use the dnscrypt-proxy package in Ubuntu 18.04, it is quite old. Build or Get a modern dnscrypt-proxy version yourself. v2.0.44 at the time of this writing. Debian Buster has a more recent 2.0.x package, that one might work for you... But beware of the distro package shenanigans. I noticed Ubuntu 18.04's old package tries to add a "dnscrypt-proxy-resolveconf" systemd service into the mix which is really just a command line attempting to extract the address dnscrypt-proxy is listening on from its config file in such a way it would not understand an IPv6 address... (you can't cut -d ":" -f 1 an IPv6 address to strip the trailing :port people!). Installing our own with a simpler config was easier to control.

I was thinking of running dnscrypt-proxy on my NAT64 gateway. Alas, no reason for that. The nat64 gateway is an opaque host that my IPv6 only host never needs to be configured to know the specifics of. Run dnscrypt-proxy locally on your IPv6 host itself. Here's how:

dnscrypt-proxy configuration

Create an /etc/dnscrypt-proxy/dnscrypt-proxy.toml config file:

# An IPv6-only 6to4 config derived from the dnscrypt-proxy 2.0.44 example
# and https://github.com/DNSCrypt/dnscrypt-proxy/wiki/Configuration

server_names = ['google-ipv6']  # Yes, not even the DNS64 ones.

listen_addresses = ['[::1]:53']

ipv4_servers = false
ipv6_servers = true

dnscrypt_servers = true
doh_servers = true

## These are normal, non-encrypted DNS resolvers, that will be only used
## for one-shot queries when retrieving the initial resolvers list.
## No user application queries will ever be leaked through these resolvers.
fallback_resolvers = ['[2001:4860:4860::6464]:53', '[2001:4860:4860::64]:53']

## Always use the fallback resolver before the system DNS settings.
## (because we _are_ the system DNS setting)
ignore_system_dns = true

## Address and port to try initializing a connection to, just to check
## if the network is up. It can be any address and any port.
netprobe_address = '[2001:4860:4860::64]:53'

# Because this would be dumb...
block_ipv6 = false

block_unqualified = true
block_undelegated = true

cache = true
cache_size = 4096
cache_min_ttl = 2400
cache_max_ttl = 86400
cache_neg_min_ttl = 60
cache_neg_max_ttl = 600

#########################
#        Servers        #
#########################

# I got this from https://dnscrypt.info/public-servers/
# explore the configuration, you can use a dynamically updated maintained list
# of these rather than a hardcoded static one like this.

[static]
  [static.'google-ipv6']
  stamp = 'sdns://AgUAAAAAAAAAFlsyMDAxOjQ4NjA6NDg2MDo6ODg4OF2gHvYkz_9ea9O63fP92_3qVlRn43cpncfuZnUWbzAMwbkgdoAkR6AZkxo_AEMExT_cbBssN43Evo9zs5_ZyWnftEUKZG5zLmdvb2dsZQovZG5zLXF1ZXJ5'

# The key to the whole thing... We don't need a DNS64 server, we are one.
# Thanks dnscrypt-proxy!
[dns64]
prefix = ["64:ff9b::/96"]

There's an example config included with dnscrypt-proxy that goes into much more detail on the plethora of config options.

Installing dnscrypt-proxy

Once you have a build of dnscrypt-proxy, install it:

sudo mkdir /opt/dnscrypt-proxy
sudo cp /path/to/my/dnscrypt-proxy-build/dnscrypt-proxy /opt/dnscrypt-proxy/
sudo /opt/dnscrypt-proxy -service install

The latter command should create an /etc/systemd/system/dnscrypt-proxy.service file for you. If you previously installed an OS package and have on laying around or an uninstall symlink to /dev/null, remove that first.

Now edit the generated config to fix a few things. This is what I wound up with:

[Unit]
Description=Encrypted/authenticated DNS proxy
ConditionFileIsExecutable=/opt/dnscrypt-proxy/dnscrypt-proxy


[Service]
StartLimitInterval=5
StartLimitBurst=10
ExecStart=/opt/dnscrypt-proxy/dnscrypt-proxy -config /etc/dnscrypt-proxy/dnscrypt-proxy.toml

WorkingDirectory=/opt/dnscrypt-proxy

Restart=always

RestartSec=120

[Install]
WantedBy=multi-user.target

You may be able to skip that spooky -service install command and just plop that file into place directly? I didn't investigate what all it did, if you are curious it's action appear to come from https://github.com/kardianos/service/blob/master/service_systemd_linux.go.

Like your config? Fire it up:

servicectl start dnscrypt-proxy

Confirm all is well via servicectl status dnscrypt-proxy. If not, happy debugging.

Reconfiguring DNS from before to point to ::1 instead of Google DNS64

Re-edit both /etc/systemd/resolved.conf and /etc/systemd/network/dns64.network files and change the DNS= line from the 2001:4860:4860::64 settings we configured earlier... Replace those with:

DNS=::1

Now force the system to refresh its config:

servicectl restart systemd-resolved
servicectl restart systemd-networkd

After that, you should see /etc/resolv.conf is still your symlink to /run/systemd/resolve/resolv.conf and only contains a single nameserver ::1 line.

Normal software should route queries to your local dnscrypt-proxy dns server which in turn is both proxying your traffic over DNS over HTTPS to the remote DNS server (avoiding man in the middle DNS-rewriting) and it is locally creating IPv6 AAAA records for A-only hosts to your NAT64 64:ff9b::/96 address range.

If you are doing this on a desktop... note that the Trustworthy Brand web browsers such as Mozilla Firefox and Google Chrome may have their own ideas about DNS and speak to their own secure resolvers directly to avoid man in the middle shenanigans, including our special system config. DoH! They can likely be configured not to, but as I'm not doing this to a desktop I'll leave discovering how to do that up to you.

(Deprecated!) Setting up per protocol proxies for internet hosts that do not support IPv6.

As my purpose is to setup a buildbot in this configuration I need to have it speak with github and with the buildbot master. Both of which sadly do not support IPv6. (really Github? In 2019? Sad).

You will need another host on your local network to run a proxy. Get creative with local address and port mappings. IPv6 has plenty of room in the local address space you can make it work. A proxy that appears to work is: https://github.com/elgs/gproxy - aka gpr

I tested a gpr json config looking like this.

{
  "github.com" : {
    "dstPort" : 443,
    "localPort" : 443,
    "localAddr" : "[fdb8:ab16:abe5:1:204:4bff:fe25:bffa]",
    "dstAddr" : "github.com"
  },
  "buildbot.blah.blah" : {
    "dstPort" : 9020,
    "localPort" : 9020,
    "localAddr" : "[fdb8:ab16:abe5:1:204:4bff:fe25:bffa]",
    "dstAddr" : "buildbot.blah.blah"
  }
}

Thankfully you don't actually need gpr. I just left those notes in for reference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment