vps·web

Proxmox Port Forwarding

Proxmox Port Forwarding with iptables — DNAT Guide

Server · Proxmox Port Forwarding

Proxmox Port Forwarding with iptables — A Complete DNAT Guide for VMs and CTs

Proxmox port forwarding with iptables lets you expose a service running inside a VM or container to the public internet through a single host IP. The pattern is straightforward — a PREROUTING DNAT rule on vmbr0, a matching FORWARD rule, and a POSTROUTING MASQUERADE for the private subnet — but a missed ip_forward = 1, a forgotten conntrack module, or a hairpin NAT gap will silently break it. This guide walks you through the full configuration, the rules each Proxmox host needs in /etc/iptables/rules.v4, the errors you'll hit when something is off, and how the Proxmox Port Forwarding generator on vps.pyrek.com.pl writes the rules for you in the right order.

The Proxmox Port Forwarding generator — what it does and who it's for

The generator on vps.pyrek.com.pl produces a complete set of iptables rules for forwarding a public port on the Proxmox host to a port on a VM or container that lives on a private bridge such as vmbr1. You fill in the protocol (TCP or UDP), the public IP and port, the local IP and port, the public and local interface names, and the rule order in IPTables. The output is four blocks: a PREROUTING DNAT rule, a FORWARD rule with conntrack matching, a POSTROUTING MASQUERADE rule for hairpin NAT, and the persistence commands (iptables-save plus the iptables-persistent install line). You copy them into your shell or into /etc/network/interfaces post-up hooks — done.

This tool is for the admin running Proxmox VE on a single public IP — a hetzner / OVH / scaleway dedicated box, a home lab on a single uplink, or any environment where you need to fan out one public address to several internal services. It's also useful when you're building a NAT-only Proxmox network behind a private bridge (vmbr1, 192.168.x.0/24 or 10.x.0.0/24) and you need SSH on port 2222 to reach VM A, HTTP on 8080 to reach container B, and a game server on UDP 27015 to reach VM C — all on the same public IP.

What the generator saves is the order. iptables rules are positional, the difference between -A (append) and -I 1 (insert at position 1) matters, and the order of NAT vs FORWARD chain entries determines whether the packet ever reaches the VM or hits a default DROP somewhere along the way. The form encodes that ordering for you.

Hands-on — port forwarding from the Proxmox host to a VM

Here's the end state we're building: a Proxmox VE 8.x host with one public IP on vmbr0 and a private bridge vmbr1 carrying the 192.168.23.0/24 subnet. A single VM at 192.168.23.10 runs an SSH daemon on port 22; we want to expose it to the internet on port 5829 so that ssh -p 5829 user@public-ip lands on the VM. The same recipe extends to web, mail, game, or any other TCP/UDP service.

Step 1 — verify IP forwarding and the bridges

The kernel will not route packets between interfaces unless net.ipv4.ip_forward is set. Check it before you write a single iptables rule:

sysctl net.ipv4.ip_forward

If it returns 0, enable it permanently in /etc/sysctl.conf (or a drop-in under /etc/sysctl.d/):

echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
sysctl -p

Confirm the bridges are up. vmbr0 is your public-facing bridge with the host's public IP and the default gateway. vmbr1 is the internal bridge — no bridge-ports, an RFC 1918 address, and the VM's NIC is attached to it. A minimal /etc/network/interfaces looks like this:

# /etc/network/interfaces

auto vmbr0
iface vmbr0 inet static
    address 51.70.74.52/24
    gateway 51.70.74.1
    bridge-ports eno1
    bridge-stp off
    bridge-fd 0

auto vmbr1
iface vmbr1 inet static
    address 192.168.23.1/24
    bridge-ports none
    bridge-stp off
    bridge-fd 0

The host owns 192.168.23.1 on vmbr1. The VM uses that as its default gateway and an address from the same /24 for itself.

Step 2 — install iptables-persistent

Proxmox ships with iptables but not the persistence layer. Without iptables-persistent, every reboot wipes your rules. Install it now so the rules you're about to write survive:

apt-get update
apt-get install -y iptables-persistent

