natnetfilternftables

Hairpin NAT with dynamic WAN ip?


I have a fairly simple network, outlined below. For the question in particular, two subnets in their own VLAN.

I have a service running in VLAN1000, lets say it's a webserver. I have forwarded 443/tcp from my WAN interface to this machine, and it works fine from the outside.

From VLAN5, however, it doesn't work properly. It requires either split horizon DNS or hairpin NAT. I'm currently using split horizon DNS, but that only works for machines were I can control DNS, which I can't for some computers. It also doesn't work with some services just using IPs.

I'd like to implement a hairpin NAT rule which works with my dynamic WAN IP. I can't find any relevant documentation on this, but I'm probably missing something obvious.

How do I implement this?

network overview


Solution

  • I solved this with some scripting, accepting the potential for a small time where it might not work in case the WAN IP changes, before the script has had a chance to run again.

    The script is simple:

    #!/usr/bin/env bash
    
    fname=/etc/nftables.d/ip.conf
    test -f $fname || touch $fname
    
    current_ip=$(curl -s https://ifconfig.me)
    last_ip=$(awk '{print $4}' "${fname}")
    
    if [[ $current_ip != $last_ip ]]
    then
        cat <<EOF > "${fname}"
    define wan_ip = ${current_ip}
    EOF
        nft -n -f /etc/nftables.conf && nft -f /etc/nftables.conf && exit 0
        exit 1
    fi
    

    It writes the current IP to a file (/etc/nftables.d/ip.conf), and checks if the IP ifconfig.me returns is different from the current one. If it is, it writes a new ip.conf and reloads the ruleset.

    There's also a possibility of the curl command failing. No error handling.

    Then, for nftables, I now match on IP rather than interface.

    chain prerouting {
        type nat hook prerouting priority 0;
    
        ip daddr $wan_ip dnat ip to tcp dport map {
            80 : $host_http,
            443 : $host_http,
            8883 : $host_mqtt
        } 
    }
    

    I then run this as a systemd timer/service.

    /etc/systemd/system/update-wan-ip.timer

    [Unit]
    Description=Update wan ip
    
    [Timer]
    OnBootSec=3min
    OnUnitActiveSec=7min
    RandomizedDelaySec=31sec
    
    [Install]
    WantedBy=timers.target
    

    /etc/systemd/system/update-wan-ip.service

    [Unit]
    Description=Update and store WAN IP
    Requires=network-online.target
    After=network-online.target
    
    [Service]
    Type=oneshot
    WorkingDirectory=%h
    ExecStart=%h/update-current-ip.sh
    
    [Install]
    WantedBy=default.target