User Tools

Site Tools


NAT6: IPv6 Masquerading Router


This page describes how to set up NAT6 masquerading on your OpenWrt router. Most users won't need or want this, but there are use cases for NAT even on IPv6 networks - e.g. if you want to create a subnet, but the network doesn't support subnetting or prefix delegation. Since NAT6 is available in the netfilter framework of the Linux kernel, it's fairly easy to set up on OpenWrt.


This guide assumes you already have a working IPv6 connection on your OpenWrt router. It's further assumed that you are (mostly) using the OpenWrt default settings, especially those related to IPv6 and DHCPv6 on your LAN interface. See the section FAQ and Troubleshooting for more details.

NAT6 requires the package on OpenWrt which is available in the official repositories. The NAT6 setup outlined in this HOWTO has been reported working on Barrier Breaker and Chaos Calmer, but it should work just fine on Trunk (Designated Driver) as well.

Step-by-step instructions

The following instructions use shell commands to carry out the necessary configuration changes - simply because steps 4 and 5 cannot be done over the webinterface. But everything else could be done via LuCI as well, if you prefer.

  1. Install the package kmod-ipt-nat6
    opkg update && opkg install kmod-ipt-nat6
  2. If you are handing out only local addresses (ie not part of delegated prefix from your upstream): Change the first letter of the “IPv6 ULA Prefix” from to (see the FAQ section below, for an explanation why this is needed)
    uci set network.globals.ula_prefix="$(uci get network.globals.ula_prefix | sed 's/^./d/')"
    uci commit network
  3. Set the DHCP server to “Always announce default router”
    uci set dhcp.lan.ra_default='1'
    uci commit dhcp
  4. Add firewall.d script /etc/firewall.d/with_reload/90-nat6.fw from the section Firewall.d into it.
  5. Make it executable:
    chmod +x /etc/firewall.d/with_reload/90-nat6.fw
  6. Enable the new masq6 option in your firewall on your upstream zone:
    zone_id=$(uci show firewall | sed -ne "s/^firewall\.@zone\[\(\d\+\)\]\.name='wan'\$/\1/p")
    uci set firewall.@zone[${zone_id:?}].masq6=1
    uci commit
  7. Reboot your router - your client devices should now be able to establish IPv6 connections once upstream ipv6 has connected.
  8. In addition, you may now disable the default firewall rule “Allow-ICMPv6-Forward” since it's not needed when masquerading is enabled
    uci set firewall.@rule["$(uci show firewall | grep 'Allow-ICMPv6-Forward' | cut -d'[' -f2 | cut -d']' -f1)"].enabled='0'
    uci commit firewall


Save the following to /etc/firewall.d/with_reload/90-nat6.fw and make it executable.