The installer offers to save the current rules into /etc/iptables/rules.v4 and rules.v6. Accept. The package adds a systemd unit (netfilter-persistent.service) that restores the rules on boot from those two files. That's what every later iptables-save > /etc/iptables/rules.v4 is hooking into.

Step 3 — outbound NAT for the private subnet

Before forwarding inbound traffic, the VMs need outbound internet. That's a MASQUERADE rule that translates the source address of any packet leaving vmbr0 from the 192.168.23.0/24 range to the host's public IP:

iptables -t nat -A POSTROUTING -s '192.168.23.0/24' -o vmbr0 -j MASQUERADE

Now a ping 1.1.1.1 from inside the VM goes through the host, gets its source rewritten, and replies come back. If this rule is missing the VM has no internet at all and nothing else in this guide will work.

Step 4 — DNAT inbound traffic to the VM

This is the core of port forwarding. A packet arrives on the public interface, destined for the host's public IP on TCP port 5829. The DNAT rule rewrites the destination to the VM's private IP and port:

iptables -t nat -I PREROUTING 1 -d 51.70.74.52 -p tcp --dport 5829 \
    -j DNAT --to-destination 192.168.23.10:22

A few directives worth understanding:

  • -t nat — this rule lives in the nat table, where address translation happens. The default table is filter, which is for ACCEPT/DROP decisions, not rewriting.
  • -I PREROUTING 1 — insert at position 1 in the PREROUTING chain. DNAT must happen before the kernel's routing decision, otherwise the packet goes to the host's local stack instead of being forwarded.
  • -d 51.70.74.52 — match only packets destined for the public IP. Without this, any packet hitting the host on TCP 5829 (including from inside) would be NATted, which causes hairpin issues.
  • -p tcp --dport 5829 — protocol and destination port match.
  • -j DNAT --to-destination 192.168.23.10:22 — the action: rewrite destination to the VM. Note the public port (5829) and the local port (22) can differ, which is the whole point — you can map external 5829 to internal 22.

Step 5 — FORWARD rule with conntrack

DNAT rewrites the address but the packet still needs the kernel's permission to leave one interface and enter another. That's the FORWARD chain. On a default-DROP filter setup you need an explicit ACCEPT:

iptables -I FORWARD 1 -i vmbr0 -o vmbr1 -d 192.168.23.10 -p tcp --dport 22 \
    -m conntrack --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT

The conntrack match ensures only packets in valid TCP states get through. NEW lets the SYN through, ESTABLISHED covers ongoing connections, RELATED covers things like ICMP errors associated with the connection. Without conntrack you'd be writing a separate rule per direction; with it, one rule covers both ways of the same flow.

Step 6 — hairpin NAT (optional but commonly needed)

If a host inside 192.168.23.0/24 tries to reach the public IP on port 5829, the packet will hit the DNAT rule, get rewritten to 192.168.23.10:22, and the VM will see the original sender's private IP as the source. The reply goes directly back to the sender — bypassing the host — and the sender's TCP stack drops it because it's coming from the wrong address. That's the classic NAT hairpin / NAT loopback bug.

The fix is a POSTROUTING rule that rewrites the source of internal-to-internal forwarded traffic to the host's vmbr1 address:

iptables -t nat -I POSTROUTING 1 -s '192.168.23.0/24' -d 192.168.23.10/32 \
    -o vmbr1 -j MASQUERADE

Now the VM sees the host's 192.168.23.1 as the source and replies through the host as expected. You only need this if you actually have services on the same private network reaching each other through the public IP — otherwise skip it.

Step 7 — persist the rules

You've built the rules in memory. They're gone on reboot unless you save them:

iptables-save > /etc/iptables/rules.v4

Verify the file looks right:

cat /etc/iptables/rules.v4

You should see your PREROUTING, FORWARD, and POSTROUTING lines plus whatever else was already there. On the next boot, netfilter-persistent restores them.

If you prefer the older approach of putting commands in /etc/network/interfaces as post-up hooks, that works too — but iptables-persistent is cleaner for any rule set larger than a handful of lines. The official iptables documentation on netfilter.org covers the model in depth if you want the upstream view.

