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.
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.
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
- 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 🙂
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
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
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:
Enable hostapd by editing /etc/default/hostapd:
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.
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:
/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 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:
And once that’s working, any device connecting to the Pi’s wireless will have full access to the Internet, via IPv6 and NAT64.
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
Here is my final configuration (with global IPv6 address and MAC addresses obfuscated for privacy):
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
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 -i any icmp 02:51:34.123206 IP 10.1.1.239 > 220.127.116.11: ICMP echo request, id 3290, seq 4, length 64 02:51:34.135922 IP 18.104.22.168 > 10.1.1.239: ICMP echo reply, id 3290, seq 4, length 64 02:51:34.136173 IP 22.214.171.124 > 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.