# Masquerading nat6 firewall.d script.
# Place as: /etc/firewall.d/with_reload/90-nat6.fw and make it executable.
# Then you can configure in /etc/config/firewall per zone, ala where you have:
#   option masq 1
# Just drop this in beneath it:
#   option masq6 1
# For IPv6 privacy (temporary addresses used for outgoing), also add:
#   option masq6_privacy 1
# Hope it's useful!
# ~ trevorj <>
set -eo pipefail
. /lib/
. /lib/functions/
. /usr/share/libubox/
log() {
    logger -t nat6 -s "$@"
get_ula_prefix() {
    uci get network.globals.ula_prefix
validate_ula_prefix() {
    local ula_prefix="$1"
    if [ $(echo "$ula_prefix" | grep -c -E "^([0-9a-fA-F]{4}):([0-9a-fA-F]{0,4}):") -ne 1 ] ; then
        log "Fatal error: IPv6 ULA ula_prefix=\"$ula_prefix\" seems invalid. Please verify that a ula_prefix is set and valid."
        return 1
ip6t() {
    ip6tables "$@"
ip6t_ensure_append() {
    if ! ip6t -C "$@" >/dev/null 2>&1; then
        ip6t -A "$@"
masq6_network() {
    # $config contains the ID of the current section
    local network_name="$1"
    local device
    network_get_device device "$network_name" || return 0
    local done_net_dev
    for done_net_dev in $DONE_NETWORK_DEVICES; do
        if [[ "$done_net_dev" == "$device" ]]; then
            log "Already configured device=\"$device\", so leaving as is."
            return 0
    log "Found device=\"$device\" for network_name=\"$network_name\"."
    if [ $zone_masq6_privacy -eq 1 ]; then
        log "Enabling IPv6 temporary addresses for device=\"$device\"."
        log "Accepting router advertisements on $device even if forwarding is enabled (required for temporary addresses)"
        echo 2 > "/proc/sys/net/ipv6/conf/$device/accept_ra" \
          || log "Error: Failed to change router advertisements accept policy on $device (required for temporary addresses)"
        log "Using temporary addresses for outgoing connections on interface $device"
        echo 2 > "/proc/sys/net/ipv6/conf/$device/use_tempaddr" \
          || log "Error: Failed to enable temporary addresses for outgoing connections on interface $device"
    append DONE_NETWORK_DEVICES "$device"
handle_zone() {
    # $config contains the ID of the current section
    local config="$1"
    local zone_name
    config_get zone_name "$config" name
    # Enable masquerading via NAT6?
    local zone_masq6
    config_get_bool zone_masq6 "$config" masq6 0
    log "Firewall config=\"$config\" zone=\"$zone_name\" zone_masq6=\"$zone_masq6\"."
    if [ $zone_masq6 -eq 0 ]; then
        return 0
    # IPv6 privacy extensions: Use temporary addrs for outgoing connections?
    local zone_masq6_privacy
    config_get_bool zone_masq6_privacy "$config" masq6_privacy 1
    log "Found firewall zone_name=\"$zone_name\" with zone_masq6=\"$zone_masq6\" zone_masq6_privacy=\"$zone_masq6_privacy\"."
    log "Setting up masquerading nat6 for zone_name=\"$zone_name\" with zone_masq6_privacy=\"$zone_masq6_privacy\""
    local ula_prefix=$(get_ula_prefix)
    validate_ula_prefix "$ula_prefix" || return 1
    local postrouting_chain="zone_${zone_name}_postrouting"
    log "Ensuring ip6tables chain=\"$postrouting_chain\" contains our MASQUERADE."
    ip6t_ensure_append "$postrouting_chain" -t nat -s "$ula_prefix" -j MASQUERADE
    config_list_foreach "$config" network masq6_network
    log "Done setting up nat6 for zone=\"$zone_name\" on devices: $DONE_NETWORK_DEVICES"
main() {
    config_load firewall
    config_foreach handle_zone zone
main "$@"

Configuration is done per firewall zone, just like standard masquerading.

This provides two configurables in UCI's firewall zone section:

# in /etc/config/firewall:
config zone
        option name 'wan'
        option input 'DROP'
        option forward 'DROP'
        option output 'ACCEPT'
        option masq '1'
        option mtu_fix '1'
        list network wan
        list network wan6

        ## Above is just an example, below are the nat6 related options:

        option masq6 '1'            # Enable masquerading NAT6
        # option masq6_privacy '1'  # Enable IPv6 privacy extensions

If masq6_privacy is set to 1 (default is 0 aka off), then outgoing IPv6 connections will use temporary addresses that change dynamically. This supposedly makes it harder to track users and surf behavior.

If you prefer to use static addresses (at least as long your ISP assigned prefix doesn't change), leave it at the default of 0 .

The script logs informational and error messages to the system log. The messages are tagged with nat6 .

Read them via say logread .

See the FAQ and Troubleshooting section for more details.

FAQ and Troubleshooting

DHCPv6 default settings

Make sure DHCPv6 uses the following settings (on an unmodified OpenWrt installation these should by the default):

  • “Router Advertisement-Service” and “DHCPv6-Service” are set to server mode*
  • “DHCPv6-Mode” is stateless + stateful
  • “NDP-Proxy” is disabled

You can check this by running the following command:

uci show dhcp.lan | grep -e 'dhcpv6=' -e 'ra=' -e 'ra_management=' -e 'ndp='

The output should look like this:


If the output is different, you are not using the defaults and you should set these options to the ones shown above. If there is an additional line starting with dhcp.lan.ndp, the NDP-Proxy is enabled and should be disabled. Use set to carry out changes, if necessary and don't forget to apply those changes with commit.

*: Setups with “DHCPv6-Service” disabled have been reported working as well by some users. However, if “DHCPv6-Service” is disabled, some clients (e.g. Android devices) will prefer IPv4 over IPv6. Therefore, enabling the “DHCPv6-Service” server mode is recommended.


The init script logs all messages to the system log designated by the tag NAT6. You can view the log messages with the command

logread | grep nat6

If all was configured successfully, the output should look similar to this:

2017-02-04T18:03:44-08:00 notice nat6[]: Found firewall zone_name="wan" with zone_masq6="1" zone_masq6_privacy="1".
2017-02-04T18:03:44-08:00 notice nat6[]: Setting up masquerading nat6 for zone_name="wan" with zone_masq6_privacy="1"
2017-02-04T18:03:44-08:00 notice nat6[]: Ensuring ip6tables chain="zone_wan_postrouting" contains our MASQUERADE.
2017-02-04T18:03:44-08:00 notice nat6[]: Found device="eth1" for network_name="wan".
2017-02-04T18:03:44-08:00 notice nat6[]: Enabling IPv6 temporary addresses for device="eth1".
2017-02-04T18:03:44-08:00 notice nat6[]: Accepting router advertisements on eth1 even if forwarding is enabled (required for temporary addresses)
2017-02-04T18:03:44-08:00 notice nat6[]: Using temporary addresses for outgoing connections on interface eth1
2017-02-04T18:03:44-08:00 notice nat6[]: Already configured device="eth1", so leaving as is.
2017-02-04T18:03:44-08:00 notice nat6[]: Done setting up nat6 for zone="wan" on devices: eth1

Failures that occur during initialization (upon each firewall reload), will be logged and marked as such. Pay attention to any noted as error or fatal error. You're also welcome to run the script from a shell to see it progress; tis just a shell script after all.

Why should the ULA prefix be changed?

The default ULA prefix starting with represents an address that is not globally routed on the internet. Some (or most) clients will prefer the IPv4 route, if they don't have a global IPv6 address assigned, so you need to change the prefix to indicate a global address. It doesn't necessarily have to start with , but to avoid conflicts, you should use a prefix that is not being used yet. The letters are unassigned and therefore safe choices.

Using your ISP assigned prefix as ULA works, too. However, unless you have a static IPv6 prefix assigned by your ISP, this is not recommended, since it can cause address conflicts once the prefix changes.


This howto was inspired by a blog post by :

The solution provided here can be considered more robust and portable, though. Especially the init script comes with benefits such as configurability, sanity checks and error handling, as well as logging (which might help debugging, if something is not working as expected).


- Updated to use a firewall.d script instead of an init script as well as utilize UCI.

docs/guide-user/network/ipv6/ipv6.nat6.txt · Last modified: 2018/03/03 20:35 by