OPNsense: Tayga NAT64/DNS64 installation

Have you ever dreamed of running a pure IPv6 only network? I have. But until the IPv4 defenders finally kick the bucket we have to rely on transition methods. In this case NAT64 in combination with DNS64 so IPv6 only hosts can talk to IPv4 legacy systems transparently. And here is how you can do it with OPNsense and the help of a recursive BIND resolver.
With OPNsense 20.1.1 Michael (🖇️ 🔐) built a plugin for the NAT64 application tayga. However he did not test it. He only packaged it with a GUI for OPNsense. You currently have to configure the addresses and routes manually via the CLI. In the near future the plugin will do that for you.

Warning: This setup is not as easy as you might think. You can punch a nasty hole in your firewall if you don’t understand how things work. Read chapter “Interface assignment and firewall policies” very carefully!

Try it out first

Before setting up something on your own you can try it out easily. Check out the NAT64/DNS64 service from Level66 (🖇️ 🔐) . Configure your test client so that it uses the mentioned DNS server. Reboot the device afterwards or clear the DNS cache to avoid cache problems.

Background knowledge


NAT64 describes a stateless IP/ICMP translation (SIIT) process. The whole IPv4 address space has 32 bits. The whole IPv6 address space has 128 bits. You can fit the entire IPv4 address space into a single /96 IPv6 network (128 - 32 = 96). And here comes NAT64 into play. When the client wants to reach an IPv4 address it maps the IPv4 address into an IPv6 address. A translation service in the network recognizes the NAT64 prefix and does a stateless IPv6 to IPv4 NAT and vice versa. A commonly used software for that is called tayga (Warning: No TLS!). Let’s say you define 2001:db8:300:b0ff:0:1::/96 in your network as NAT64 prefix. Here are some examples:

Destination IPv4  Mapped address Actual resulting IPv6 address  2001:db8:300:b0ff:0:1: 2001:db8:300:b0ff:0:1:808:808 2001:db8:300:b0ff:0:1: 2001:db8:300:b0ff:0:1:c0a8:118  2001:db8:300:b0ff:0:1: 2001:db8:300:b0ff:0:1:9765:818c

To convert the mapped address to an actual IPv6 address you can use the sipcalc application: sipcalc 2001:db8:300:b0ff:0:1:


In plain DNS an IPv4 address is stored in a so called A record. An IPv6 address is stored in an AAAA record. IPv6 only clients query only for AAAA records. But many hostnames don’t have AAAA records and are therefore only reachable over legacy IP. A DNS64 enabled recursive resolver always returns an AAAA record. The resolver checks if an AAAA record is present. If so the result is passed unaltered to the client. But when no AAAA record is found it resolves for the A record and maps the resulting IPv4 address into the NAT64 prefix and returns the result as “fake” AAAA record to the client. Confused? Here is a table based again on the example NAT64 prefix 2001:db8:300:b0ff:0:1::/96

Hostname Resolved AAAA record Resolved A record AAAA record returned to the client
reddit.com doesn’t exist 2001:db8:300:b0ff:0:1:9765:818c
google.com 2a00:1450:4001:821::200 doesn’t matter 2a00:1450:4001:821::200

Installation and configuration of tayga

You can currently install the tayga package only via CLI: pkg install os-tayga-devel.

Define networks

You have to define three networks:

You can also use the well known prefix for NAT64: 64:ff9b::/96. I decided against it. I’ve a dedicated /64 from my allocation where I slice out smaller networks for development stuff or transfer networks. Or in this case a /96 for NAT64. In the end it doesn’t matter what prefix you use. Routing for me was just easier.

Configure the tayga service via Web GUI

In the WebGUI under Services -> Tayga you can generate the config file. In case of my example networks:

IPv4 Address:
IPv6 Address: 2001:db8:300:b0ff::
IPv6 Prefix: 2001:db8:0300:b0ff:0:1::/96
IPv4 Pool:

Why not use the first address from the NAT64 prefix? Because tayga won’t accept it and refuses to start.

Interface assignment and firewall policies

Next step is to assign the interface via the WebGUI. Go to Interfaces > Assignments and choose the nat64 interface from the dropdown menu and hit the plus button. After that click on the newly assigned interface to configure it. Check the “Enable interface” checkbox. Seet a description if you want to override the display name of it.
Don’t touch anything else! Leave especially “IPv4 Configuration Type” and “IPv6 Configuration Type” to “None”! Hit save.

After that it’s time to create firewall policies as you desire / need. Head over to Firewall -> Rules -> Your interface name. I’ve two rules at the moment:

  1. If you are not using the well known NAT64 prefix but public IPs: Limit IPv6 access to your home network only! If you don’t do that anybody in the world could use your NAT64 range and you’d probably proxy that requests! This is a big security risk! Think twice and test your setup afterwards from an external source. My rule looks like this:
Option Value
IP-Version IPv6
Protocol Any
Source My allocated /56 IPv6 network from my ISP
Destination Any

Notice: I’ve a floating rule that allows IPv6 ICMP traffic from anywhere to anywhere. This affects also my dedicted NAT64 range. Anybody in the world is able to send me ICMPv6 traffic and tayga would translate it! I’ve not found a nice way to limit that yet.

  1. Allow outgoing IPv4 traffic.
Option Value
IP-Version IPv4
Protocol Any
Destination Any

CLI configuration

Now it’s time to issue the CLI commands the plugin should actually do:

Now you can test it out. I use Reddit for that because it’s IPv4 only. One of the IPv4 addresses for me is Try to ping it:

:-$ ping6 2001:db8:300:b0ff:0:1:
16 bytes from 2001:db8:300:b0ff:0:1:9765:18c, icmp_seq=37 hlim=53 time=10.598 ms
16 bytes from 2001:db8:300:b0ff:0:1:9765:18c, icmp_seq=38 hlim=53 time=10.627 ms

If you get replies your setup is working.

Configuring BIND to do DNS64

BIND support DNS64 since 9.8.0. Open your BIND config file which includes the options block. On Arch the whole BIND config is stored under /etc/named.conf. Last time I used debian the config was splitted into multiple files. When you found the options block add the DNS64 config:

dns64 2001:db8:300:b0ff:0:1::/96 {
    clients {

    break-dnssec yes;

For reference: The beginning of my named.conf:

options {
	listen-on port 53 {

	listen-on-v6 port 53 {

    dns64 2001:db8:300:b0ff:0:1::/96 {
		clients {
			# any;
			2001:db8:300:baff:1337:ff:fe32:97c2; # pihole

		break-dnssec yes;

Under clients you can limit the DNS64 mechanism to specific sources. In my case I’ve limited it to my pi hole. In addition I break DNSSEC, because I had some trouble with applications doing the validation on the client. Restart BIND and test it by querying the resolver:

dig @ipv6addressofbind reddit.com AAAA

Your BIND should always return AAAA records now. Either with the original AAAA value or a NAT64 mapped address.

Next steps

After you tested NAT64 and DNS64 it’s time to disable IPv4 and see what doesn’t work anymore :-)
Disable DHCPv4 in OPNsense on your desired network and remove the IPv4 interface configuration. Then restart ever device so that IPv4 is truly disabled and DNS caches are cleared.

I’ve done it on my normal LAN and here is what broke:

On my server only networks I’ve encountered no problems at all.

Du hast einen Kommentar, einen Wunsch oder eine Verbeserung? Schreib mir doch eine E-Mail! Die Infos dazu stehen hier.

🖇️ = Link zu anderer Webseite
🔐 = Webseite nutzt HTTPS (verschlüsselter Transportweg)