Almost all ISPs today offer both IPv4 and IPv6. The same applies to network hardware for home use. Modern devices can easily communicate via IPv4 and IPv6 simultaneously using so-called dual-stack technology. If the server supports both protocols, the end device decides which protocol is used. Of course, this can only work if the software of the entire infrastructure is laid out twice: once for IPv4 and once for IPv6. This dual implementation of the network software also has disadvantages. To begin with, with the complexity of the software increases the attack surface for cyberattacks. Also, the likelihood of misconfiguration raises, which can make the attackers' work easier. Last but not least, all firewall rules must be defined for both protocols, which is prone to errors.

NAT64/DNS64 technology can provide a remedy. Here, communication in the home network takes place exclusively via IPv6. If a device in the home network wants to communicate with an IPv4 only server, the DNS64 proxy in the home router translates the IPv4 address into a pseudo IPv6 address. When the end device contacts the server via this address, the NAT64 of the home router unpacks the IPv6 packet it receives from the end device and forwards the request to the server as a regular IPv4 package. The server's response is packaged back into an IPv6 package on the way back through the home router before being sent to the end device in the home network. The whole communication appears to the end device as IPv6, the conversion process takes place transparently in the router via NAT64/DNS64.

The implementation presented here is based on the OpenWrt 24.10 operating system and should work on any router on which it can be installed. For initial tests 64 MiB RAM is sufficient, for everyday operation 256 MiB RAM is recommended. Since all outgoing IPv4 packets must be unpacked and repackaged, an up-to-date processor in the router is an advantage.

Although all my software works fine with IPv6, there are a few exceptions: e.g. the VPN client nordvpn does not work in an IPv6-only network (Wireguard works). For such cases, the solution presented here provides as a fallback a conventional IPv4/IPv6 dual stack subnet that can be accessed over the 2.4GHz Wi-Fi.

NAT64 for a IPv6-only network (Jool)

To give us a brief outline of the related technologies, let's refer to the documention on OpenWrt Wiki: Jool:

NAT64 (Network address translation from IPv6 to IPv4) is a technology for allowing an IPv6-only network to connect and interoperate with the IPv4 Internet.

It's very similar to the NAT44 used by most home networks that forwards packets between IPv4 private address space and IPv4 public address space, except it forwards between IPv6 (public) addresses and IPv4 public addresses.

It works in conjunction with several technologies:

  • DNS64, where the DNS returns a specially formatted IPv6 address that encodes the target IPv4 address, which is then handled by NAT64 to forward packets.

  • PREF64, where the router advertises in an ICMPv6 Router Advertisement the NAT64 prefix which devices can use to create a CLAT interface (Android, iOS and macOS uses this).

In OpenWrt, NAT64 can be easily activated using Jool.

The prerequisite for this to work, is that your ISP provides both: IPv4 and IPv6:

wan

Running Jool in a separate network namespace

Note: The above cited article “OpenWrt Wiki: Jool” outlines two options possible to run Jool in namespace. In this section we follow the chapter: Option 2 - Running Jool in a separate network namespace.

Prepare your OpenWrt router

  1. The following packages need to be installed first:

    opkg update
    opkg install kmod-veth ip-full kmod-jool-netfilter jool-tools-netfilter
    
  2. Setup Jool network namespace:

    Create or copy the following shell script to /etc/jool/setupjool.sh

    #!/bin/sh
    ip link add jool type veth peer openwrt
    ip netns add jool
    ip link set dev openwrt netns jool
    ip netns exec jool sh <<EOF
        sysctl -w net.ipv4.conf.all.forwarding=1
        sysctl -w net.ipv6.conf.all.forwarding=1
        sysctl -w net.ipv6.conf.openwrt.accept_ra=2
        sysctl -w net.ipv4.ip_local_port_range="32768 32999"
        ip link set dev lo up
        ip link set dev openwrt up
        ip addr add dev openwrt 192.168.164.2/24
        ip addr add dev openwrt fe80::64
        ip route add default via 192.168.164.1
        modprobe jool
        jool instance add --netfilter --pool6 64:ff9b::/96
        jool global update lowest-ipv6-mtu 1500
        jool pool4 add 192.168.164.2 33000-65535 --tcp
        jool pool4 add 192.168.164.2 33000-65535 --udp
        jool pool4 add 192.168.164.2 33000-65535 --icmp
    EOF
    
  3. Make it executable and execute it once.

    chmod +x /etc/jool/setupjool.sh
    /etc/jool/setupjool.sh
    
  4. Add the following line to /etc/rc.local through the CLI or Luci UI (System - Startup - Local Startup), before the exit 0.

    ...
    /etc/jool/setupjool.sh
    exit 0
    
  5. In order to make it persistent across system upgrades, append the path to the script /etc/sysupgrade.conf through the CLI or Luci UI (System - Backup / Flash Firmware- Configuration)

    cat << EOF >> /etc/sysupgrade.conf
    /etc/jool/setupjool.sh
    EOF
    

