fix: Improve NAT port forwarding (#1260)

This commit is contained in:
Kroese
2026-07-15 12:24:25 +02:00
committed by GitHub
parent a32d215e90
commit 0cd08f2ee1
+253 -114
View File
@@ -126,6 +126,11 @@ maskToCIDR() {
local mask="$1"
local prefix=""
if ! command -v ipcalc > /dev/null 2>&1; then
error "Required command 'ipcalc' is not installed!"
return 1
fi
prefix=$(ipcalc -n -b "0.0.0.0/$mask" 2>/dev/null | awk '
/^Netmask:/ {
for (i = 1; i <= NF; i++) {
@@ -137,8 +142,8 @@ maskToCIDR() {
}
')
if [[ ! "$prefix" =~ ^[0-9]+$ ]] || (( prefix < 1 || prefix > 30 )); then
error "Invalid MASK: '$mask'"
if [[ ! "$prefix" =~ ^[0-9]+$ ]] || (( prefix < 1 || prefix > 24 )); then
error "Invalid MASK: '$mask' (supported range: /1 through /24)"
return 1
fi
@@ -365,9 +370,6 @@ configureDNS() {
arguments+=" --interface=$fa"
arguments+=" --bind-interfaces"
# Set pid file
arguments+=" --pid-file=$DNSMASQ_PID"
# Workaround NET_RAW capability
arguments+=" --no-ping"
@@ -382,6 +384,9 @@ configureDNS() {
# Set local dns resolver to dnsmasq when needed
[ -f /etc/resolv.dnsmasq ] && arguments+=" --resolv-file=/etc/resolv.dnsmasq"
# Set pid file
arguments+=" --pid-file=$DNSMASQ_PID"
# Enable logging to file
local log="/var/log/dnsmasq.log"
rm -f "$log"
@@ -413,37 +418,38 @@ configureDNS() {
getHostPorts() {
local ports=""
local port=""
local num=""
local proto=""
local port=""
local ports=""
local mode="${1:-tcp}"
local list="${HOST_PORTS// /},"
for port in ${list//,/ }; do
proto="tcp"
num="$port"
if [[ "$port" == *"/udp" ]]; then
proto="udp"
num="${port%/udp}"
elif [[ "$port" == *"/tcp" ]]; then
proto="tcp"
num="${port%/tcp}"
fi
[ -z "$num" ] && continue
[ -z "$port" ] && continue
case "$mode" in
"all" )
ports+="$num/$proto," ;;
"tcp" )
[[ "$proto" == "tcp" ]] && ports+="$num," ;;
"udp" )
[[ "$proto" == "udp" ]] && ports+="$num," ;;
[[ "$port" == *"/udp" ]] && continue
num="${port%/tcp}"
;;
"all" )
if [[ "$port" == *"/udp" ]]; then
num="${port%/udp}"
[ -n "$num" ] && ports+="$num/udp,"
else
num="${port%/tcp}"
[ -n "$num" ] && ports+="$num/tcp,"
fi
continue
;;
*)
return 1
;;
esac
[ -n "$num" ] && ports+="$num,"
done
# Remove duplicates
@@ -455,21 +461,18 @@ getHostPorts() {
getUserPorts() {
local ssh="22/tcp"
local dsm="5000/tcp,5001/tcp"
local defaults="22/tcp,5000/tcp,5001/tcp"
local list="$defaults,${USER_PORTS// /},"
local list="$ssh,$dsm,"
list+="${USER_PORTS// /},"
local num=""
local ports=""
local proto=""
local userport=""
local hostport=""
local exclude=""
exclude=$(getHostPorts "all")
local ports=""
local userport=""
local hostport=""
local proto=""
local num=""
for userport in ${list//,/ }; do
proto="tcp"
@@ -488,6 +491,7 @@ getUserPorts() {
for hostport in ${exclude//,/ }; do
if [[ "$num/$proto" == "$hostport" ]]; then
num=""
if [[ "$hostport" != "${WEB_PORT:-}/tcp" ]]; then
@@ -505,6 +509,7 @@ getUserPorts() {
# Remove duplicates
echo "${ports//,,/,}," | awk 'BEGIN{RS=ORS=","} !seen[$0]++' | sed 's/,*$//g'
return 0
}
@@ -808,14 +813,16 @@ configurePasst() {
createBridge() {
local gateway="$1"
local rc
local rc msg=""
# Create a bridge with a static IP for the VM guest
{ ip link add dev "$BRIDGE" type bridge; rc=$?; } || :
{ msg=$(ip link add dev "$BRIDGE" type bridge 2>&1); rc=$?; } || :
if (( rc != 0 )); then
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
warn "failed to create bridge. $ADD_ERR --cap-add NET_ADMIN" && return 1
[ -n "$msg" ] && echo "$msg" >&2
warn "failed to create bridge. $ADD_ERR --cap-add NET_ADMIN"
return 1
fi
if [[ "$GUEST_MTU" != "0" ]]; then
@@ -840,11 +847,16 @@ createBridge() {
createTap() {
local tuntap="$1"
local rc msg=""
# Set tap to the bridge created
if ! ip tuntap add dev "$TAP" mode tap; then
{ msg=$(ip tuntap add dev "$TAP" mode tap 2>&1); rc=$?; } || :
if (( rc != 0 )); then
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
warn "$tuntap" && return 1
[ -n "$msg" ] && echo "$msg" >&2
warn "$tuntap"
return 1
fi
if [[ "$GUEST_MTU" != "0" ]]; then
@@ -870,11 +882,79 @@ createTap() {
return 0
}
checkExistingTables() {
local rules=""
local conflicts=""
rules=$(iptables -t nat -S PREROUTING 2>/dev/null || true)
conflicts=$(grep -E -- \
'^-A PREROUTING .*(-j DNAT|-j REDIRECT)( |$)' \
<<< "$rules" || true)
if [ -n "$conflicts" ]; then
local msg="existing NAT rules may take precedence over VM port forwarding"
if enabled "$DEBUG"; then
warn "${msg}."
else
warn "${msg}; enable DEBUG=Y to inspect them."
fi
fi
if enabled "$DEBUG" && [ -n "$rules" ]; then
printf "Existing NAT PREROUTING rules:\n\n%s\n\n" "$rules"
fi
rules=$(iptables -t filter -S FORWARD 2>/dev/null || true)
conflicts=$(grep -E -- \
'^-A FORWARD .*(-j DROP|-j REJECT)( |$)' \
<<< "$rules" || true)
if [ -n "$conflicts" ]; then
local msg="existing firewall rules may block traffic forwarded to or from the VM"
if enabled "$DEBUG"; then
warn "${msg}."
else
warn "${msg}; enable DEBUG=Y to inspect them."
fi
fi
if enabled "$DEBUG" && [ -n "$rules" ]; then
printf "Existing filter FORWARD rules:\n\n%s\n\n" "$rules"
fi
if enabled "$DEBUG"; then
rules=$(iptables -t nat -S POSTROUTING 2>/dev/null || true)
if [ -n "$rules" ]; then
printf "Existing NAT POSTROUTING rules:\n\n%s\n\n" "$rules"
fi
rules=$(iptables -t mangle -S FORWARD 2>/dev/null || true)
if [ -n "$rules" ]; then
printf "Existing mangle FORWARD rules:\n\n%s\n\n" "$rules"
fi
rules=$(iptables -t mangle -S POSTROUTING 2>/dev/null || true)
if [ -n "$rules" ]; then
printf "Existing mangle POSTROUTING rules:\n\n%s\n\n" "$rules"
fi
fi
return 0
}
configureTables() {
local ip="$1"
local subnet="$2"
local exclude="$3"
local exclude=""
local port=""
local dnat_chain="QEMU_DNAT"
local rule_tag="remove"
local tables_err="failed to configure IP tables!"
local tables="the 'ip_tables' kernel module is not loaded. Try this command: sudo modprobe ip_tables iptable_nat"
@@ -885,53 +965,71 @@ configureTables() {
return 1
fi
if [ -n "$exclude" ]; then
if [[ "$exclude" != *","* ]]; then
exclude=" ! --dport $exclude"
else
exclude=" -m multiport ! --dports $exclude"
fi
fi
checkExistingTables
exclude=$(getHostPorts)
# NAT traffic from bridge subnet to Docker uplink
# NAT traffic from the VM subnet leaving through any external interface.
if ! iptables -t nat -A POSTROUTING \
-o "$DEV" \
! -o "$BRIDGE" \
-s "$subnet" \
! -d "$subnet" \
-m comment --comment "$rule_tag" \
-j MASQUERADE > /dev/null 2>&1; then
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
if ! iptables -t nat -A POSTROUTING \
-o "$DEV" \
! -o "$BRIDGE" \
-s "$subnet" \
! -d "$subnet" \
-m comment --comment "$rule_tag" \
-j MASQUERADE; then
warn "$tables" && return 1
warn "$tables"
return 1
fi
fi
# shellcheck disable=SC2086
if ! iptables -t nat -A PREROUTING \
-i "$DEV" \
-d "$UPLINK" \
-p tcp${exclude} \
-m comment --comment "$rule_tag" \
-j DNAT --to "$ip"; then
warn "$tables_err" && return 1
# Use a dedicated chain so protected TCP ports do not depend on multiport support.
if ! iptables -t nat -N "$dnat_chain"; then
warn "$tables_err"
return 1
fi
if ! iptables -t nat -A PREROUTING \
-i "$DEV" \
-d "$UPLINK" \
-p udp \
# Keep container-owned TCP ports handled by the container.
for port in ${exclude//,/ }; do
[ -z "$port" ] && continue
if ! iptables -t nat -A "$dnat_chain" \
-p tcp \
--dport "$port" \
-m comment --comment "$rule_tag" \
-j RETURN; then
warn "$tables_err"
return 1
fi
done
# Forward every remaining protocol and port to the VM.
if ! iptables -t nat -A "$dnat_chain" \
-m comment --comment "$rule_tag" \
-j DNAT --to "$ip"; then
warn "$tables_err" && return 1
warn "$tables_err"
return 1
fi
# Process incoming traffic addressed to the container through the VM chain.
if ! iptables -t nat -A PREROUTING \
! -i "$BRIDGE" \
-m addrtype --dst-type LOCAL \
-m comment --comment "$rule_tag" \
-j "$dnat_chain"; then
warn "$tables_err"
return 1
fi
if (( KERNEL > 4 )); then
# Hack for guest VMs complaining about "bad udp checksums in 5 packets"
# Hack for guest VMs complaining about "bad udp checksums in 5 packets".
iptables -t mangle -A POSTROUTING \
-s "$subnet" \
-p udp \
@@ -955,22 +1053,26 @@ configureTables() {
-m comment --comment "$rule_tag" \
-j TCPMSS --clamp-mss-to-pmtu > /dev/null 2>&1 || true
# Allow forwarding from bridge -> dev
# Allow forwarding from the VM bridge to external interfaces.
if ! iptables -A FORWARD \
-i "$BRIDGE" \
-o "$DEV" \
! -o "$BRIDGE" \
-s "$subnet" \
-m comment --comment "$rule_tag" \
-j ACCEPT; then
warn "$tables_err" && return 1
warn "$tables_err"
return 1
fi
# Allow forwarding from dev -> guest
# Allow forwarding from external interfaces to the VM.
if ! iptables -A FORWARD \
-i "$DEV" \
! -i "$BRIDGE" \
-o "$BRIDGE" \
-d "$ip" \
-m comment --comment "$rule_tag" \
-j ACCEPT; then
warn "$tables_err" && return 1
warn "$tables_err"
return 1
fi
return 0
@@ -979,15 +1081,21 @@ configureTables() {
configureNAT() {
local tuntap="TUN device is missing. $ADD_ERR --device /dev/net/tun"
local rc
local msg=""
local rc ip subnet forwarding=""
enabled "$DEBUG" && echo "Configuring NAT networking..."
# Create the necessary file structure for /dev/net/tun
if [ ! -c /dev/net/tun ]; then
[ ! -d /dev/net ] && mkdir -m 755 /dev/net
if mknod /dev/net/tun c 10 200; then
[ ! -d /dev/net ] && mkdir -m 755 /dev/net > /dev/null 2>&1 || :
{ msg=$(mknod /dev/net/tun c 10 200 2>&1); rc=$?; } || :
if (( rc == 0 )); then
chmod 666 /dev/net/tun
elif ! enabled "$ROOTLESS" || enabled "$DEBUG"; then
[ -n "$msg" ] && echo "$msg" >&2
fi
fi
@@ -997,7 +1105,10 @@ configureNAT() {
fi
# Check port forwarding flag
if [[ $(< /proc/sys/net/ipv4/ip_forward) -eq 0 ]]; then
[ -r /proc/sys/net/ipv4/ip_forward ] &&
forwarding=$(< /proc/sys/net/ipv4/ip_forward)
if [[ "$forwarding" != "1" ]]; then
{ sysctl -w net.ipv4.ip_forward=1 > /dev/null 2>&1; rc=$?; } || :
if (( rc != 0 )) || [[ $(< /proc/sys/net/ipv4/ip_forward) -eq 0 ]]; then
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
@@ -1006,8 +1117,6 @@ configureNAT() {
fi
fi
local ip exclude subnet
if [ -n "$IP" ]; then
ip=$(guestIP "$IP" 2)
else
@@ -1030,8 +1139,7 @@ configureNAT() {
GUEST_MTU=$(minMTU "$GUEST_MTU" "$(getMTU "$BRIDGE")" "$(getMTU "$TAP")")
fi
exclude=$(getHostPorts)
configureTables "$ip" "$subnet" "$exclude" || return 1
configureTables "$ip" "$subnet" || return 1
NET_OPTS="-netdev tap,id=hostnet0,ifname=$TAP"
@@ -1065,9 +1173,13 @@ setTables() {
testTables() {
# Test actual ruleset access instead of only checking the binary version.
iptables -w -t nat -S > /dev/null 2>&1 || return 1
iptables-save -t nat > /dev/null 2>&1 || return 1
local table=""
# Test every table required by the networking rules.
for table in nat filter; do
iptables -t "$table" -S > /dev/null 2>&1 || return 1
iptables-save -t "$table" > /dev/null 2>&1 || return 1
done
return 0
}
@@ -1075,13 +1187,24 @@ testTables() {
selectTables() {
local mode=""
local current=""
local modes=()
# Prefer nftables for Podman namespaces, but retain legacy first for Docker.
if [[ "${ENGINE,,}" == "podman" ]]; then
modes=( "nft" "legacy" )
else
# Keep the currently selected backend when it is fully functional.
if testTables; then
return 0
fi
current=$(iptables --version 2>/dev/null || true)
if [[ "$current" == *"nf_tables"* ]]; then
modes=( "legacy" )
elif [[ "$current" == *"legacy"* ]]; then
modes=( "nft" )
elif [[ "${ENGINE,,}" == "docker" ]]; then
modes=( "legacy" "nft" )
else
modes=( "nft" "legacy" )
fi
for mode in "${modes[@]}"; do
@@ -1100,6 +1223,7 @@ clearTables() {
local line=""
local rules=""
local failed="N"
local dnat_chain="QEMU_DNAT"
local rule_tag="remove"
local re="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
@@ -1107,30 +1231,46 @@ clearTables() {
# Store the current iptables ruleset.
! rules=$(iptables-save 2> /dev/null) && return 1
[ -z "$rules" ] && return 0
# Delete every rule tagged with our unique identifier,
# leaving all other rules intact.
while IFS= read -r line; do
if [ -n "$rules" ]; then
case "$line" in
\*nat ) table="nat" ;;
\*filter ) table="filter" ;;
\*mangle ) table="mangle" ;;
\*raw ) table="raw" ;;
esac
# Delete every rule tagged with our unique identifier,
# leaving all other rules intact.
while IFS= read -r line; do
if [[ "$line" == -A* ]] && [[ "$line" =~ $re ]]; then
line="${line/-A /-D }"
case "$line" in
\*nat ) table="nat" ;;
\*filter ) table="filter" ;;
\*mangle ) table="mangle" ;;
\*raw ) table="raw" ;;
esac
# Parse the quoting produced by iptables-save before deleting the rule.
if ! printf '%s\n' "$line" |
xargs -r iptables -t "$table" > /dev/null 2>&1; then
failed="Y"
if [[ "$line" == -A* ]] && [[ "$line" =~ $re ]]; then
line="${line/-A /-D }"
# Parse the quoting produced by iptables-save before deleting the rule.
if ! printf '%s\n' "$line" |
xargs -r iptables -t "$table" > /dev/null 2>&1; then
failed="Y"
fi
fi
done <<< "$rules"
fi
# Remove the dedicated DNAT chain after deleting its rules and references.
if iptables -t nat -S "$dnat_chain" > /dev/null 2>&1; then
if ! iptables -t nat -F "$dnat_chain" > /dev/null 2>&1; then
failed="Y"
fi
done <<< "$rules"
if ! iptables -t nat -X "$dnat_chain" > /dev/null 2>&1; then
failed="Y"
fi
fi
enabled "$failed" && return 1
return 0
@@ -1140,11 +1280,14 @@ clearTables() {
# Cleanup
# ######################################
closeBridge() {
closeInterfaces() {
local pids=( "$PASST_PID" "$DNSMASQ_PID" )
mKill "${pids[@]}"
exec 30>&- 2>/dev/null || true
exec 40>&- 2>/dev/null || true
ip link set "$TAP" down promisc off &> /dev/null || :
ip link delete "$TAP" &> /dev/null || :
@@ -1157,7 +1300,7 @@ closeBridge() {
closeWeb() {
local pids=( "$WEB_PID" "$WSD_PID" )
local pids=( "${WEB_PID:-}" "${WSD_PID:-}" )
mKill "${pids[@]}"
return 0
@@ -1171,17 +1314,14 @@ closeNetwork() {
disabled "$NETWORK" && return 0
exec 30>&- 2>/dev/null || true
exec 40>&- 2>/dev/null || true
closeBridge
closeInterfaces
return 0
}
cleanUp() {
closeBridge
closeInterfaces
# Clean up old files
rm -f "$PASST_PID" "$PASST_SOCKET"
@@ -1383,8 +1523,7 @@ configureMAC() {
exit 28
fi
# Keep the guest-facing gateway MAC stable across runs, otherwise Windows guests
# may detect a new network every boot.
# Keep the guest-facing gateway MAC stable across runs.
GATEWAY_MAC=$(gatewayMAC "$MAC")
return 0
@@ -1578,7 +1717,7 @@ else
# Configure tap interface
if ! configureNAT; then
closeBridge
closeInterfaces
NETWORK="user"
if ! enabled "$ROOTLESS" || enabled "$DEBUG"; then