FortiGate
FortiGate Next Generation Firewall utilizes purpose-built security processors and threat intelligence security services from FortiGuard labs to deliver top-rated protection and high performance, including encrypted traffic.
vdralio
Staff
Staff
Article Id 195448

Description

 

This article describes how to configure Hairpin NAT.

Hair-pinning also known as NAT loopback is a technique where a machine accesses another machine on the LAN or DMZ via an external network.

Traffic goes through the LAN interface to the Internet, then goes back to the same interface, connecting to it is External IP. Traffic is then forwarded by Fortigate through a virtual IP to the local destination of LAN or DMZ.

 


hairpin_diag.png

 


Scope

 

FortiGate.

 

Solution

 

If the external IP belongs to FortiGate (IP address of an external interface), FortiGate will require a different set of rules when the external IP is just from range, but not directly configured on FortiGate’s interfaces. In all examples, traffic will be flowing like this: Client -> external IP -> FortiGate -> internal IP -> Server.

 

Example 1:

External IP is the same as the external interface and uses VIP1 from the diagram. In this example, does not matter if extintf is any or wan.

 

config firewall vip
    edit "VIP1"
        set extip 20.0.0.1
        set extintf "any"
        set portforward enable
        set mappedip "172.16.1.2"
        set extport 10443
        set mappedport 10443
     next
end

 

config firewall vip
    edit "VIP1"
        set extip 20.0.0.1
        set extintf "wan"
        set portforward enable
        set mappedip "172.16.1.2"
        set extport 10443
        set mappedport 10443
    next
end

 

In both scenarios, extintf any or WAN, needs to have two firewall policies. One is to allow access from LAN to WAN and the second policy from WAN to DMZ.

 

config firewall policy
    edit 1
        set name "AllLan1"
        set srcintf "lan"
        set dstintf "wan"
        set srcaddr "all"
        set dstaddr "all"
        set action accept
        set schedule "always"
        set service "ALL"
        set logtraffic all
        set nat enable
    next
    edit 3
        set name "VIP"
        set srcintf "wan"
        set dstintf "dmz"
        set srcaddr "all"
        set dstaddr "VIP1"
        set action accept
        set schedule "always"
        set service "ALL"
        set logtraffic all
    next
end

 

Example 2:

The external IP address is from the same subnet but does not belong to FortiGate directly. Use VIP2 from the diagram. There are two options to select extintf: any or specific.

 

Option 1:

 

config firewall vip
    edit "VIP2"
        set extip 20.0.0.2
        set extintf "any"
        set portforward enable
        set mappedip "172.16.1.2"
        set extport 10443
        set mappedport 10443
    next
end

 

If the interface is any, there will be just one firewall policy from LAN to DMZ with VIP2 as the destination address.

 

config firewall policy
    edit 3
        set name "VIP"
        set srcintf "lan"
        set dstintf "dmz"
        set srcaddr "all"
        set dstaddr "VIP2"
        set action accept
        set schedule "always"
        set service "ALL"
        set logtraffic all
    next
end

 

Note:

If srcintf-filter to VIP2 is configured, the LAN port will need to be a member of that filter.  In that case, the same firewall policy as the previous one will be enough.

If LAN will not be a member of the filter, but only WAN, hairpin will not work even if firewall policies are corrected when srcintf will be WAN directly (next option).

 

Option 2:

With option 2, when the external IP address is from the same subnet but does not belong to FortiGate directly:
This setup uses the same set of firewall policies as in Example-1. One policy is in place to allow access from LAN to WAN, and the second policy is for WAN to DMZ.

 

Hairpin-NAT_1.png

config firewall vip

edit "VIP"

set uuid cf7d355c-5729-51ef-abab-952e8d72ad56

set extip 172.16.16.10       <- External IP address does not belong to FortiGate directly.

set mappedip "10.10.10.2"

set extintf "any"

next

end

 

config firewall policy

edit 1

set name "Lan-to-Wan"

set uuid bf1cdbee-574d-51ef-9242-288e63f80e1f

set srcintf "port3"

set dstintf "port4"
set action accept

set srcaddr "Lan_Subnet"

set dstaddr "Wan_Subnet"

set schedule "always"

set service "ALL"

set nat enable

next

end

 

config firewall policy

edit 2

set name "Wan-to-Dmz"
set uuid dbdfac02-574d-51ef-984d-ab53ab0d1258
set srcintf "port4"
set dstintf "port2"
set action accept
set srcaddr "Lan_Subnet"
set dstaddr "VIP"
set schedule "always"
set service "ALL"
set nat enable