Check the success

  1. Check the presence of the new IP interface:

    ip a show dev jool
    12: jool@if11: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
        link/ether 66:29:79:8d:ff:83 brd ff:ff:ff:ff:ff:ff link-netns joold
    
  2. Check that the kernel modules are loaded:

    lsmod | grep jool
    jool                   12288  0 
    jool_common           176128  2 jool_siit,jool
    jool_siit              12288  0 
    nf_defrag_ipv4         12288  2 nf_conntrack,jool
    nf_defrag_ipv6         16384  2 nf_conntrack,jool
    

Configure Jool

  1. Setup jool interface

    Our plan:

    • use IPv4 subnet 192.168.164.1/24
    • allocate one IPv6 /64 with SLAAC
    • route NAT64 prefix to fe80::64
    • configure jool firewall zone and forward from lan zone

    Enter the following in your router terminal:

    # /etc/config/network
    uci set network.jool=interface
    uci set network.jool.proto='static'
    uci set network.jool.device='jool'
    uci set network.jool.ipaddr='192.168.164.1'
    uci set network.jool.netmask='255.255.255.0'
    uci set network.jool.ip6assign='64'
    uci set network.jool.ip6hint='64'
    

    This will cause changes in /etc/config/network

    config interface 'jool'
      option proto 'static'
      option device 'jool'
      option ipaddr '192.168.164.1'
      option netmask '255.255.255.0'
      option ip6assign '64'
      option ip6hint '64'
    
  2. Configure DHCPv4 and SLAAC/DHCPv6

    Enter the following in your router terminal:

    # /etc/config/dhcp
    uci set dhcp.jool=dhcp
    uci set dhcp.jool.interface='jool'
    uci set dhcp.jool.start='100'
    uci set dhcp.jool.limit='150'
    uci set dhcp.jool.leasetime='12h'
    uci set dhcp.jool.ignore='1'
    uci set dhcp.jool.ra='server'
    uci set dhcp.jool.ra_default='2'
    

    This will add the following to /etc/config/dhcp:

    config dhcp 'jool'
      option interface 'jool'
      option start '100'
      option limit '150'
      option leasetime '12h'
      option ignore '1'
      option ra 'server'
      option ra_default '2'
    
  3. Add a static IPv6 route

    Enter the following in your router terminal:

    uci add network route6
    uci set network.@route6[-1].interface='jool'
    uci set network.@route6[-1].target='64:ff9b::/96'
    uci set network.@route6[-1].gateway='fe80::64'
    

    This will add the following to /etc/config/network:

    config route6
      option interface 'jool'
      option target '64:ff9b::/96'
      option gateway 'fe80::64'
    
  4. Add jool firewall zone

    Enter the following in your router terminal:

    # /etc/config/firewall
    uci add firewall zone 
    uci set firewall.@zone[-1].name='jool'
    uci set firewall.@zone[-1].input='ACCEPT'
    uci set firewall.@zone[-1].output='ACCEPT'
    uci set firewall.@zone[-1].forward='REJECT'
    uci add_list firewall.@zone[-1].network='jool'
    

    This will add the following to /etc/config/firewall:

    config zone
      option name 'jool'
      option input 'ACCEPT'
      option output 'ACCEPT'
      option forward 'REJECT'
      list network 'jool'
    
  5. Forward jool zone to wan

    Enter the following in your router terminal:

    uci add firewall forwarding #
    uci set firewall.@forwarding[-1].src='jool'
    uci set firewall.@forwarding[-1].dest='wan'
    
  6. Add br-lan64 Bridge

    # /etc/config/network
    uci add network device
    uci set network.@device[-1].type='bridge'
    uci set network.@device[-1].name='br-lan64'
    

    bridges

  7. Create lan64 interface, with the same properties as lan.

    # /etc/config/network
    uci set network.lan64=interface
    uci set network.lan64.proto='static'
    uci set network.lan64.device='br-lan64'
    uci set network.lan64.ip6assign='64'
    uci set network.lan64.ra_pref64='64:ff9b::/96'
    
  8. Correct assignment length of lan:

    uci set network.lan=interface
    uci set network.lan.ip6assign='64'
    uci commit
    

    lan64

  9. Set up DHCP on lan64

    Android and iOS as well as macOS are working fine in IPv6-only networks. To signal to clients which are able and willing to run IPv6-only, the DHCP option 108 was introduced with RFC8925.

    Add this option to the DHCPv4 configuration of the desired zone e.g., lan64 file /etc/config/dhcp

    30 minutes = 1800 seconds = 0x708 seconds
    

    This results in:

    dhcp_option ‘108,0:0:7:8’
    

    After this all your mobile and macOS devices will drop the IPv4 lease and run in IPv6-only mode.

    We also add to the router advertisement messages, the NAT64 prefix the network is using. This is relatively a new feature introduced with v23.05.0.

    option ra_pref64 '64:ff9b::/96'
    

    Enter the following in your router terminal:

    # /etc/config/dhcp
    uci set dhcp.lan64=dhcp
    uci set dhcp.lan64.interface='lan64'
    uci set dhcp.lan64.start='100'
    uci set dhcp.lan64.limit='150'
    uci set dhcp.lan64.leasetime='12h'
    # This is a IPv6 only network.
    uci add_list dhcp.lan64.dhcp_option='108,0:0:7:8'
    uci set dhcp.lan64.ra='server'
    uci add_list dhcp.lan64.ra_flags='managed-config'
    uci add_list dhcp.lan64.ra_flags='other-config'
    # NAT64 prefix.
    uci set dhcp.lan64.ra_pref64='64:ff9b::/96'
    uci set dhcp.lan64.dhcpv6='server'
    
  10. Add firewall lan64 zone

    uci add firewall zone
    uci add_list firewall.@zone[-1].network='lan64'
    uci set firewall.@zone[-1].name='lan64'
    uci set firewall.@zone[-1].input='ACCEPT'
    uci set firewall.@zone[-1].output='ACCEPT'
    uci set firewall.@zone[-1].forward='ACCEPT'
    uci set firewall.@zone[-1].family='ipv6'
    
  11. Forward lan64 zone to jool

    Enter the following in your router terminal:

    uci add firewall forwarding
    uci set firewall.@forwarding[-1].src='lan64'
    uci set firewall.@forwarding[-1].dest='jool'
    
  12. Forward lan64 zone to wan

    Enter the following in your router terminal:

    uci add firewall forwarding
    uci set firewall.@forwarding[-1].src='lan64'
    uci set firewall.@forwarding[-1].dest='wan'
    uci commit
    

    firewall

  13. Add some firewall rules for pinging and special protocols.

    The following is a copy from the Allow-IPSec-ESP rule for lan64:

    uci add firewall rule
    uci set firewall.@rule[-1].name='Allow-IPSec-ESP-lan64'
    uci set firewall.@rule[-1].dest='lan64'
    uci set firewall.@rule[-1].family='ipv6'
    uci set firewall.@rule[-1].proto='esp'
    uci set firewall.@rule[-1].src='wan'
    uci set firewall.@rule[-1].target='ACCEPT'
    

    The following is a copy from the Allow-ISAKMP rule for lan64:

    uci add firewall rule
    uci set firewall.@rule[-1].name='Allow-ISAKMP-lan64'
    uci set firewall.@rule[-1].dest='lan64'
    uci set firewall.@rule[-1].dest_port='500'
    uci set firewall.@rule[-1].family='ipv6'
    uci set firewall.@rule[-1].proto='udp'
    uci set firewall.@rule[-1].src='wan'
    uci set firewall.@rule[-1].target='ACCEPT'
    
  14. Add firewall forwarding to allow communication between subnets:

    uci add firewall forwarding
    uci set firewall.@forwarding[-1].src='lan64'
    uci set firewall.@forwarding[-1].dest='lan'
    uci add firewall forwarding
    uci set firewall.@forwarding[-1].src='lan'
    uci set firewall.@forwarding[-1].dest='lan64'
    
  15. Apply changes

    uci commit
    service firewall restart
    service network restart
    

    Or, if the following test does not work, reboot:

    reboot
    

    Try a complete reboot before you start tweaking and debugging.

