In general, Network Address Translation (NAT) solves some problems but should be avoided wherever possible. It has nothing to do with security and is only a short-term solution on the way to IPv6. (Yes, I know, the last 20 years have proven that NAT is used everywhere every time 😉). This applies to all kinds of NATs for IPv4 (SNAT, DNAT, PAT) as well as for NPTv6 and NAT66.
However, there are two types of NATs that do not only change the network addresses but do a translation between the two Internet Protocols, that is IPv4 <-> IPv6 and vice versa. Let’s focus on NAT46 this time. In which situations is it used and why? There’s also a configuration guide for the FortiGates, a downloadable PCAP and Wireshark screenshots.
Don’t get confused: I’m talking about NAT46 this time — not NAT64.
Basically, if you have IPv6-only servers (to avoid the unnecessary dual-stack burden for at least some of your infrastructure), but still want to have those servers accessible for IPv4-only clients, you have to use some kind of protocol translation somewhere. Either through reverse proxies or load balancers or through a NAT46 gateway like Figure 1.
Note that the NAT46 proxy, pronounced NAT-four-six by the way, does not necessarily have to be in the direct path between the client and server — it only has to be accessible by appropriate routes. Nevertheless, having one central firewall in place that does the job fits perfectly for small installations.
Furthermore, note that you need a hostname in the public DNS with an A record for the IPv4 address on your NAT46 gateway. But you don’t need a special DNS device such as a DNS64 box for NAT64 to work.
The basic concept of translating IP and ICMP between IPv4 and IPv6, known as the ‘Stateless IP/ICMP Translation Algorithm (SIIT)’ is described in RFC 7915. Funnily enough, the keyword ‘NAT46’ is not said in the document at all.
Since I have at least one ‘server’ (it’s a Raspberry Pi) running IPv6-only, I was able to test this NAT46 gateway. My True Random PSK Generator at https://random.weberlab.de/ has only an AAAA record, while I used https://random46.weberlab.de/ to access it via legacy IP.
NAT46 on a FortiGate
I’m not a big fan of FortiGate firewalls because they are neither reliable nor sound in many situations. However, they offer many cool and new features that other vendors don’t have. So let’s configure NAT46. I’m using a FortiWiFi-61E with FortiOS v7.0.9 for this setup.
At first, it requires a NAT object, aka ‘virtual IP’ of IPv4, which maps the (public) IPv4 address to the (internal) IPv6 address (Figure 2).
Second, you need an IPv6 pool for the IPv6 source from which the firewall will initiate the internal IPv6 connections. Note the NAT46 checkbox. Also, note that the ‘pool’ is not really a pool but is only capable of one or two IPv6 addresses. It took me a while to figure it out. All ranges I tested weren’t valid until I reduced the pool to one single IPv6 address. As you can see, I chose a very special-looking one — an IPv6 address with 4646 at the very end to spot it easily (Figure 3).
Finally, you need an appropriate Firewall Policy. Note that you must select the ‘NAT46’ feature before you can select the destination, that is, the virtual IP object (Figure 4).
I’m a little scared when looking at this policy in the overview since the destination IPv6 address object says ‘any’ though nothing was selected when creating the policy. It looks like this policy now allows all IPv6 destinations (Figure 5). Hopefully, it doesn’t (that’s what I mean when stating that FortiGate firewalls are not that sound).
Having a look at the Forward Traffic log you can indeed see both Internet Protocols (Figure 6).
The appropriate CLI commands for this feature are below. Note, you have to adjust some of them according to your needs/setup, for example, the profile-group, the inspection-mode, and similar.
config firewall vip edit "random46" set comment "Test NAT46" set extip 22.214.171.124 set nat44 disable set nat46 enable set extintf "wan1" set ipv6-mappedip 2001:470:1f0b:16b0:6986:b8d4:3649:9cbe next end config firewall ippool6 edit "SNAT46" set startip 2001:470:1f0b:16b0::4646 set endip 2001:470:1f0b:16b0::4646 set nat46 enable next end config firewall policy edit 41 set name "random46" set srcintf "wan1" set dstintf "internal" set action accept set nat46 enable set srcaddr "all" set dstaddr "random46" set srcaddr6 "all" set dstaddr6 "all" set schedule "always" set service "HTTP" "HTTPS" "PING" "PING6" set utm-status enable set inspection-mode proxy set profile-type group set profile-group "app-only" set logtraffic all set ippool enable set poolname6 "SNAT46" next end
Having a look at the sessions via CLI, you can see both ones, legacy IP and IPv6. Note the ‘peer’ line for each IP in which the other IP is referenced. Nice! You have to use two different commands to show those sessions, though (that’s what I mean when stating that FortiGate firewalls are not that sound).
fg2 # diagnose sys session list session info: proto=6 proto_state=05 duration=1 expire=0 timeout=3600 flags=00000000 socktype=0 sockport=0 av_idx=0 use=3 origin-shaper= reply-shaper= per_ip_shaper= class_id=0 ha_id=0 policy_dir=0 tunnel=/ vlan_cos=0/255 state=log may_dirty npu f00 statistic(bytes/packets/allow_err): org=1343/11/1 reply=7375/9/1 tuples=2 tx speed(Bps/kbps): 1316/10 rx speed(Bps/kbps): 7230/57 orgin->sink: org pre->post, reply pre->post dev=6->20/20->6 gwy=126.96.36.199/188.8.131.52 hook=pre dir=org act=noop 184.108.40.206:53928->220.127.116.11:443(0.0.0.0:0) hook=post dir=reply act=noop 18.104.22.168:443->22.214.171.124:53928(0.0.0.0:0) peer=2001:470:1f0b:16b0::4646:53928->2001:470:1f0b:16b0:6986:b8d4:3649:9cbe:443 naf=1 hook=pre dir=org act=noop 2001:470:1f0b:16b0::4646:53928->2001:470:1f0b:16b0:6986:b8d4:3649:9cbe:443(:::0) hook=post dir=reply act=noop 2001:470:1f0b:16b0:6986:b8d4:3649:9cbe:443->2001:470:1f0b:16b0::4646:53928(:::0) pos/(before,after) 0/(0,0), 0/(0,0) misc=0 policy_id=41 pol_uuid_idx=606 auth_info=0 chk_client_info=0 vd=0 serial=018cd679 tos=ff/ff app_list=0 app=0 url_cat=0 rpdb_link_id=00000000 ngfwid=n/a npu_state=0x4040400 ofld-O npu info: flag=0x00/0x00, offload=0/0, ips_offload=0/0, epid=0/0, ipid=0/0, vlan=0x0000/0x0000 vlifid=0/0, vtag_in=0x0000/0x0000 in_npu=0/0, out_npu=0/0, fwd_en=0/0, qid=0/0 no_ofld_reason: ofld_fail_reason(kernel, drv): none/not-established, none(0)/none(0) npu_state_err=00/04 total session 1 fg2 # diagnose sys session6 list session6 info: proto=6 proto_state=15 duration=1 expire=0 timeout=3600 flags=00000000 sockport=0 socktype=0 use=3 origin-shaper= reply-shaper= per_ip_shaper= class_id=0 ha_id=0 policy_dir=0 tunnel=/ vlan_cos=0/0 state=log may_dirty npu app_valid statistic(bytes/packets/allow_err): org=1563/11/0 reply=7802/12/0 tuples=2 tx speed(Bps/kbps): 893/7 rx speed(Bps/kbps): 4458/35 orgin->sink: org pre->post, reply pre->post dev=20->23/23->20 hook=pre dir=org act=noop 2001:470:1f0b:16b0::4646:53928->2001:470:1f0b:16b0:6986:b8d4:3649:9cbe:443(:::0) hook=post dir=reply act=noop 2001:470:1f0b:16b0:6986:b8d4:3649:9cbe:443->2001:470:1f0b:16b0::4646:53928(:::0) peer=126.96.36.199:443->188.8.131.52:53928 naf=2 dst_mac=b8:27:eb:03:a0:ac misc=0 policy_id=41 auth_info=0 chk_client_info=0 vd=0 serial=00312c35 tos=ff/ff ips_view=0 app_list=2000 app=40568 url_cat=0 rpdb_link_id = 00000000 ngfwid=n/a npu_state=0x4041808 ofld-R npu info: flag=0x00/0x81, offload=0/0, ips_offload=0/0, epid=0/64, ipid=0/76, vlan=0x0000/0x0000 vlifid=0/76, vtag_in=0x0000/0x0000 in_npu=0/1, out_npu=0/1, fwd_en=0/0, qid=0/3 no_ofld_reason: ofld_fail_reason(kernel, drv): none/not-established, none(0)/none(0) npu_state_err=00/04
Finally, note that this setup required several other things around the mere network config which I have not shown here. That is:
- A hostname for random46.weberlab.de with
onlyat least an A record.
- ServerAliases on the apache2 config for the virtual host.
- An adjusted rewrite condition to forward HTTP -> HTTPS for this ServerAlias.
- Running certbot again to have a valid X.509 certificate with this hostname in the subject alternative name field.
Deeper Look on the wire
I’ve captured some basic runs — doing an HTTP request, getting redirected to HTTPS, and a ping, aka, echo-request. I captured them on the client as well as on the server simultaneously and merged them later on. This capture is within my Ultimate PCAP already, but you can download it as well.
You can easily filter for ip or ipv6 to see only one of those Internet Protocols. Here they are side-by-side, looking at the SYN for the HTTP session (Figure 7).
As expected, the upper-layer protocol stuff is exactly the same after the NAT46 proxy, such as the TLS handshake with its ECDH client key exchange (Figure 8).
However, looking at ICMPv4 vs ICMPv6 messages for echo-requests/-replies, the data portion looks a little different. ICMPv4 in this example used a timestamp that should be silently dropped according to RFC 7915, section 4.2. It looks like the FortiGate isn’t doing it that way but keeps the timestamp information within the data portion (Figure 9).
However, it’s working quite well. Nice! If you can spot any other differences between those translated protocols, please write a comment!
Fun fact: NAT646
The other day I was on a German train using my T-Mobile tethering on my iPhone, which gives perfect IPv6-native access, including DNS64/NAT64. Now, when surfing to this IPv4-only NAT46 domain, it eventually does a 646 translation (Figure 10). 😉
Of course, that would not have happened if the hostname had an AAAA record as well, which would be the case for real-world purposes in which your server hostname has an AAAA record (since it is IPv6-only) *and* the additional A record for the NAT46 translation.
This post is adapted from the original at Weberblog.
Johannes Weber is a network security consultant at Webernetz.net.
The views expressed by the authors of this blog are their own and do not necessarily reflect the views of APNIC. Please note a Code of Conduct applies to this blog.