#!/usr/bin/env bash # Runs ON a zone gateway VM (Debian 12, Hetzner) as root. Makes it the zone's NAT router and p2p port forwarder: # gateway.sh [: ...] # is Hetzner's router in the zone subnet (10.20..1), the next hop of the DHCP-pushed default. # - net.ipv4.ip_forward=1 (persisted) # - iptables: MASQUERADE for out of the public interface, FORWARD accepted both ways, TCP MSS clamped # to the path MTU (the private interface is MTU 1450), one DNAT rule per : pair to that node's # p2p port; all in IGNEUM-* chains that this script flushes and refills, so it is idempotent # - /etc/igneum/nat.sh plus igneum-nat.service replay the rules after a reboot # - Hetzner pushes the network's 0.0.0.0/0 route by DHCP (option 121) to every member, the gateway included; a # dhclient exit hook drops that default route on the private interface here, so the gateway keeps its own uplink. set -euo pipefail range="$1"; router="$2"; pubip="$3"; shift 3 P2P_PORT="${P2P_PORT:-26611}" pubif=$(ip -4 route show default | awk '/dev/ { for (i = 1; i <= NF; i++) if ($i == "dev") { print $(i + 1); exit } }' | head -1) [ -n "$pubif" ] || pubif=eth0 privif=$(ip -4 -o addr show | awk -v p="${range%%.*}." '$4 ~ "^" p { print $2; exit }') [ -n "$privif" ] || { echo "no interface in $range yet"; exit 1; } command -v iptables >/dev/null 2>&1 || { export DEBIAN_FRONTEND=noninteractive; apt-get update -qq; apt-get install -y -qq iptables >/dev/null; } mkdir -p /etc/igneum printf 'net.ipv4.ip_forward = 1\n' > /etc/sysctl.d/90-igneum-gateway.conf sysctl -q -p /etc/sysctl.d/90-igneum-gateway.conf { printf '#!/bin/sh\n# generated by infra/cloud-devnet/net/gateway.sh; replayed by igneum-nat.service\nset -e\n' printf 'sysctl -q net.ipv4.ip_forward=1\n' for t in nat:PREROUTING:IGNEUM-DNAT nat:POSTROUTING:IGNEUM-SNAT filter:FORWARD:IGNEUM-FWD mangle:FORWARD:IGNEUM-MSS; do IFS=: read -r table chain mine <<< "$t" printf 'iptables -t %s -N %s 2>/dev/null || true\niptables -t %s -F %s\n' "$table" "$mine" "$table" "$mine" printf 'iptables -t %s -C %s -j %s 2>/dev/null || iptables -t %s -I %s -j %s\n' "$table" "$chain" "$mine" "$table" "$chain" "$mine" done printf 'iptables -t nat -A IGNEUM-SNAT -s %s -o %s -j MASQUERADE\n' "$range" "$pubif" printf 'iptables -A IGNEUM-FWD -i %s -s %s -j ACCEPT\n' "$privif" "$range" printf 'iptables -A IGNEUM-FWD -o %s -d %s -m conntrack --ctstate RELATED,ESTABLISHED,DNAT -j ACCEPT\n' "$privif" "$range" printf 'iptables -t mangle -A IGNEUM-MSS -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu\n' for pair in "$@"; do port="${pair%%:*}"; ip="${pair#*:}" printf 'iptables -t nat -A IGNEUM-DNAT -d %s -p tcp --dport %s -j DNAT --to-destination %s:%s\n' "$pubip" "$port" "$ip" "$P2P_PORT" done # the DHCP-pushed default route on the private interface must not win over the uplink printf 'while ip -4 route show default dev %s | grep -q .; do ip route del default dev %s; done\n' "$privif" "$privif" } > /etc/igneum/nat.sh chmod +x /etc/igneum/nat.sh cat > /etc/systemd/system/igneum-nat.service </dev/null 2>&1 cat > /etc/dhcp/dhclient-exit-hooks.d/igneum-gateway < /etc/ssh/sshd_config.d/igneum-jump.conf systemctl reload ssh 2>/dev/null || systemctl reload sshd 2>/dev/null || true /etc/igneum/nat.sh systemctl start igneum-nat echo "gateway: forward on, $range via $pubif (masquerade), $# port forward(s) on $pubip, private interface $privif (router $router)" iptables -t nat -S IGNEUM-DNAT | grep -c DNAT || true ip -4 route show default