next

end

 

id=65308 trace_id=1 func=print_pkt_detail line=5895 msg="vd-root:0 received a packet(proto=1, 192.168.1.2:33214->172.16.16.10:2048) tun_id=0.0.0.0 from port3. type=8, code=0, id=33214, seq=1."
id=65308 trace_id=1 func=init_ip_session_common line=6076 msg="allocate a new session-0000035b, tun_id=0.0.0.0"
id=65308 trace_id=1 func=iprope_dnat_check line=5331 msg="in-[port3], out-[]"
id=65308 trace_id=1 func=iprope_dnat_tree_check line=823 msg="len=1"
id=65308 trace_id=1 func=__iprope_check_one_dnat_policy line=5191 msg="checking gnum-100000 policy-1"
id=65308 trace_id=1 func=get_new_addr line=1228 msg="find DNAT: IP-10.10.10.2, port-0(fixed port)"
id=65308 trace_id=1 func=__iprope_check_one_dnat_policy line=5286 msg="matched policy-1, act=accept, vip=1, flag=104, sflag=2000000"
id=65308 trace_id=1 func=iprope_dnat_check line=5343 msg="result: skb_flags-02000000, vid-1, ret-matched, act-accept, flag-00000104"
id=65308 trace_id=1 func=fw_pre_route_handler line=178 msg="VIP-10.10.10.2:33214, outdev-unknown"
id=65308 trace_id=1 func=__ip_session_run_tuple line=3515 msg="DNAT 172.16.16.10:8->10.10.10.2:33214"
id=65308 trace_id=1 func=vf_ip_route_input_common line=2605 msg="find a route: flag=04000000 gw-10.10.10.2 via port2"
id=65308 trace_id=1 func=iprope_fwd_check line=789 msg="in-[port3], out-[port2], skb_flags-020000c0, vid-1, app_id: 0, url_cat_id: 0"
id=65308 trace_id=1 func=__iprope_check line=2292 msg="gnum-100004, check-ffffffffa002c2a7"
id=65308 trace_id=1 func=__iprope_check_one_policy line=2044 msg="checked gnum-100004 policy-0, ret-matched, act-accept"
id=65308 trace_id=1 func=__iprope_user_identity_check line=1819 msg="ret-matched"
id=65308 trace_id=1 func=__iprope_check_one_policy line=2262 msg="policy-0 is matched, act-drop"
id=65308 trace_id=1 func=__iprope_check line=2309 msg="gnum-100004 check result: ret-matched, act-drop, flag-08010800, flag2-00004000"
id=65308 trace_id=1 func=iprope_fwd_check line=826 msg="after iprope_captive_check(): is_captive-0, ret-matched, act-drop, idx-0"
id=65308 trace_id=1 func=iprope_fwd_auth_check line=845 msg="after iprope_captive_check(): is_captive-0, ret-matched, act-drop, idx-0"
id=65308 trace_id=1 func=fw_forward_handler line=738 msg="Denied by forward policy check (policy 0)"

 

Traffic can be observed not leaving the firewall even though the correct firewall policy is configured from WAN to DMZ. After matching with the VIP, the traffic is matched with 'policy 0' instead of the configured firewall policy.

 

This can also be verified by modifying the VIP configuration with an IP address that belongs to the firewall directly. After making these changes we could observe that traffic is matched the correct configured policy instead of 'policy 0'.

 

config firewall vip

edit "VIP"

set uuid cf7d355c-5729-51ef-abab-952e8d72ad56

set extip 172.16.16.1         <- External IP address belong to FortiGate directly.

set mappedip "10.10.10.2"

set extintf "any"

next

end

 

With the same set of Firewall Policies used above:

 

config firewall policy

edit 1

set name "Lan-to-Wan"

set uuid bf1cdbee-574d-51ef-9242-288e63f80e1f
set srcintf "port3"
set dstintf "port4"
set action accept
set srcaddr "Lan_Subnet"
set dstaddr "Wan_Subnet"
set schedule "always"
set service "ALL"
set nat enable

next

end

 

config firewall policy

edit 2

set name "Wan-to-Dmz"
set uuid dbdfac02-574d-51ef-984d-ab53ab0d1258
set srcintf "port4"
set dstintf "port2"
set action accept
set srcaddr "Lan_Subnet"
set dstaddr "VIP"
set schedule "always"
set service "ALL"
set nat enable

next

end

 

