feat: Refactor networking code (#78)

This commit is contained in:
Kroese
2026-07-06 17:48:25 +02:00
committed by GitHub
parent bb809eebd2
commit 7fc2cfb5db
3 changed files with 315 additions and 123 deletions
+1
View File
@@ -36,6 +36,7 @@ RUN <<EOF
gnupg \
procps \
chrony \
ipcalc \
postfix \
ethtool \
dnsmasq \
+44 -6
View File
@@ -13,14 +13,42 @@ info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "${1:-}" "\E[0m\n"; }
error () { printf "%b%s%b" "\E[1;31m " "ERROR: ${1:-}" "\E[0m\n" >&2; }
warn () { printf "%b%s%b" "\E[1;31m " "Warning: ${1:-}" "\E[0m\n" >&2; }
is_enabled() {
case "${1:-}" in
Y|y|YES|yes|TRUE|true|1|ON|on) return 0 ;;
strip() {
local value="${1:-}"
# Remove surrounding whitespace
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
# Remove leading/trailing single/double quotes
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
# Remove surrounding whitespace again
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
enabled() {
case "$(strip "${1:-}")" in
Y|y|YES|Yes|yes|TRUE|True|true|1|ON|On|on) return 0 ;;
*) return 1 ;;
esac
}
disabled() {
case "$(strip "${1:-}")" in
N|n|NO|No|no|FALSE|False|false|0|OFF|Off|off) return 0 ;;
*) return 1 ;;
esac
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
error "Required command not found: $1"
exit 21
@@ -30,6 +58,7 @@ require_cmd() {
}
require_file() {
[ -f "$1" ] || {
error "Required file not found: $1"
exit 22
@@ -39,9 +68,10 @@ require_file() {
}
continue_or_exit() {
local code="$1"
if is_enabled "$DEBUG"; then
if enabled "$DEBUG"; then
warn "DEBUG is enabled (DEBUG=${DEBUG}), continuing despite previous error."
return 0
fi
@@ -50,6 +80,7 @@ continue_or_exit() {
}
check_privileged() {
local cap_bnd
local last_cap
local max_cap
@@ -69,7 +100,8 @@ check_privileged() {
}
check_fuse() {
if ! is_enabled "$REQUIRE_FUSE"; then
if disabled "$REQUIRE_FUSE"; then
return 0
fi
@@ -90,7 +122,7 @@ check_kvm() {
local kvm_err=""
local flags=""
if ! is_enabled "$REQUIRE_KVM"; then
if disabled "$REQUIRE_KVM"; then
return 0
fi
@@ -119,6 +151,7 @@ check_kvm() {
}
check_cgroups() {
if [ ! -d /sys/fs/cgroup ]; then
error "/sys/fs/cgroup is missing. systemd inside the container will not work correctly."
continue_or_exit 25
@@ -136,6 +169,7 @@ check_cgroups() {
}
check_systemd_command() {
if [ "$#" -eq 0 ]; then
error "No command specified. This image should normally be started with systemd as PID 1."
exit 26
@@ -154,6 +188,7 @@ check_systemd_command() {
}
remount_shm() {
if [ ! -d /dev/shm ]; then
warn "/dev/shm does not exist."
return 0
@@ -184,6 +219,7 @@ set_timezone() {
}
check_localtime() {
if [ ! -e /etc/localtime ] && [ ! -L /etc/localtime ]; then
return 1
fi
@@ -200,6 +236,7 @@ check_localtime() {
}
prepare_directory() {
local path="$1"
local owner="$2"
local mode="${3:-}"
@@ -215,6 +252,7 @@ prepare_directory() {
}
cleanup_stale_runtime_files() {
rm -f \
/run/pveproxy/pveproxy.pid \
/run/pvedaemon/pvedaemon.pid \
+270 -117
View File
@@ -5,31 +5,27 @@ set -Eeuo pipefail
: "${DEV:=""}"
: "${MTU:=""}"
: "${MAC:=""}"
: "${TAP:="tap0"}"
: "${NETWORK:="Y"}"
: "${BRIDGE:="vmbr0"}"
: "${MASK:="255.255.255.0"}"
# Sanitize variables
DEV=$(strip "$DEV")
MTU=$(strip "$MTU")
TAP=$(strip "$TAP")
MAC=$(strip "$MAC")
MASK=$(strip "$MASK")
BRIDGE=$(strip "$BRIDGE")
NETWORK=$(strip "$NETWORK")
ADD_ERR="Please add the following setting to your container:"
# ######################################
# Generic helpers
# ######################################
enabled() {
case "$(strip "${1:-}")" in
Y|y|YES|Yes|yes|TRUE|True|true|1|ON|On|on) return 0 ;;
*) return 1 ;;
esac
}
disabled() {
case "$(strip "${1:-}")" in
N|n|NO|No|no|FALSE|False|false|0|OFF|Off|off) return 0 ;;
*) return 1 ;;
esac
}
isNAT() {
case "${NETWORK,,}" in
@@ -40,6 +36,38 @@ isNAT() {
esac
}
maskToCIDR() {
local mask="$1"
local prefix=""
prefix=$(ipcalc -p 0.0.0.0 "$mask" | awk -F= '/^PREFIX=/ { print $2 }')
if [[ ! "$prefix" =~ ^[0-9]+$ ]] || (( prefix < 1 || prefix > 30 )); then
error "Invalid MASK: '$mask'"
return 1
fi
echo "$prefix"
return 0
}
networkCIDR() {
local ip="$1"
local network=""
network=$(ipcalc -n "$ip" "$MASK" | awk -F= '/^NETWORK=/ { print $2 }')
if [ -z "$network" ]; then
error "Failed to calculate network address from IP '$ip' and netmask '$MASK'."
return 1
fi
echo "$network/$PREFIX"
return 0
}
getMTU() {
local dev="$1"
@@ -85,6 +113,83 @@ setMTU() {
return 0
}
gatewayMAC() {
local mac="$1"
echo "$mac" | md5sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/'
}
detectInterface() {
if [ -n "$DEV" ]; then
return 0
fi
# Give Kubernetes priority over the default interface
[ -d "/sys/class/net/net0" ] && DEV="net0"
[ -d "/sys/class/net/net1" ] && DEV="net1"
[ -d "/sys/class/net/net2" ] && DEV="net2"
[ -d "/sys/class/net/net3" ] && DEV="net3"
# Automatically detect the default network interface
[ -z "$DEV" ] && DEV=$(awk '$2 == 00000000 { print $1; exit }' /proc/net/route)
[ -z "$DEV" ] && DEV="eth0"
return 0
}
detectAddresses() {
GATEWAY=$(ip route list dev "$DEV" | awk ' /^default/ {print $3}' | head -n 1)
{ UPLINK=$(ip address show dev "$DEV" | grep inet | awk '/inet / { print $2 }' | cut -f1 -d/ | head -n 1); } 2>/dev/null || :
IP6=""
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]]; then
{ IP6=$(ip -6 addr show dev "$DEV" scope global up); rc=$?; } 2>/dev/null || :
(( rc != 0 )) && IP6=""
[ -n "$IP6" ] && IP6=$(echo "$IP6" | sed -e's/^.*inet6 \([^ ]*\)\/.*$/\1/;t;d' | head -n 1)
fi
return 0
}
detectAdapter() {
local result=""
NIC=""
BUS=""
result=$(ethtool -i "$DEV" 2>/dev/null || :)
NIC=$(grep -m 1 -i 'driver:' <<< "$result" | awk '{print $2}')
BUS=$(grep -m 1 -i 'bus-info:' <<< "$result" | awk '{print $2}')
return 0
}
containerID() {
local id=""
id=$(hostname -s 2>/dev/null || true)
if [ -z "$id" ] && [ -s /etc/machine-id ]; then
id=$(< /etc/machine-id)
fi
if [ -z "$id" ] && [ -s /proc/sys/kernel/random/boot_id ]; then
id=$(< /proc/sys/kernel/random/boot_id)
fi
[ -z "$id" ] && id="unknown"
echo "$id"
return 0
}
disableIPv6() {
local dev="$1"
@@ -98,6 +203,30 @@ disableIPv6() {
return 0
}
subnetBase() {
local ip="$1"
local third=""
local second=""
local base=""
local subnet=""
third=$(cut -d. -f3 <<< "$ip")
for second in {30..254}; do
base="172.$second.$third"
subnet="$base.0/$PREFIX"
if ! ip route show "$subnet" 2>/dev/null | grep -q .; then
echo "$base"
return 0
fi
done
error "No available VM subnet found in 172.30.$third.0/$PREFIX through 172.254.$third.0/$PREFIX."
return 1
}
# ######################################
# DNS / interface helpers
# ######################################
@@ -105,12 +234,9 @@ disableIPv6() {
configureDNS() {
local fa="$1"
local ip="$2"
local mask="$3"
local gateway="$4"
local base="${ip%.*}"
local ip_last="${ip##*.}"
local gw_last="${gateway##*.}"
local mask="$2"
local gateway="$3"
local base="${gateway%.*}"
local file="/etc/dnsmasq.d/$fa.conf"
local mtu_option=""
local filter_dns=""
@@ -124,27 +250,6 @@ configureDNS() {
filter_dns="filter-AAAA"
fi
# Reserve both the bridge gateway address and the translated container address.
# The translated address is intentionally excluded from DHCP so it can be used
# later as a stable container/host identity inside the VM subnet.
# Determine the sorted positions
local low high
if (( ip_last < gw_last )); then
low=$ip_last
high=$gw_last
else
low=$gw_last
high=$ip_last
fi
# Build dhcp-range lines
local ranges=""
(( low > 1 )) && ranges+="dhcp-range=set:${fa},${base}.1,${base}.$((low - 1))"$'\n'
(( high - low > 1 )) && ranges+="dhcp-range=set:${fa},${base}.$((low + 1)),${base}.$((high - 1))"$'\n'
(( high < 254 )) && ranges+="dhcp-range=set:${fa},${base}.$((high + 1)),${base}.254"$'\n'
ranges="${ranges%$'\n'}" # strip trailing newline
if ! sed 's/^ //' > "$file" <<EOF
# Listen only on bridge
@@ -152,8 +257,8 @@ configureDNS() {
bind-interfaces
except-interface=lo
# IPv4 DHCP ranges
$ranges
# IPv4 DHCP range
dhcp-range=set:${fa},${base}.2,${base}.254
# Set gateway address
dhcp-option=option:netmask,$mask
@@ -218,7 +323,7 @@ EOF
auto $fa
iface $fa inet static
address $gateway/24
address $gateway/$PREFIX
bridge-ports $tap
bridge-stp off
bridge-fd 0
@@ -240,7 +345,6 @@ EOF
createBridge() {
local gateway="$1"
local broadcast="$2"
local rc
# Create a bridge with a static IP for the VM LAN
@@ -254,7 +358,7 @@ createBridge() {
setMTU "$BRIDGE" "$LAN_MTU"
fi
if ! ip address add "$gateway/24" broadcast "$broadcast" dev "$BRIDGE"; then
if ! ip address add "$gateway/$PREFIX" dev "$BRIDGE"; then
error "failed to add IP address pool!" && return 1
fi
@@ -320,16 +424,6 @@ configureTables() {
error "$tables" && return 1
fi
if (( KERNEL > 4 )); then
# Hack for guest VMs complaining about "bad udp checksums in 5 packets"
iptables -t mangle -A POSTROUTING \
-s "$subnet" \
-p udp \
--dport bootpc \
-m comment --comment "$rule_tag" \
-j CHECKSUM --checksum-fill > /dev/null 2>&1 || true
fi
# Clamp TCP MSS to avoid subtle MTU blackholes when the outer path has a smaller MTU.
iptables -t mangle -A FORWARD \
-s "$subnet" \
@@ -386,26 +480,18 @@ configureNAT() {
fi
fi
local ip base
base=$(cut -d. -f3,4 <<< "$IP")
local base gateway subnet
if [[ "$IP" != "172.30."* ]]; then
ip="172.30.$base"
else
ip="172.31.$base"
base=$(subnetBase "$UPLINK") || return 1
gateway="$base.1"
subnet=$(networkCIDR "$gateway") || return 1
if ip route show "$subnet" 2>/dev/null | grep -q .; then
error "VM subnet $subnet conflicts with an existing route inside the container."
return 1
fi
local last="${ip##*.}"
if [[ ! "$last" =~ ^[0-9]+$ ]] || (( last < 2 || last > 254 )); then
ip="${ip%.*}.4"
fi
local gateway="${ip%.*}.1"
local subnet="${ip%.*}.0/24"
local broadcast="${ip%.*}.255"
createBridge "$gateway" "$broadcast" || return 1
createBridge "$gateway" || return 1
createTap "$tuntap" || return 1
# Use the lowest effective VM-LAN MTU, without mutating the parent/uplink MTU.
@@ -416,7 +502,7 @@ configureNAT() {
configureTables "$subnet" || return 1
setInterfaces "$BRIDGE" "$TAP" "$gateway" || return 1
configureDNS "$BRIDGE" "$ip" "$MASK" "$gateway" || return 1
configureDNS "$BRIDGE" "$MASK" "$gateway" || return 1
return 0
}
@@ -426,6 +512,7 @@ configureNAT() {
# ######################################
clearTables() {
local table="" line rules
local rule_tag="remove"
local re="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
@@ -478,49 +565,51 @@ closeBridge() {
# Detection
# ######################################
getInfo() {
if [ -z "$DEV" ]; then
# Give Kubernetes priority over the default interface
[ -d "/sys/class/net/net0" ] && DEV="net0"
[ -d "/sys/class/net/net1" ] && DEV="net1"
[ -d "/sys/class/net/net2" ] && DEV="net2"
[ -d "/sys/class/net/net3" ] && DEV="net3"
# Automatically detect the default network interface
[ -z "$DEV" ] && DEV=$(awk '$2 == 00000000 { print $1; exit }' /proc/net/route)
[ -z "$DEV" ] && DEV="eth0"
fi
validateInterface() {
if [ ! -d "/sys/class/net/$DEV" ]; then
error "Network interface '$DEV' does not exist inside the container!"
error "$ADD_ERR -e \"DEV=NAME\" to specify another interface name." && exit 26
error "$ADD_ERR -e \"DEV=NAME\" to specify another interface name."
exit 26
fi
GATEWAY=$(ip route list dev "$DEV" | awk ' /^default/ {print $3}' | head -n 1)
{ IP=$(ip address show dev "$DEV" | grep inet | awk '/inet / { print $2 }' | cut -f1 -d/ | head -n 1); } 2>/dev/null || :
[ -z "$IP" ] && error "Could not determine container IPv4 address!" && exit 26
return 0
}
IP6=""
# shellcheck disable=SC2143
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]] && [ -n "$(ifconfig -a | grep inet6)" ]; then
{ IP6=$(ip -6 addr show dev "$DEV" scope global up); rc=$?; } 2>/dev/null || :
(( rc != 0 )) && IP6=""
[ -n "$IP6" ] && IP6=$(echo "$IP6" | sed -e's/^.*inet6 \([^ ]*\)\/.*$/\1/;t;d' | head -n 1)
validateMask() {
PREFIX=$(maskToCIDR "$MASK") || exit 28
if [[ "$PREFIX" != "24" ]]; then
error "MASK values other than 255.255.255.0 are not supported by this network layout."
exit 28
fi
local nic="" bus="" result=""
result=$(ethtool -i "$DEV" 2>/dev/null || :)
return 0
}
nic=$(grep -m 1 -i 'driver:' <<< "$result" | awk '{print $2}')
bus=$(grep -m 1 -i 'bus-info:' <<< "$result" | awk '{print $2}')
validateAddresses() {
if [[ -n "$bus" && "${bus,,}" != "n/a" && "${bus,,}" != "tap" ]]; then
enabled "$DEBUG" && info "Detected NIC: ${nic:-unknown} BUS: $bus"
[ -z "$UPLINK" ] && error "Could not determine container IPv4 address!" && exit 26
return 0
}
validateAdapter() {
if [[ -n "$BUS" && "${BUS,,}" != "n/a" && "${BUS,,}" != "tap" ]]; then
enabled "$DEBUG" && info "Detected NIC: ${NIC:-unknown} BUS: $BUS"
error "This container does not support host mode networking!"
exit 29
fi
local mac mtu="" mtu_custom="N"
return 0
}
configureMTU() {
local mtu=""
local mtu_custom="N"
if [ -f "/sys/class/net/$DEV/mtu" ]; then
mtu=$(< "/sys/class/net/$DEV/mtu")
@@ -538,22 +627,81 @@ getInfo() {
LAN_MTU="1500"
fi
# Generate MAC address based on Docker container ID in hostname
HOST="$(hostname -s)"
mac=$(echo "$HOST" | md5sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/')
GATEWAY_MAC=$(echo "${mac^^}" | md5sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/')
return 0
}
if enabled "$DEBUG"; then
line="Host: $HOST IP: $IP Gateway: $GATEWAY Interface: $DEV MTU: $mtu"
[[ "$MTU" != "0" && "$MTU" != "$mtu" ]] && line+=" ($MTU)"
info "$line"
if [ -f /etc/resolv.conf ]; then
nameservers=$(grep '^nameserver ' /etc/resolv.conf | sed 's/^nameserver //' | paste -sd ',' | sed 's/,/, /g')
[ -n "$nameservers" ] && info "Nameservers: $nameservers"
fi
echo
configureMAC() {
local container=""
container=$(containerID)
if [ -z "$MAC" ]; then
# Generate a MAC address based on a stable container identifier when possible.
MAC=$(echo "$container" | md5sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/')
fi
MAC="${MAC,,}"
MAC="${MAC//-/:}"
if [[ ${#MAC} == 12 ]]; then
local m="$MAC"
MAC="${m:0:2}:${m:2:2}:${m:4:2}:${m:6:2}:${m:8:2}:${m:10:2}"
fi
if [[ ${#MAC} != 17 ]]; then
error "Invalid MAC address: '$MAC', should be 12 or 17 digits long!"
exit 28
fi
# Keep the guest-facing gateway MAC stable across runs, otherwise Windows guests
# may detect a new network every boot.
GATEWAY_MAC=$(gatewayMAC "$MAC")
return 0
}
printNetworkDebug() {
local line=""
local host=""
local nameservers=""
enabled "$DEBUG" || return 0
host=$(hostname -s 2>/dev/null || true)
[ -z "$host" ] && host="unknown"
line="Host: $host IP: $UPLINK Gateway: $GATEWAY Interface: $DEV MAC: $MAC MTU: $MTU Mask: $MASK/$PREFIX"
info "$line"
if [ -f /etc/resolv.conf ]; then
nameservers=$(grep '^nameserver ' /etc/resolv.conf | sed 's/^nameserver //' | paste -sd ',' | sed 's/,/, /g')
[ -n "$nameservers" ] && info "Nameservers: $nameservers"
fi
echo
return 0
}
prepareNetwork() {
detectInterface
validateInterface
validateMask
detectAddresses
validateAddresses
detectAdapter
validateAdapter
configureMTU
configureMAC
printNetworkDebug
return 0
}
@@ -574,10 +722,15 @@ blockLicense
disabled "$NETWORK" && return 0
if ! isNAT; then
error "Unrecognized NETWORK value: \"$NETWORK\""
exit 48
fi
msg="Initializing network..."
enabled "$DEBUG" && info "$msg"
getInfo
prepareNetwork
closeBridge
# Configure NAT networking