You are not logged in.

#1 2024-02-10 02:35:26

Registered: 2015-05-15
Posts: 113

nftables rules for UDP hole punching for OpenVPN

Was playing with nftables. This nftables config is mostly comments. Don't work with symmetric NAT
OpenVPN seems to be working fine. Didn't test more.

Maybe there is a more easy way?

# PC1 (OpenVPN client) -> NAT1 -> WWW <- NAT2 <- PC2 (OpenVPN server)

# OpenVPN server (PC2) don't have an option to send initial packet to some client (PC1)
# All of the below only needed to send UDP packet (bootstrap) by the server (PC2)
# to client (PC1 represented by external IP of NAT1)
# Which allow to achieve UDP holepunching

# Bootstraping packed can be called on the server PC (PC2) with use of openbsd-netcat
# echo '+' | nc -q1 -u -p 4008 ***PC1-external-IP*** 4007
# Use 'nobody' user to run this command
# Also it can be executed by some systemd timer which 'pings' client PC (PC1) every few minutes

# This nftables script is for the computer (PC2) behind NAT (NAT2)
# which is acts as OpenVPN server
# Computer (PC1) with OpenVPN client doesn't need this
# because it initiates connection to the server and is sending UDP a lot

# Replace ***PC1-external-IP*** by IPv4 of NAT1 outbound interface (e.g.
# Replace ***PC2-internal-IP*** by IPv4 of PC2 outbound interface (inside NAT, e.g.

# Port 1195 is OpenVPN server listen port on the PC2
# OpenVPN client on the PC1 MUST have 'lport 4007' and 'rport 4008' in its configuration
# (or another chosen port with appropriate changes in this configuration)
# 4007 is client port
# 4008 is server port
# better to change both of them, they can be the same

# This script is an example and should not be used without more rules that keeps PC2 safe

# Test it with command
# nft flush ruleset; nft -f this_file_path
table ip raw
delete table ip raw
table ip raw {

        # This 'set' is used only to keep bootstrap packed from being tracked by 'connection tracking'
        # When it's empty that do 'notrack' in 'OUTPUT' chain
        set tracking {
                typeof ip saddr . udp dport . udp sport
                timeout 120s

        chain PREROUTING {
                type filter hook prerouting priority raw; policy accept;
                # If there is an incoming packet from client PC1 then NAT2 was penetrated and we can start/continue to redirect traffic to OpenVPN server
                ip saddr . udp sport . udp dport == { ***PC1-external-IP*** . 4007 . 4008 } update @tracking  { ip saddr . udp sport . udp dport } counter

        chain OUTPUT {
                type filter hook output priority raw; policy accept;
                # bootstraping packet that create UDP hole should not be tracked by 'connection tracking'
                # bacause once first packed in the flow gets tracked, all further packets skip 'nat' table
                ip daddr . udp dport . udp sport == { ***PC1-external-IP*** . 4007 . 4008 } ip daddr . udp dport . udp sport != @tracking counter notrack accept

table ip nat
delete table ip nat
table ip nat {
        chain PREROUTING {
                type nat hook prerouting priority dstnat; policy accept;
                # 'redirect' not working for some reason, so we have to use 'dnat'
                # this table is only evaluated for the first packet in the flow
                # so 'masquerade' in 'POSTROUTING' chain is not needed,
                # once 'dnat' evaluated, then all next packets are processed by the 'connection tracking'
                ip saddr . udp sport . udp dport == { ***PC1-external-IP*** . 4007 . 4008 } log prefix "[nft.rdr]: " counter dnat to ***PC2-internal-IP***:1195


Board footer

Powered by FluxBB