FortiGate # id=65308 trace_id=6 func=print_pkt_detail line=5895 msg="vd-root:0 received a packet(proto=1, 192.168.1.2:43966->172.16.16.1:2048) tun_id=0.0.0.0 from port3. type=8, code=0, id=43966, seq=1."
id=65308 trace_id=6 func=init_ip_session_common line=6076 msg="allocate a new session-000003a0, tun_id=0.0.0.0"
id=65308 trace_id=6 func=iprope_dnat_check line=5331 msg="in-[port3], out-[]"
id=65308 trace_id=6 func=iprope_dnat_tree_check line=823 msg="len=1"
id=65308 trace_id=6 func=__iprope_check_one_dnat_policy line=5191 msg="checking gnum-100000 policy-1"
id=65308 trace_id=6 func=get_new_addr line=1228 msg="find DNAT: IP-10.10.10.2, port-0(fixed port)"
id=65308 trace_id=6 func=__iprope_check_one_dnat_policy line=5286 msg="matched policy-1, act=accept, vip=1, flag=104, sflag=2000000"
id=65308 trace_id=6 func=iprope_dnat_check line=5343 msg="result: skb_flags-02000000, vid-1, ret-matched, act-accept, flag-00000104"
id=65308 trace_id=6 func=iprope_fwd_check line=789 msg="in-[port3], out-[port4], skb_flags-02000000, vid-1, app_id: 0, url_cat_id: 0"
id=65308 trace_id=6 func=__iprope_check line=2292 msg="gnum-100004, check-ffffffffa002c2a7"
id=65308 trace_id=6 func=__iprope_check_one_policy line=2044 msg="checked gnum-100004 policy-1, ret-matched, act-accept"
id=65308 trace_id=6 func=__iprope_user_identity_check line=1819 msg="ret-matched"
id=65308 trace_id=6 func=get_new_addr line=1228 msg="find SNAT: IP-172.16.16.1(from IPPOOL), port-43966"
id=65308 trace_id=6 func=__iprope_check_one_policy line=2262 msg="policy-1 is matched, act-accept"
id=65308 trace_id=6 func=__iprope_check line=2309 msg="gnum-100004 check result: ret-matched, act-accept, flag-08050100, flag2-00004000"
id=65308 trace_id=6 func=iprope_fwd_check line=826 msg="after iprope_captive_check(): is_captive-0, ret-matched, act-accept, idx-1"
id=65308 trace_id=6 func=iprope_fwd_auth_check line=845 msg="after iprope_captive_check(): is_captive-0, ret-matched, act-accept, idx-1"
id=65308 trace_id=6 func=fw_pre_route_handler line=178 msg="VIP-10.10.10.2:43966, outdev-unknown"
id=65308 trace_id=6 func=__ip_session_run_tuple line=3515 msg="DNAT 172.16.16.1:8->10.10.10.2:43966"
id=65308 trace_id=6 func=vf_ip_route_input_common line=2605 msg="find a route: flag=04000000 gw-10.10.10.2 via port2"
id=65308 trace_id=6 func=iprope_fwd_check line=789 msg="in-[port4], out-[port2], skb_flags-020000c0, vid-1, app_id: 0, url_cat_id: 0"
id=65308 trace_id=6 func=__iprope_check line=2292 msg="gnum-100004, check-ffffffffa002c2a7"
id=65308 trace_id=6 func=__iprope_check_one_policy line=2044 msg="checked gnum-100004 policy-2, ret-matched, act-accept"
id=65308 trace_id=6 func=__iprope_user_identity_check line=1819 msg="ret-matched"
id=65308 trace_id=6 func=get_new_addr line=1228 msg="find SNAT: IP-10.10.10.1(from IPPOOL), port-43966"
id=65308 trace_id=6 func=__iprope_check_one_policy line=2262 msg="policy-2 is matched, act-accept"
id=65308 trace_id=6 func=__iprope_check line=2309 msg="gnum-100004 check result: ret-matched, act-accept, flag-08050500, flag2-00004000"
id=65308 trace_id=6 func=iprope_fwd_check line=826 msg="after iprope_captive_check(): is_captive-0, ret-matched, act-accept, idx-2"
id=65308 trace_id=6 func=iprope_fwd_auth_check line=845 msg="after iprope_captive_check(): is_captive-0, ret-matched, act-accept, idx-2"
id=65308 trace_id=6 func=iprope_reverse_dnat_check line=1307 msg="in-[port4], out-[port2], skb_flags-020000c0, vid-1"
id=65308 trace_id=6 func=iprope_reverse_dnat_tree_check line=915 msg="len=0"
id=65308 trace_id=6 func=fw_forward_handler line=903 msg="Allowed by Policy-2: SNAT"
id=65308 trace_id=6 func=__ip_session_run_tuple line=3502 msg="SNAT 192.168.1.2->10.10.10.1:43966"
id=65308 trace_id=7 func=print_pkt_detail line=5895 msg="vd-root:0 received a packet(proto=1, 10.10.10.2:43966->10.10.10.1:0) tun_id=0.0.0.0 from port2. type=0, code=0, id=43966, seq=1."
id=65308 trace_id=7 func=resolve_ip_tuple_fast line=5983 msg="Find an existing session, id-000003a0, reply direction"

 