Check success

See also this chapter.

  1. To test the lan64 interface, connect it to one Wi-Fi antenna, e.g. 2,4 Hz and connect your desktop to it:

    uci set wireless.default_radio0.network='lan64'
    uci commit
    service network restart
    
  2. Confirm working NAT64 from your router

    ping 64:ff9b::1.1.1.1
    
  3. Make sure it works also from the connected devices

    ping 64:ff9b::1.1.1.1
    

Configure the DNS64 proxy server

See also:DNS64 - Wikipedia

  1. Disable dnsmasq:

    uci set dhcp.@dnsmasq[-1].port='0'
    
  2. Activate Unbound:

    # /etc/config/unbound
    uci set unbound.ub_main.enabled='1'
    uci set unbound.ub_main.dns64='1'
    uci del unbound.ub_main.dhcp4_slaac6
    uci del unbound.ub_main.query_minimize
    uci del unbound.ub_main.query_min_strict
    uci set unbound.ub_main.unbound_control='1'
    uci set unbound.ub_main.dhcp4_slaac6='1'
    uci set unbound.ub_main.rebind_localhost='1'
    
    # Set trigger to `lan64`
    uci del unbound.ub_main.iface_trig
    uci add_list unbound.ub_main.iface_trig='lan64'
    uci add_list unbound.ub_main.iface_trig='wan'
    uci set unbound.ub_main.dhcp_link='odhcpd'
    
    # Set ISP upstream DNS server as provider
    uci set unbound.auth_icann.fallback='0'
    uci set unbound.fwd_isp.fallback='0'
    uci set unbound.fwd_isp.enabled='1'
    uci set unbound.fwd_google.fallback='0'
    uci set unbound.fwd_cloudflare.fallback='0'
    

    You'll see error message about a missing path; we correct that in the next step.

  3. Configure Odhcpd:

    #/etc/config/dhcp:
    uci set dhcp.odhcpd=odhcpd
    uci set dhcp.odhcpd.leasefile='/var/lib/odhcpd/dhcp.leases'
    uci set dhcp.odhcpd.leasetrigger='/usr/lib/unbound/odhcpd.sh'
    uci set dhcp.odhcpd.maindhcp='1'
    
    
  4. Reboot

    uci commit
    reboot
    

