Accessing IPv6-only resources via legacy IP: NAT46 on a FortiGate

By on 1 Feb 2023

Category: Tech matters

Tags: , , , ,

1 Comment

Blog home

Cropped from Joshua Sortino's orginal at Unsplash.

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.

Figure 1 — The NAT46 concept.
Figure 1 — The NAT46 concept.

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).

Figure 2 — Input virtual IP on FortiOS v7.0.9.
Figure 2 — Input virtual IP on FortiOS v7.0.9.

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).

Figure 3 — Inputting IPv6 pool.
Figure 3 — Inputting IPv6 pool.

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).

Figure 4 — Inputting the firewall policy.
Figure 4 — Inputting the firewall policy.

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).

Figure 5 — Firewall policy overview.
Figure 5 — Firewall policy overview.

Having a look at the Forward Traffic log you can indeed see both Internet Protocols (Figure 6).

Figure 6 — Forward Traffic log.
Figure 6 — Forward Traffic log.

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 194.247.5.23
        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=194.247.5.23/194.247.4.1
hook=pre dir=org act=noop 85.215.94.29:53928->194.247.5.23:443(0.0.0.0:0)
hook=post dir=reply act=noop 194.247.5.23:443->85.215.94.29: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=194.247.5.23:443->85.215.94.29: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 only at 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).

Figure 7 — SYN for the HTTP session
Figure 7 — SYN for the HTTP session

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).

Figure 8 — TLS client key exchange in Wireshark.
Figure 8 — TLS client key exchange in Wireshark.

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).

Figure 9 — ICMPv4 vs ICMPv6 in Wireshark.
Figure 9 — ICMPv4 vs ICMPv6 in Wireshark.

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). 😉

Figure 10 — A '646' translation on an IPv4-only NAT46 domain.
Figure 10 — A ‘646’ translation on an IPv4-only NAT46 domain.

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.

Rate this article

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.

One Comment

  1. Jay Tuckey

    Hi Johannes, regarding “the destination IPv6 address object says ‘any’ though nothing was selected”

    Probably what is happening here is that you have the IPv6 Feature Visibility turned off on the Fortigate, but turning on a NAT46 rule triggers it to add IPv6 addresses.

    Go to (in Global VDOM if using VDOM’s) System -> Feature Visibility
    There turn on IPv6, and you should be able to see and set the source/destination IPv6 addresses also.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Top