During the evaluation process, the first policy performs SNAT, but since the traffic processing is not complete, the action of the second policy (the last one matched) takes precedence.
The traffic will not be SNAT-ed if there's no SNAT in the second policy ID (ID 3 in the example above). If SNAT is required, an IP pool has to be created and 'called' in the second policy that is being evaluated: Policy ID 3:

 

config firewall policy
    edit 3
        set name "VIP"
        set srcintf "wan"
        set dstintf "dmz"
        set srcaddr "all"
        set dstaddr "VIP2"
        set action accept
        set schedule "always"
        set service "ALL"
        set logtraffic all

        set nat enable

        set ippool enable

        set poolname <pool-name>

    next

 

Example 3:

Configuring Hairpin NAT when central NAT is enabled requires creating the corresponding VIP for NAT:

 

config firewall vip
    edit "VIP2"
        set extip 20.0.0.2
        set extintf "any"
        set mappedip "172.16.1.2"
    next
end

 

Port-forwarding can be added in the VIP configuration according to the setup.

 

An address object has to be created for the server in the DMZ - it will be used as a destination in the policy instead of VIP, unlike the setup without central NAT enabled:

 

config firewall address
    edit "server-VIP2"
        set subnet 172.16.1.2 255.255.255.255
    next
end

 

A firewall policy and a corresponding central NAT entry have to be configured:

 

config firewall policy
    edit 1
        set name "VIP"
        set srcintf "lan"
        set dstintf "dmz"
        set srcaddr "all"
        set dstaddr "server-VIP2"
        set action accept
        set schedule "always"
        set service "ALL"
    next
end

 

config firewall central-snat-map
    edit 1
        set srcintf "lan"
        set dstintf "dmz"
        set orig-addr "all"
        set dstaddr "server-VIP2"
    next
end

 

The NAT-ed session can be checked with the following command:

 

get sys session list | grep 20.0.0.2
PROTO   EXPIRE  SOURCE         SOURCE-NAT        DESTINATION  DESTINATION-NAT
icmp    58      192.168.1.2:1  172.16.1.1:60723  20.0.0.2:8   172.16.1.2:1

 

 

Example 4:

If the external address of the VIP is an IP that is on the same subnet as the FortiGate but does not belong directly to the FortiGate, hairpin NAT can be achieved by specifying the WAN-side interface in the VIP's extintf.


Below is an example configuration to achieve hairpin NAT with two policies: a LAN-to-WAN policy and a WAN-to-DMZ policy:

 

diagram.png

Configurations:

 

    config firewall vip
        edit "VIP"
            set extip 20.0.0.100
            set mappedip "172.16.10.100"
            set extintf "port2"
        next
    end

    config firewall policy
        edit 1
            set name "LAN-to-WAN"
            set srcintf "port3"
            set dstintf "port2"
            set action accept
            set srcaddr "all"
            set dstaddr "all"
            set schedule "always"
            set service "ALL"
            set logtraffic all
            set nat enable
        next
        edit 2
            set name "WAN-to-DMZ"
            set srcintf "port2"
            set dstintf "port1"
            set action accept
            set srcaddr "all"
            set dstaddr "VIP"
            set schedule "always"
            set service "ALL"
            set logtraffic all
        next
    end


Flow Trace:

 