Check success

  1. Connect your PC to Wi-Fi 2.4GHz (which is currently assigned to the lan64 interface).

  2. Check with ip a that the interface has no IPv4. We are IPv6 only now!

    5: wlp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
        link/ether c8:94:02:bd:a6:d3 brd ff:ff:ff:ff:ff:ff
        inet6 fdde::f6f/128 scope global dynamic noprefixroute 
           valid_lft 43178sec preferred_lft 545sec
        inet6 2001:XXX:XXXX:XXXX::f6f/128 scope global  dynamic noprefixroute 
           valid_lft 1745sec preferred_lft 545sec
        inet6 2001:XXX:XXXX:XXXX:d80d:76ed:d47:476d/64 scope global temporary dynamic 
           valid_lft 1745sec preferred_lft 545sec
        inet6 2001:XXX:XXXX:XXXX:56e4:ae0e:28b6:353f/64 scope global dynamic mngtmpaddr noprefixroute 
           valid_lft 1745sec preferred_lft 545sec
        inet6 fdde::29c7:dd7c:2002:249a/64 scope global temporary dynamic 
           valid_lft 593201sec preferred_lft 74695sec
        inet6 fdde::993f:600d:6c8a:fc52/64 scope global mngtmpaddr noprefixroute 
           valid_lft forever preferred_lft 604795sec
        inet6 fe80::79a3:b6cb:84f5:aecf/64 scope link noprefixroute 
           valid_lft forever preferred_lft forever
    
  3. Observe the unbound's log:

    OpenWRT Luci ->  Services -> Recursive DNS -> Status -> Log
    

    This is how mine looks like:

    Tue Jan 14 21:46:37 2025 daemon.info unbound: [4056:0] info: start of service (unbound 1.21.0).
    Tue Jan 14 21:46:38 2025 daemon.info unbound: [4056:0] info: generate keytag query _ta-4f66. NULL IN
    Tue Jan 14 21:48:16 2025 daemon.info unbound: [4056:0] info: service stopped (unbound 1.21.0).
    Tue Jan 14 21:48:16 2025 daemon.info unbound: [4056:0] info: server stats for thread 0: 96 queries, 27 answers from cache, 69 recursions, 0 prefetch, 0 rejected by ip ratelimiting
    Tue Jan 14 21:48:16 2025 daemon.info unbound: [4056:0] info: server stats for thread 0: requestlist max 13 avg 2.89855 exceeded 0 jostled 0
    Tue Jan 14 21:48:16 2025 daemon.info unbound: [4056:0] info: average recursion processing time 0.182811 sec
    Tue Jan 14 21:48:16 2025 daemon.info unbound: [4056:0] info: histogram of recursion processing times
    Tue Jan 14 21:48:16 2025 daemon.info unbound: [4056:0] info: [25%]=0.0551595 median[50%]=0.120149 [75%]=0.276187
    Tue Jan 14 21:48:16 2025 daemon.info unbound: [4056:0] info: lower(secs) upper(secs) recursions
    Tue Jan 14 21:48:16 2025 daemon.info unbound: [4056:0] info:    0.004096    0.008192 3
    ...
    Tue Jan 14 21:48:16 2025 daemon.info unbound: [4056:0] info:    0.524288    1.000000 4
    Tue Jan 14 21:48:24 2025 daemon.notice unbound: [5112:0] notice: init module 0: respip
    Tue Jan 14 21:48:24 2025 daemon.notice unbound: [5112:0] notice: init module 1: dns64
    Tue Jan 14 21:48:24 2025 daemon.notice unbound: [5112:0] notice: init module 2: validator
    Tue Jan 14 21:48:24 2025 daemon.notice unbound: [5112:0] notice: init module 3: iterator
    Tue Jan 14 21:48:24 2025 daemon.info unbound: [5112:0] info: start of service (unbound 1.21.0).
    Tue Jan 14 21:48:24 2025 daemon.info unbound: [5112:0] info: generate keytag query _ta-4f66. NULL IN
    
  4. Query an IPv4 only domain from your PC, e.g.:

    nslookup web.de
    

    Observe the last 2 IPv6 addresses, they are composed by unbound.

    Server:  fddd:0:0:1::1
    Address:	fddd:0:0:1::1#53
    
    Non-authoritative answer:
    Name:	web.de
    Address: 82.165.229.83
    Name:	web.de
    Address: 82.165.229.138
    Name:	web.de
    Address: 64:ff9b::52a5:e58a
    Name:	web.de
    Address: 64:ff9b::52a5:e553
    
  5. Open an IPv4 only website, e.g. https://web.de or https://ipv4.google.com.