Step 8 — test from outside

From a machine that's not on the host's local network, run:

ssh -p 5829 user@51.70.74.52

If you land on the VM's SSH banner — done. If you get Connection refused, the VM's sshd isn't running or isn't bound to all interfaces. If you get Connection timed out, a firewall (the host's filter table, the VM's local firewall, or a datacenter firewall upstream) is blocking the path. The "Common mistakes" section below walks through each.

Common mistakes and pitfalls

net.ipv4.ip_forward = 0

The most common single-line bug. Packets arrive at the public interface, get DNATted, and then sit because the kernel won't move them between interfaces. Symptom: ssh -p 5829 hangs and times out, no entries in iptables -t nat -L -v -n show packet counts incrementing on the FORWARD chain. Fix: sysctl -w net.ipv4.ip_forward=1 and persist it in /etc/sysctl.conf.

Wrong rule order — DNAT after FORWARD DROP

If you have a default FORWARD policy of DROP and the ACCEPT rule for the forwarded traffic comes after a generic DROP, the packet gets dropped before the ACCEPT can match. Run iptables -L FORWARD -v -n --line-numbers and check rule order. Fix: insert the ACCEPT with -I FORWARD 1 so it sits at the top.

Forgot to exclude the Proxmox firewall

Proxmox's own pve-firewall rewrites iptables on top of yours. If pve-firewall status shows enabled and your manual rules disappear after a systemctl restart pve-firewall, that's why. Either disable the Proxmox firewall (systemctl stop pve-firewall && systemctl disable pve-firewall) and rely on raw iptables, or add your DNAT through the Proxmox firewall configuration files — but don't mix the two ad hoc.

iptables: No chain/target/match by that name

Appears when a kernel module isn't loaded — most often nf_nat, nf_conntrack, or xt_conntrack. Run modprobe nf_conntrack nf_nat and check lsmod | grep conntrack. On stripped-down kernels these need explicit loading. Add them to /etc/modules-load.d/iptables.conf to load on boot.

VM has no default gateway pointing at the host