id=65308 trace_id=2220 func=print_pkt_detail line=5862 msg="vd-root:0 received a packet(proto=1, 172.16.56.200:1->20.0.0.100:2048) tun_id=0.0.0.0 from port3. type=8, code=0, id=1, seq=698."
id=65308 trace_id=2220 func=init_ip_session_common line=6047 msg="allocate a new session-003015b5"
id=65308 trace_id=2220 func=get_new_addr line=1213 msg="find DNAT: IP-172.16.10.100, port-0(fixed port)"
id=65308 trace_id=2220 func=__iprope_tree_check line=535 msg="gnum-100004, use addr/intf hash, len=2"
id=65308 trace_id=2220 func=get_new_addr line=1213 msg="find SNAT: IP-20.0.0.22(from IPPOOL), port-60418"
id=65308 trace_id=2220 func=fw_pre_route_handler line=184 msg="VIP-172.16.10.100:1, outdev-unknown"
id=65308 trace_id=2220 func=__ip_session_run_tuple line=3455 msg="DNAT 20.0.0.100:8->172.16.10.100:1"
id=65308 trace_id=2220 func=__vf_ip_route_input_rcu line=1990 msg="find a route: flag=00000000 gw-0.0.0.0 via port1"
id=65308 trace_id=2220 func=__iprope_tree_check line=524 msg="gnum-100004, use int hash, slot=51, len=2"
id=65308 trace_id=2220 func=fw_forward_handler line=990 msg="Allowed by Policy-2:"
id=65308 trace_id=2220 func=ip_session_confirm_final line=3110 msg="npu_state=0x100, hook=4"
id=65308 trace_id=2221 func=print_pkt_detail line=5862 msg="vd-root:0 received a packet(proto=1, 172.16.10.100:1->172.16.56.200:0) tun_id=0.0.0.0 from port1. type=0, code=0, id=1, seq=698."
id=65308 trace_id=2221 func=resolve_ip_tuple_fast line=5950 msg="Find an existing session, id-003015b5, reply direction"
id=65308 trace_id=2221 func=__vf_ip_route_input_rcu line=1990 msg="find a route: flag=00000000 gw-0.0.0.0 via port3"
id=65308 trace_id=2221 func=npu_handle_session44 line=1333 msg="Trying to offloading session from port1 to port3, skb.npu_flag=00000000 ses.state=00000204 ses.npu_state=0x00000100"
id=65308 trace_id=2221 func=fw_forward_dirty_handler line=439 msg="state=00000204, state2=00000001, npu_state=00000100"
id=65308 trace_id=2221 func=__ip_session_run_tuple line=3442 msg="SNAT 172.16.10.100->20.0.0.100:1"


get system session list | grep 20.0.0.100

    PROTO  EXPIRE    SOURCE            SOURCE-NAT   DESTINATION    DESTINATION-NAT
    icmp   55        172.16.56.200:1   -            20.0.0.100:8   172.16.10.100:1


Session List:

 

session info: proto=1 proto_state=00 duration=7 expire=52 timeout=0 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 f00
statistic(bytes/packets/allow_err): org=60/1/1 reply=60/1/1 tuples=2
tx speed(Bps/kbps): 0/0 rx speed(Bps/kbps): 0/0
orgin->sink: org pre->post, reply pre->post dev=9->3/3->9 gwy=0.0.0.0/0.0.0.0
hook=pre dir=org act=dnat 172.16.56.200:1->20.0.0.100:8(172.16.10.100:1)
hook=post dir=reply act=snat 172.16.10.100:1->172.16.56.200:0(20.0.0.100:1)
misc=0 policy_id=2 pol_uuid_idx=15754 auth_info=0 chk_client_info=0 vd=0
serial=003015b5 tos=ff/ff app_list=0 app=0 url_cat=0
rpdb_link_id=00000000 ngfwid=n/a
npu_state=0x000100
no_ofld_reason: npu-flag-off


Traffic Log:

 

date=2024-08-30 time=16:24:39 eventtime=1725002679069880080 tz="+0900" logid="0000000013" type="traffic" subtype="forward" level="notice" vd="root" srcip=172.16.56.200 identifier=1 srcintf="port3" srcintfrole="undefined" dstip=20.0.0.100 dstintf="port1" dstintfrole="undefined" srccountry="Reserved" dstcountry="United Kingdom" sessionid=3151285 proto=1 action="accept" policyid=2 policytype="policy" poluuid="b062f842-5ecb-51ef-18a3-3977b8166540" policyname="WAN-to-DMZ" service="PING" trandisp="dnat" tranip=172.16.10.100 tranport=0 duration=61 sentbyte=60 rcvdbyte=60 sentpkt=1 rcvdpkt=1 appcat="unscanned"

 

It can be seen that the traffic goes through two policies but in one session. Hairpin NAT is realized, and traffic passing through the LAN-to-WAN policy is not forwarded from the WAN interface to the Internet, but instead, the process is transferred to the WAN-to-DMZ policy, where it is DestNATed and forwarded directly to the DMZ interface.

 

Related articles:

Technical Tip: Firewall does not block incoming WAN to LAN 

Technical Note: Using an auto hairpin to browse a webpage

Technical Tip: How to disable source NAT to enable a hairpin policy or one-arm firewall