Optional: Connect your LAN to the new IPv6 subnet lan64

Currently, only the 2.4GHz Wi-Fi is connected to the lan64 IPv6 only subnet, while the LAN is still connected to the dual stack lan subnet. This section explains how to invert this assignment:

  • IPv6 only subnet lan64: Ethernet port LAN + 5GHz Wi-Fi
  • IPv6 and IPv4 dual stack lan: 2,4GHz Wi-Fi
  1. Higher weights have priority and get lower IPv6 subnets. We want 0 for lan64. On my system lan gets 10:

    # /etc/config/network
    uci set network.lan=interface
    uci set network.lan.ip6weight='8'
    uci set network.lan64=interface
    uci set network.lan64.ip6weight='9'
    uci commit
    
  2. Assign the Ethernet lan ports to lan64:

    Open a terminal in your OpenWrt router, and edit the file:

    vi /etc/config/network
    

    Edit the device sections. Before the change:

    config device
        option name 'br-lan'
        option type 'bridge' 
        list ports 'lan1'
        list ports 'lan2'
    
    config device 
        option type 'bridge' 
        option name 'br-lan64'
    

    After the change:

    config device
        option name 'br-lan'
        option type 'bridge' 
    
    config device 
        option type 'bridge' 
        option name 'br-lan64'
        list ports 'lan1'
        list ports 'lan2'
    
    service network restart
    
  3. Attach the 2.4GHz Wi-Fi to the lan interface.

    uci set wireless.default_radio0.network='lan'
    uci set wireless.default_radio1.network='lan64'
    
  4. Reboot

    uci commit
    reboot
    

Conclusion after 3 weeks of operation

In day-to-day operation, it is not noticeable at all that all communication in the home network runs via IPv6. The only software that does not run so far is the proprietary NordVPN client. One option is to switch to Wireguard (wgnord). Or, connect the computer to the Internet via 2.4GHz Wi-Fi before establishing the VPN connection as usual. A conventional dual stack IPv4/IPv6 connection runs on this interface as backup.