The VM at 192.168.23.10 must use 192.168.23.1 (the Proxmox host's vmbr1 address) as its default gateway. If it's pointing at its own NIC, at 8.8.8.8, or has no gateway at all, replies from the VM will never reach the host and the connection appears to time out from the client side. On Debian-family VMs, edit /etc/netplan/*.yaml or /etc/network/interfaces. Verify with ip route show inside the VM.

MASQUERADE rule on the wrong interface

If you write -o vmbr1 instead of -o vmbr0 for the outbound MASQUERADE, packets going out the public interface keep their private source address and the upstream gateway drops them. Symptom: VMs can't reach the internet, but VM-to-VM and VM-to-host work. Fix: change -o vmbr1 to -o vmbr0 in the POSTROUTING rule.

Datacenter firewall blocks the public port

Hetzner, OVH, and several other providers offer an upstream "Robot" / "vRack" firewall that filters traffic before it ever reaches your host. Your iptables can be perfect and the packet still won't arrive. Symptom: tcpdump -i vmbr0 port 5829 on the host shows zero packets when you connect from outside. Check the provider's web panel for any firewall ruleset on that public IP.

Hairpin NAT not working — internal hosts can't reach public IP

You can SSH from outside but not from another VM on the same vmbr1. The DNAT rewrites the destination correctly but the VM's reply goes directly back to the sender's private IP, bypassing the source NAT. Fix: add the hairpin POSTROUTING MASQUERADE rule for traffic where source and destination are both inside the private subnet, exactly as the Proxmox Port Forwarding generator emits in its HAIPIN block.

FAQ

Can I forward the same public port to two different VMs?

No. A single PREROUTING DNAT rule maps one public IP + port + protocol combination to one private destination. If you need two VMs answering on the same external port, you have two options: give them distinct public IPs and forward each one independently, or put a reverse proxy (nginx, HAProxy, Traefik) in front and let it route by hostname or path.

How do I forward a UDP port instead of TCP?

Change -p tcp to -p udp in both the PREROUTING DNAT rule and the FORWARD ACCEPT rule. UDP has no SYN/ACK so the conntrack match still works the same way — NEW for the first packet of a "flow", ESTABLISHED for subsequent ones. Common UDP forwards include WireGuard (51820), DNS (53), and game servers.

How do I forward a port range, like 30000-30010?

Use --dport 30000:30010 in the rule (note the colon, not a hyphen). The DNAT target accepts a port range too: --to-destination 192.168.23.10:30000-30010. If you want to remap to a different range, both ports must specify the same number of ports — kernel matches the offset.

Does port forwarding work with Proxmox containers (LXC) or just VMs?

Both. From the iptables rule's perspective, a container is just another network endpoint on a Proxmox bridge. As long as the LXC has an IP on vmbr1 and a default gateway pointing at the host, the same rules apply. The only difference is that LXC containers can be configured to pass through host network interfaces directly, which would skip NAT entirely — that's a different setup.

What's the difference between iptables -A and iptables -I 1?

-A appends to the end of the chain; -I 1 inserts at position 1 (the top). Order matters because rules are evaluated top-to-bottom and the first match wins. For DNAT and MASQUERADE rules, the order rarely matters within their own chain (they're usually the only rules there), but for FORWARD chain rules with a default DROP policy, an -A ACCEPT after a DROP is dead code. When in doubt, use -I 1 to be sure your rule is the first thing checked.

How do I remove a port forwarding rule I no longer need?

Use the same command but with -D instead of -I or -A, matching the rule exactly. For example:

iptables -t nat -D PREROUTING -d 51.70.74.52 -p tcp --dport 5829 \
    -j DNAT --to-destination 192.168.23.10:22

Or delete by line number after iptables -t nat -L PREROUTING --line-numbers:

iptables -t nat -D PREROUTING 1

Then iptables-save > /etc/iptables/rules.v4 to persist the change.

Why does my forwarded connection work briefly and then drop?

Conntrack table full or session timing out. Check /proc/sys/net/netfilter/nf_conntrack_count against nf_conntrack_max. If they're close, raise the max (sysctl -w net.netfilter.nf_conntrack_max=262144). For long-lived idle connections (like database pools), the TCP keepalive defaults are a frequent culprit — the conntrack entry expires before the next packet, and the session is silently invalidated.

Should I use the Proxmox firewall or raw iptables?

Either works, but don't mix them on the same host. The Proxmox firewall (pve-firewall) is convenient for per-VM rules in the GUI but has edge cases with NAT — DNAT rules need to live in /etc/pve/firewall/cluster.fw or directly in iptables, not split across both. For a NAT gateway scenario like this guide, raw iptables in /etc/iptables/rules.v4 is simpler and survives Proxmox upgrades cleanly.

Will my iptables rules survive a Proxmox upgrade?

Yes, if they're in /etc/iptables/rules.v4 and you have iptables-persistent installed. Proxmox upgrades touch the cluster config and the kernel, but they don't replace the netfilter-persistent state. Verify after the upgrade with iptables -t nat -L -v -n — if the rules are gone, run netfilter-persistent reload or check that the systemd unit is enabled.

How do I see live traffic hitting my forwarding rule?

Increment counters and tcpdump:

iptables -t nat -L PREROUTING -v -n
tcpdump -i vmbr0 -n port 5829
tcpdump -i vmbr1 -n host 192.168.23.10 and port 22

The first command shows packet counts on each NAT rule — incrementing means the rule is matching. The two tcpdump invocations let you see the packet on the public side and on the private side; if it appears on vmbr0 but not vmbr1, your DNAT or FORWARD rule is broken.

Next steps

Generate the full rule set for your specific public IP, ports, and VM addresses with the Proxmox Port Forwarding generator on vps.pyrek.com.pl — fill in the form and paste the output into your shell or /etc/iptables/rules.v4.

Related topics that build out a production Proxmox setup:

If you prefer video, check out YouTube channel — practical Linux admin and Proxmox tutorials with live demos on real hardware.