DNS64/NAT64 Raspberry Pi WiFi

A Raspberry Pi is set up as an IPv6-only wireless hotspot supporting DNS64/NAT64 for accessing the IPv4 Internet.  This is ideal for testing iOS apps for Apple’s IPv6 compliance.

Rationale

Why not use the MacOS NAT64 option to create a wireless hotspot?  As Apple’s documentation suggests (and is confirmed via experimentation), this DNS64 server always synthesises NAT64 addresses, and the hotspot is unable to pass-through non-NAT64 IPv6 addresses.  This means that devices attached to the hotspot exist in an IPv6-only network, but are unable to connect to the IPv6 Internet.
In order to have complete confidence that our application works in a NAT64 environment, we need to create our own WiFi access point that:

  • Allows access to the IPv6 Internet where available (i.e., for servers such as apple.com that support IPv6)
  • Synthesises NAT64 addresses for servers that do not have IPv6 AAAA records, and proxies those NAT64 requests via a hidden IPv4 interface.

Overview

The hardware is a Raspberry Pi 3 Model B.  This model was selected as it has both wired and wireless networking.  Any Linux box with two network interfaces (at least one of them wireless) will work.  The OS is Raspian Lite (a Debian-like Linux).  The interfaces are:

  • eth0: wired connection to the Internet router
  • wlan0: wireless interface that we will provide NAT64 on

The Pi is connected via ethernet to the Internet router, which provides a dual-stack IPv4/IPv6 connection.  Most ISPs do not support this (I use Internode, who have this set up enabled by default and for no charge), and so a separate IPv6 tunnel would be required.
We then use:

  • hostapd to provide the wireless hotspot, let’s call it pi64
  • dnsmasq to create the IPv6 network on the wireless
  • Google DNS64 to provide DNS64 addresses
  • tayga to perform NAT64 translation

For debugging:

  • A Wifi-capable Mac or Linux box to connect to the Pi’s wireless (you could use the target iOS device directly, but diagnostics will be limited).
  • tcpdump to watch ICMP activity on the Pi

Getting everything working took about 10 hours of work over a week, as I came in with virtually no networking or IPv6 experience, and lacked this guide 🙂

IP Forwarding

Raspian is not configured as a router out of the box.  Edit /etc/sysctl.conf and enable:

net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
net.ipv6.conf.wlan0.accept_ra=2
net.ipv4.conf.all.accept_source_route=1

Apply these settings immediately (or reboot):

sudo sysctl --system

IPv6 subnet

We will set up the wireless interface on wlan0 with a private IPv6 subnet of fec0::/64, with the interface itself having address fec0::1.

allow-hotplug wlan0
iface wlan0 inet6 static
 address fec0::1
 netmask 64

Apply these settings immediately (or reboot):

sudo ifdown wlan0
sudo ifup wlan0

hostapd

First step is to get the Pi advertising its own SSID.  This just involves installing and configuring hostapd.  I used this guide as a starting point:
https://frillip.com/using-your-raspberry-pi-3-as-a-wifi-access-point-with-hostapd/
Enable hostapd by editing /etc/default/hostapd:

DAEMON_CONF="/etc/hostapd/hostapd.conf"

And configure in /etc/hostapd/hostapd.conf.  Most of the default values are fine, take note of:

interface=wlan0
ssid=pi64 # The name of the wireless network to advertise
wpa_passphrase=password # Invent a password for wireless here

Start hostapd with:

sudo service hostapd start

Once this is running we will see the desired SSID being advertised, however it will not yet be possible to connect successfully.

dnsmasq

The next goal is to run an IPv6 network on the Pi’s wireless.  dnsmasq is the DHCP-equivalent for IPv6.  I used this guide:
https://egustafson.github.io/ipv6-dhcpv6.html
/etc/dnsmasq.conf is configured to advertise the IPv6 subnet already set up on the interface wlan0, and to instruct clients to use the Google DNS64 service.  The slaac option instructs clients to assign themselves an IPv6 address within the fec0::/64 subnet based on their MAC address:

interface=wlan0
bind-interfaces
domain-needed
bogus-priv
# DNS server (Google DNS64)
server=2001:4860:4860::6464
# Local DNS name
local=/pi64/
domain=pi64
dhcp-fqdn
# DHCP
enable-ra
dhcp-range=::,constructor:wlan0,slaac
dhcp-option=option6:dns-server,2001:4860:4860::6464
dhcp-authoritative

Start dnsmasq with:

sudo service dnsmasq start

Once this is running a computer attaching to pi64 will receive two IPv6 addresses (a link-local address beginning with fe80: and a global address beginning with 2001:), and will be able to ping IPv6 addresses and even access IPv6-capable servers such as www.apple.com.
Even though we have our DNS64 server dishing out NAT64 addresses (thanks to Google’s public DNS64 server), we need the Pi to proxy traffic to these addresses to the IPv4 Internet, otherwise they will be unreachable.

tayga

tayga is a user-mode network device that listens to IPv6 addresses with the NAT64 prefix, forwards them to an IPv4 private address, and routes the response back again.
First, set up /etc/tayga.conf:

tun-device nat64
ipv4-addr 192.168.255.1
ipv6-addr # a made-up global IPv6 address within your subnet
prefix 64:ff9b::/96
dynamic-pool 192.168.255.0/24

Explanation of the above:

  • tun-device: the name of the network device that tayga owns.  No need to change this.
  • ipv4-addr: an IPv4 address that exists within the dynamic-pool subnet.  No need to change this unless you change dynamic-pool.
  • ipv6-addr: an IPv6 address that exists within the real subnet that your Pi was allocated.  For example, if your Pi’s address is 2001:db8::1/64, then you could set this to 2001:db8::2/64.
  • prefix: the NAT64 prefix that tayga will use; we must use this one as it is the one used by Google’s DNS64.
  • dynamic-pool: a private IPv4 subnet that tayga can allocate addresses in.  No need to change this unless it happens to collide with your actual IPv4 subnet.

tayga does not actually manage the setup of the nat64 network device itself; it assumes you have created and brought up the link, assigned addresses, set up routes and NAT masquerading for the IPv4 subnet.
Thankfully the Debian package manages all of this, however we need to set up /etc/default/tayga:

RUN="yes"
CONFIGURE_NAT44="yes"
IPV4_TUN_ADDR="192.168.255.1"
IPV6_TUN_ADDR=# Your Pi's global IPv6 address

I found the tayga documentation, and especially the comments within this defaults file, to be extremely confusing.  My explanation:

  • RUN: enables the tayga service
  • CONFIGURE_NAT44: required (otherwise tayga’s private IPv4 subnet is not routed correctly)
  • IPV4_TUN_ADDR: must be set to ipv4-addr from tayga.conf
  • IPV6_TUN_ADDR: must be set to the Pi’s global IPv6 address

A persistent problem I ran into while trying to get this working is that with the routes incorrectly configured, pings would be sent successfully, but the response would not be routed all the way back.
Start tayga with:

sudo service tayga start

At this point you can ping a NAT64 address:

ping6 64:ff9b::151.101.1.140

And once that’s working, any device connecting to the Pi’s wireless will have full access to the Internet, via IPv6 and NAT64.

Diagnostics

Log files to watch:

dmesg (this is a program, not a log file)
/var/log/messages
/var/log/daemon.log

Trace all ICMP (i.e., “ping”) activity on all network interfaces:

tcpdump -i any icmp -n

Display network interface configuration:

Show IPv4/IPv6 network addresses on all interfaces
ip addr
ip -6 addr
Show IPv4/IPv6 routing rules.  Note they are applied top-to-bottom.
ip route
ip -6 route
# Show IPv4/IPv6 filtering (I just left everything as ACCEPT
# without any filtering rules)
iptables -L -v
ip6tables -L -v
# Show IPv4 NAT filtering
iptables -L -v -t nat

Final Configuration

Here is my final configuration (with global IPv6 address and MAC addresses obfuscated for privacy):

ip addr

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
 inet 127.0.0.1/8 scope host lo
 valid_lft forever preferred_lft forever
 inet6 ::1/128 scope host
 valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
 link/ether xx:xx:xx:xx:xx:a2 brd ff:ff:ff:ff:ff:ff
 inet 10.1.1.239/24 brd 10.1.1.255 scope global eth0
 valid_lft forever preferred_lft forever
 inet6 2001:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:157d/64 scope global noprefixroute dynamic
 valid_lft 6293sec preferred_lft 2692sec
 inet6 fe80::xxxx/64 scope link
 valid_lft forever preferred_lft forever
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
 link/ether xx:xx:xx:xx:xx:f7 brd ff:ff:ff:ff:ff:ff
 inet6 fec0::1/64 scope site
 valid_lft forever preferred_lft forever
 inet6 fe80::xxxx/64 scope link
 valid_lft forever preferred_lft forever
8: nat64: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 500
 link/none
 inet 192.168.255.1/32 scope global nat64
 valid_lft forever preferred_lft forever
 inet6 2001:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:157d/128 scope global
 valid_lft forever preferred_lft forever

ip route

default via 10.1.1.1 dev eth0 metric 202
10.1.1.0/24 dev eth0 proto kernel scope link src 10.1.1.239 metric 202
192.168.255.0/24 dev nat64 scope link

ip -6 route

64:ff9b::/96 dev nat64 metric 1024
2001:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:157d dev nat64 proto kernel metric 256
2001:xxxx:xxxx:xxxx::/64 dev eth0 proto kernel metric 202 mtu 1492
fe80::/64 dev eth0 proto kernel metric 256
fe80::/64 dev wlan0 proto kernel metric 256
fec0::/64 dev wlan0 proto kernel metric 256
default via fe80::zzzz dev eth0 metric 202 mtu 1492

iptables -L -t nat

Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         
Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination         
MASQUERADE  all  --  192.168.255.0/24     anywhere

The only item of interest is the MASQUERADE rule which provides NAT translation from the tayga private subnet (192.168.255.0) to the Pi’s actual IPv4 subnet.

tcpdump

ping6 64:ff9b::151.101.1.140
tcpdump -i any icmp
02:51:34.123206 IP 10.1.1.239 > 151.101.1.140: ICMP echo request, id 3290, seq 4, length 64
02:51:34.135922 IP 151.101.1.140 > 10.1.1.239: ICMP echo reply, id 3290, seq 4, length 64
02:51:34.136173 IP 151.101.1.140 > 192.168.255.65: ICMP echo reply, id 3290, seq 4, length 64

Note the reply being routed first to the IPv4 subnet that the Pi lives on, and then to the tayga subnet, thanks to the MASQUERADE NAT rule.

1 Reply to “DNS64/NAT64 Raspberry Pi WiFi”

  1. I’ve followed your steps but when doing the ping test, the source IP is not translated to the IP of the device at the echo request stage, instead it sticks to the TUN IPv4 address of tayga. So I never see an echo reply. I confirmed the routes and iptables. Tested with Debian Stretch on APU2. It seems, that the NAT44 is just ignored, though iptables lists the masquerade entry.

Leave a Reply

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