Files
proxmox/src/network.sh
T

1376 lines
29 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -Eeuo pipefail
# Docker environment variables
: "${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
# ######################################
isNAT() {
case "${NETWORK,,}" in
"nat" | "tap" | "tun" | "tuntap" | "y" | "" )
return 0 ;;
*)
return 1 ;;
esac
}
getMTU() {
local dev="$1"
if [ -r "/sys/class/net/$dev/mtu" ]; then
cat "/sys/class/net/$dev/mtu"
else
echo "0"
fi
return 0
}
minMTU() {
local mtu min=""
for mtu in "$@"; do
[[ -z "$mtu" || "$mtu" == "0" ]] && continue
if [[ -z "$min" || "$mtu" -lt "$min" ]]; then
min="$mtu"
fi
done
echo "${min:-0}"
return 0
}
setMTU() {
local dev="$1"
local mtu="$2"
# MTU 0 means "do not set"; MTU 1500 is the normal default and does not need setting.
[[ "$mtu" == "0" || "$mtu" == "1500" ]] && return 0
if ! ip link set dev "$dev" mtu "$mtu"; then
warn "failed to set MTU size of $dev to $mtu."
fi
return 0
}
gatewayMAC() {
local mac="$1"
echo "$mac" | md5sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/'
}
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++) {
if ($i == "=") {
print $(i + 1)
exit
}
}
}
')
if [[ ! "$prefix" =~ ^[0-9]+$ ]] || (( prefix < 0 || prefix > 32 )); then
error "Invalid MASK: '$mask'"
return 1
fi
echo "$prefix"
return 0
}
networkCIDR() {
local ip="$1"
local network
network=$(ipcalc -n -b "$ip/$MASK" 2>/dev/null | awk '
/^Network:/ {
print $2
exit
}
')
if [[ ! "$network" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]+$ ]]; then
error "Failed to calculate network address from IP '$ip' and netmask '$MASK'."
return 1
fi
echo "$network"
return 0
}
detectInterface() {
if [ -n "$DEV" ]; then
return 0
fi
# Prefer the last attached Kubernetes network
[ -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
}
defaultGateway() {
ip -4 route list default dev "$1" 2>/dev/null |
awk '$1 == "default" { for (i = 1; i < NF; i++) if ($i == "via") { print $(i + 1); exit } }' || :
return 0
}
addUpstream() {
local upstream="$1"
[ -n "$upstream" ] || return 1
if ! ip address add "$upstream/32" dev "$BRIDGE"; then
warn "failed to add upstream IP alias."
return 1
fi
return 0
}
detectAddresses() {
GATEWAY=$(defaultGateway "$DEV")
{ 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); local 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=$(awk -F':[[:space:]]*' '
tolower($1) == "driver" {
print $2
exit
}
' <<< "$result")
BUS=$(awk -F':[[:space:]]*' '
tolower($1) == "bus-info" {
print $2
exit
}
' <<< "$result")
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" ] && [ -r /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"
[ -d "/proc/sys/net/ipv6/conf/$dev" ] || return 0
# Best-effort only: container sysctl writes can fail.
sysctl -w "net.ipv6.conf.$dev.disable_ipv6=1" > /dev/null 2>&1 || :
sysctl -w "net.ipv6.conf.$dev.accept_ra=0" > /dev/null 2>&1 || :
return 0
}
subnetInUse() {
local subnet="$1"
local broader narrower routes
if ! broader=$(ip -4 route show table all match "$subnet" 2>/dev/null); then
error "Failed to inspect existing routes for subnet $subnet."
return 2
fi
if ! narrower=$(ip -4 route show table all root "$subnet" 2>/dev/null); then
error "Failed to inspect existing routes for subnet $subnet."
return 2
fi
routes=$(
printf '%s\n%s\n' "$broader" "$narrower" |
grep -Ev '(^|[[:space:]])default([[:space:]]|$)' |
sort -u || true
)
[ -n "$routes" ]
}
subnetBase() {
local ip="$1"
local third second
third=$(cut -d. -f3 <<< "$ip")
for second in {30..254}; do
local base="172.$second.$third"
local subnet="$base.0/$PREFIX"
if subnetInUse "$subnet"; then
continue
else
local rc=$?
(( rc == 1 )) || return 1
fi
echo "$base"
return 0
done
error "No available VM subnet found in 172.30.$third.0/$PREFIX through 172.254.$third.0/$PREFIX."
return 1
}
# ######################################
# DNS / interface helpers
# ######################################
configureDNS() {
local fa="$1"
local mask="$2"
local gateway="$3"
local upstream="${4:-}"
local base="${gateway%.*}"
local file="/etc/dnsmasq.d/$fa.conf"
local mtu_option=""
local filter_dns=""
local upstream_entry=""
if [[ "$LAN_MTU" != "0" && "$LAN_MTU" != "1500" ]]; then
mtu_option="dhcp-option=option:mtu,$LAN_MTU"
fi
[ -n "$upstream" ] && upstream_entry="address=/system.lan/$upstream"
# Avoid returning IPv6 records when the active network mode is IPv4-only.
if isNAT || [ -z "$IP6" ]; then
filter_dns="filter-AAAA"
fi
if ! sed 's/^ //' > "$file" <<EOF2
# Listen only on bridge
interface=$fa
bind-interfaces
except-interface=lo
# IPv4 DHCP range
dhcp-range=set:${fa},${base}.2,${base}.253
# Set gateway address
dhcp-option=option:netmask,$mask
dhcp-option=option:router,$gateway
dhcp-option=option:dns-server,$gateway
$mtu_option
address=/host.lan/$gateway
$upstream_entry
$filter_dns
# DHCP settings
dhcp-authoritative
# Windows compatibility
dhcp-option=252,"\n"
dhcp-option=vendor:MSFT,2,1i
EOF2
then
error "Failed to write dnsmasq config file: $file"
return 1
fi
return 0
}
setInterfaces() {
local fa="$1"
local tap="$2"
local gateway="$3"
# Add all available network interfaces
local file="/etc/network/interfaces.new"
if ! sed 's/^ //' > "$file" <<EOF2
auto lo
iface lo inet loopback
EOF2
then
error "Failed to write network interface config file: $file"
return 1
fi
while IFS= read -r i; do
[[ "${i,,}" == "${fa,,}" ]] && continue
if ! sed 's/^ //' >> "$file" <<EOF2
auto $i
iface $i inet manual
EOF2
then
error "Failed to append interface $i to config file: $file"
return 1
fi
done < <(ip -o link show | awk -F': ' '{ print $2 }' | grep -v lo | sed 's/@.*//')
# Configure bridge
if ! sed 's/^ //' >> "$file" <<EOF2
auto $fa
iface $fa inet static
address $gateway/$PREFIX
bridge-ports $tap
bridge-stp off
bridge-fd 0
source /etc/network/interfaces.d/*
EOF2
then
error "Failed to append bridge config to file: $file"
return 1
fi
return 0
}
# ######################################
# Network mode setup
# ######################################
createBridge() {
local gateway="$1"
local msg
# Create a bridge with a static IP for the VM LAN
{ msg=$(ip link add dev "$BRIDGE" type bridge 2>&1); local rc=$?; } || :
if (( rc != 0 )); then
[ -n "$msg" ] && echo "$msg" >&2
case "${msg,,}" in
*"operation not permitted"* | *"permission denied"* )
error "failed to create bridge. $ADD_ERR --cap-add NET_ADMIN" ;;
* )
error "failed to create bridge." ;;
esac
return 1
fi
if [[ "$LAN_MTU" != "0" ]]; then
setMTU "$BRIDGE" "$LAN_MTU"
fi
if ! ip address add "$gateway/$PREFIX" dev "$BRIDGE"; then
error "failed to add IP address pool!"
return 1
fi
while ! ip link set "$BRIDGE" up; do
info "Waiting for IP address to become available..."
sleep 2
done
# NAT networking is IPv4-only; disable IPv6 on the VM bridge if possible.
disableIPv6 "$BRIDGE"
return 0
}
createTap() {
local tuntap="$1"
local msg
# Set tap to the bridge created
{ msg=$(ip tuntap add dev "$TAP" mode tap 2>&1); local rc=$?; } || :
if (( rc != 0 )); then
[ -n "$msg" ] && echo "$msg" >&2
error "$tuntap"
return 1
fi
if [[ "$LAN_MTU" != "0" ]]; then
setMTU "$TAP" "$LAN_MTU"
fi
if ! ip link set dev "$TAP" address "$GATEWAY_MAC"; then
warn "failed to set gateway MAC address."
fi
while ! ip link set "$TAP" up promisc on; do
info "Waiting for TAP to become available..."
sleep 2
done
# NAT networking is IPv4-only; disable IPv6 on the VM tap if possible.
disableIPv6 "$TAP"
if ! ip link set dev "$TAP" master "$BRIDGE"; then
error "failed to set master bridge!"
return 1
fi
return 0
}
# ######################################
# IP tables
# ######################################
getTablesBackend() {
local version
version=$(iptables --version 2>/dev/null || true)
case "$version" in
*nf_tables* ) echo "nft" ;;
*legacy* ) echo "legacy" ;;
* ) return 1 ;;
esac
}
setTables() {
local mode="$1"
local path
path=$(command -v "iptables-$mode" 2>/dev/null || true)
[ -z "$path" ] && return 1
update-alternatives --set iptables "$path" > /dev/null 2>&1
}
showRules() {
local table="$1"
local chain="$2"
local label="$3"
local rule_tag="$4"
local rules
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
enabled "$DEBUG" || return 0
rules=$(
iptables -t "$table" -S "$chain" 2>/dev/null |
awk '$1 == "-A"' |
grep -Ev -- "$own_rule" || true
)
[ -n "$rules" ] || return 0
printf "Existing %s rules:\n\n%s\n\n" "$label" "$rules"
return 0
}
checkExistingTables() {
local rules conflicts
local rule_tag="PROXMOX_NAT"
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
rules=$(
iptables -t filter -S FORWARD 2>/dev/null |
awk '$1 == "-A"' |
grep -Ev -- "$own_rule" || true
)
conflicts=$(grep -E -- \
'^-A FORWARD .*(-j DROP|-j REJECT)( |$)' \
<<< "$rules" || true)
if [ -n "$conflicts" ]; then
local msg="your existing firewall rules may block traffic forwarded to or from the VM subnet"
if enabled "$DEBUG"; then
warn "${msg}."
else
warn "${msg}; enable DEBUG=Y to inspect them."
fi
fi
showRules filter FORWARD "filter FORWARD" "$rule_tag"
showRules nat POSTROUTING "NAT POSTROUTING" "$rule_tag"
return 0
}
runTableRule() {
local silent="$1"
local result="$2"
local msg
shift 2
printf -v "$result" '%s' ""
{ msg=$("$@" 2>&1); local rc=$?; } || :
(( rc == 0 )) && return 0
printf -v "$result" '%s' "$msg"
if ! enabled "$silent" || enabled "$DEBUG"; then
[ -n "$msg" ] && echo "$msg" >&2
fi
return 1
}
tableError() {
local silent="$1"
local message="${2,,}"
if enabled "$silent" && ! enabled "$DEBUG"; then
return 1
fi
case "$message" in
*"permission denied"* | *"operation not permitted"* )
warn "IP tables access was denied. Add the NET_ADMIN capability."
;;
*"table does not exist"* | *"can't initialize iptables table"* )
warn "The required IP tables kernel modules may be unavailable. Try: sudo modprobe ip_tables iptable_nat"
;;
*"no chain/target/match by that name"* )
warn "A required IP tables target or match is unavailable in the host kernel."
;;
*"could not fetch rule set generation id"* )
warn "The nftables backend is unavailable or inaccessible in this container."
;;
* )
warn "Failed to configure IP tables. Verify NET_ADMIN access and host IP tables support."
;;
esac
return 1
}
showTableCleanupError() {
local command="$1"
local message="$2"
enabled "$DEBUG" || return 0
printf "Failed IP tables cleanup command:\n\n%s\n\n" "$command" >&2
[ -n "$message" ] && printf "%s\n\n" "$message" >&2
return 0
}
applyTables() {
local subnet="$1"
local upstream="${2:-}"
local gateway="${3:-}"
local silent="${4:-N}"
local table_error
local rule_tag="PROXMOX_NAT"
# NAT traffic from the VM subnet leaving through any external interface.
if ! runTableRule "$silent" table_error \
iptables -t nat -A POSTROUTING \
! -o "$BRIDGE" \
-s "$subnet" \
! -d "$subnet" \
-m comment --comment "$rule_tag" \
-j MASQUERADE; then
tableError "$silent" "$table_error"
return 1
fi
# Forward the reserved system.lan alias to the container upstream gateway.
if [ -n "$upstream" ] && [ -n "$gateway" ]; then
if ! runTableRule "$silent" table_error \
iptables -t nat -A PREROUTING \
-i "$BRIDGE" \
-s "$subnet" \
-d "$upstream" \
-m comment --comment "$rule_tag" \
-j DNAT --to-destination "$gateway"; then
tableError "$silent" "$table_error"
return 1
fi
fi
# Allow traffic from the VM bridge to any external interface.
if ! runTableRule "$silent" table_error \
iptables -A FORWARD \
-i "$BRIDGE" \
! -o "$BRIDGE" \
-s "$subnet" \
-m comment --comment "$rule_tag" \
-j ACCEPT; then
tableError "$silent" "$table_error"
return 1
fi
# Allow traffic from any external interface to the VM subnet.
if ! runTableRule "$silent" table_error \
iptables -A FORWARD \
! -i "$BRIDGE" \
-o "$BRIDGE" \
-d "$subnet" \
-m comment --comment "$rule_tag" \
-j ACCEPT; then
tableError "$silent" "$table_error"
return 1
fi
return 0
}
clearTables() {
local table=""
local line rules remaining
local rule_tag="PROXMOX_NAT"
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
# Return 2 when the currently selected backend cannot be accessed.
# This lets configureTables() distinguish it from an actual rule-cleanup failure.
if ! rules=$(iptables-save 2> /dev/null); then
if enabled "$DEBUG"; then
local message
message=$(iptables-save 2>&1 > /dev/null || true)
showTableCleanupError "iptables-save" "$message"
fi
return 2
fi
if [ -n "$rules" ]; then
# Delete every rule tagged with our unique identifier,
# leaving all other rules intact.
while IFS= read -r line; do
case "$line" in
\*nat ) table="nat" ;;
\*filter ) table="filter" ;;
\*mangle ) table="mangle" ;;
\*raw ) table="raw" ;;
esac
if [[ "$line" == -A* ]] && [[ "$line" =~ $own_rule ]]; then
line="${line/-A /-D }"
# Parse the quoting produced by iptables-save before deleting the rule.
local message
if ! message=$(
printf '%s\n' "$line" |
xargs -r iptables -t "$table" 2>&1
); then
showTableCleanupError "iptables -t $table $line" "$message"
fi
fi
done <<< "$rules"
fi
# Base the result on the final ruleset instead of intermediate errors.
if ! rules=$(iptables-save 2> /dev/null); then
if enabled "$DEBUG"; then
local message
message=$(iptables-save 2>&1 > /dev/null || true)
showTableCleanupError "iptables-save" "$message"
fi
return 1
fi
remaining=$(grep -E -- "$own_rule" <<< "$rules" || true)
if [ -n "$remaining" ]; then
if enabled "$DEBUG"; then
warn "IP tables cleanup left the following rules behind:"
echo "$remaining" >&2
fi
return 1
fi
return 0
}
hasTaggedRules() {
local save="$1"
local rules
local rule_tag="PROXMOX_NAT"
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
# Return 2 when the backend cannot be inspected.
if ! rules=$("$save" 2>/dev/null); then
return 2
fi
if grep -Eq -- "$own_rule" <<< "$rules"; then
return 0
fi
return 1
}
configureTables() {
local subnet="$1"
local upstream="${2:-}"
local gateway="${3:-}"
local preferred
local alternate_save
local preferred_clean="N"
local alternate_dirty="N"
preferred=$(getTablesBackend) || {
error "failed to determine the active IP tables backend!"
return 1
}
case "$preferred" in
"nft" ) local alternate="legacy" ;;
"legacy" ) local alternate="nft" ;;
* )
error "unsupported IP tables backend: $preferred"
return 1 ;;
esac
# Inspect the alternate backend without changing the active alternative.
alternate_save=$(command -v "iptables-$alternate-save" 2>/dev/null || true)
if [ -n "$alternate_save" ]; then
if hasTaggedRules "$alternate_save"; then
# Only switch backends when stale Proxmox rules were positively found.
if ! setTables "$alternate"; then
error "failed to select the $alternate IP tables backend for cleanup!"
return 1
fi
if ! clearTables; then
alternate_dirty="Y"
fi
# Always restore the originally selected backend after cleanup.
if ! setTables "$preferred"; then
error "failed to restore the preferred $preferred IP tables backend!"
return 1
fi
if enabled "$alternate_dirty"; then
error "failed to clean up the existing $alternate IP tables configuration!"
return 1
fi
else
local rc=$?
# An unavailable alternate backend does not affect normal startup.
if (( rc == 2 )) && enabled "$DEBUG"; then
warn "failed to inspect the $alternate IP tables backend!"
fi
fi
fi
# Try the preferred backend first.
if clearTables; then
preferred_clean="Y"
# Try the preferred backend without reporting provisional failures.
if applyTables "$subnet" "$upstream" "$gateway" "Y"; then
checkExistingTables
return 0
fi
# Never switch backends while partial rules remain in the preferred backend.
if ! clearTables; then
error "failed to clean up the partial $preferred IP tables configuration!"
return 1
fi
else
local rc=$?
# The preferred backend was accessible, but its rules could not be removed.
# Do not switch while partial or stale rules may still be active.
if (( rc == 1 )); then
error "failed to clean up the existing $preferred IP tables configuration!"
return 1
fi
# Return code 2 means the preferred backend itself could not be accessed,
# so it is safe to try the alternate backend.
if (( rc != 2 )); then
error "failed to access the $preferred IP tables backend!"
return 1
fi
enabled "$DEBUG" && warn "failed to access the $preferred IP tables backend!"
fi
# Try the alternate backend when the preferred backend failed.
if setTables "$alternate"; then
# Remove rules left by a previous run from the alternate backend.
if clearTables; then
if applyTables "$subnet" "$upstream" "$gateway" "Y"; then
checkExistingTables
return 0
fi
if ! clearTables; then
alternate_dirty="Y"
error "failed to clean up the partial $alternate IP tables configuration!"
fi
else
local rc=$?
# Only mark the alternate backend dirty when it was accessible but cleanup failed.
if (( rc == 1 )); then
alternate_dirty="Y"
error "failed to clean up the existing $alternate IP tables configuration!"
elif (( rc != 2 )); then
alternate_dirty="Y"
error "failed to inspect the existing $alternate IP tables configuration!"
elif enabled "$DEBUG"; then
warn "failed to access the $alternate IP tables backend!"
fi
fi
fi
# Restore the preferred backend after the alternate attempt failed.
if ! setTables "$preferred"; then
error "failed to restore the preferred $preferred IP tables backend!"
return 1
fi
# Do not continue while partial rules remain in the alternate backend.
enabled "$alternate_dirty" && return 1
# Both backend failures were already shown in debug mode.
enabled "$DEBUG" && return 1
# An inaccessible preferred backend cannot be retried diagnostically.
if ! enabled "$preferred_clean"; then
error "failed to access both IP tables backends!"
return 1
fi
# Verify that no rules remain before the diagnostic attempt.
if ! clearTables; then
error "failed to clean up the existing $preferred IP tables configuration!"
return 1
fi
# Repeat the preferred backend once to show its actual failure.
if applyTables "$subnet" "$upstream" "$gateway" "N"; then
checkExistingTables
return 0
fi
# Do not leave a partial ruleset after the final failed attempt.
if ! clearTables; then
error "failed to clean up the partial $preferred IP tables configuration!"
fi
return 1
}
configureNAT() {
local base subnet upstream=""
local forwarding=""
local tuntap="TUN device is missing. $ADD_ERR --device /dev/net/tun"
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 > /dev/null 2>&1 || :
local msg
{ msg=$(mknod /dev/net/tun c 10 200 2>&1); local rc=$?; } || :
if (( rc == 0 )); then
chmod 666 /dev/net/tun
elif [ -n "$msg" ]; then
echo "$msg" >&2
fi
fi
if [ ! -c /dev/net/tun ]; then
error "$tuntap"
return 1
fi
# Check IPv4 port forwarding flag
[ -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; local rc=$?; } || :
forwarding=""
[ -r /proc/sys/net/ipv4/ip_forward ] &&
forwarding=$(< /proc/sys/net/ipv4/ip_forward)
if (( rc != 0 )) || [[ "$forwarding" != "1" ]]; then
error "IP forwarding is disabled. $ADD_ERR --sysctl net.ipv4.ip_forward=1"
return 1
fi
fi
base=$(subnetBase "$UPLINK") || return 1
local gateway="$base.1"
subnet=$(networkCIDR "$gateway") || return 1
if subnetInUse "$subnet"; then
error "VM subnet $subnet conflicts with an existing route inside the container."
return 1
else
local rc=$?
(( rc == 1 )) || return 1
fi
createBridge "$gateway" || return 1
createTap "$tuntap" || return 1
# Reserve the final usable address for access to the upstream gateway.
if [ -n "$GATEWAY" ]; then
upstream="$base.254"
addUpstream "$upstream" || upstream=""
fi
# Use the lowest effective VM-LAN MTU, without mutating the uplink MTU.
if [[ "$LAN_MTU" != "0" ]]; then
LAN_MTU=$(minMTU "$LAN_MTU" "$(getMTU "$BRIDGE")" "$(getMTU "$TAP")")
fi
configureTables "$subnet" "$upstream" "$GATEWAY" || return 1
setInterfaces "$BRIDGE" "$TAP" "$gateway" || return 1
configureDNS "$BRIDGE" "$MASK" "$gateway" "$upstream" || return 1
showBridgeInfo "$subnet" "$gateway"
return 0
}
# ######################################
# Cleanup
# ######################################
closeInterfaces() {
ip link set "$TAP" down promisc off &> /dev/null || :
ip link delete "$TAP" &> /dev/null || :
ip link set "$BRIDGE" down &> /dev/null || :
ip link delete "$BRIDGE" &> /dev/null || :
clearTables || :
return 0
}
# ######################################
# Detection
# ######################################
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
fi
return 0
}
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
return 0
}
validateAddresses() {
[ -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
return 0
}
configureMTU() {
local mtu=""
local mtu_custom="N"
if [ -f "/sys/class/net/$DEV/mtu" ]; then
mtu=$(< "/sys/class/net/$DEV/mtu")
fi
[ -n "$MTU" ] && mtu_custom="Y"
[ -z "$MTU" ] && MTU="$mtu"
[ -z "$MTU" ] && MTU="0"
LAN_MTU="$MTU"
# Automatically propagate smaller-than-standard MTUs, but do not automatically
# advertise jumbo frames unless the user explicitly requested MTU.
if [[ "$LAN_MTU" != "0" && "$LAN_MTU" -gt "1500" ]] &&
! enabled "$mtu_custom"; then
LAN_MTU="1500"
fi
return 0
}
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 guests may
# detect a new network every boot.
GATEWAY_MAC=$(gatewayMAC "$MAC")
return 0
}
formatAddress() {
local ip="${1:-}"
local prefix="${2:-}"
local result="$ip"
[ -z "$result" ] && return 1
if [ -n "$prefix" ] && [[ "$prefix" != "24" ]]; then
result+="/$prefix"
fi
echo "$result"
return 0
}
showHostInfo() {
local prefix
prefix=$(ip -4 -o address show dev "$DEV" scope global 2>/dev/null |
awk -v ip="$UPLINK" '
{
split($4, address, "/")
if (address[1] == ip) {
print address[2]
exit
}
}
')
local uplink
uplink=$(formatAddress "$UPLINK" "$prefix" || true)
[ -z "$uplink" ] && uplink="(none)"
local line=" Host: $uplink"
local host
host=$(containerID)
[ -n "$host" ] && line+=" ($host)"
local obvious=""
if [[ "$UPLINK" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)\.[0-9]+$ ]]; then
obvious="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}.1"
fi
local gateway="${GATEWAY:-}"
if [ -z "$gateway" ]; then
line+=" | Gateway: (none)"
elif [[ "$gateway" != "$obvious" ]]; then
line+=" | Gateway: $gateway"
fi
local iface="$DEV"
if [ -n "$NIC" ] && [[ "${NIC,,}" != "veth" ]]; then
iface+="/$NIC"
fi
[ -z "$iface" ] && iface="(none)"
[[ "$iface" != "eth0" ]] && line+=" | Interface: $iface"
local mtu
mtu=$(getMTU "$DEV")
if [ -n "$mtu" ] && [[ "$mtu" != "0" && "$mtu" != "1500" ]]; then
line+=" | MTU: $mtu"
fi
local nameservers=""
local file="/etc/resolv.dnsmasq"
[ ! -f "$file" ] && file="/etc/resolv.conf"
if [ -f "$file" ]; then
nameservers=$(awk '$1 == "nameserver" { print $2 }' "$file" |
paste -sd ',' |
sed 's/,/, /g' || true)
fi
[ -z "$nameservers" ] && nameservers="(none)"
[[ "$nameservers" == "127.0.0.1"* ]] && nameservers=""
echo
if (( ${#nameservers} <= 40 )); then
[ -n "$nameservers" ] && line+=" | DNS: $nameservers"
echo "$line"
else
echo "$line"
echo " DNS: $nameservers"
fi
return 0
}
showBridgeInfo() {
local subnet="$1"
local gateway="$2"
local display
display=$(formatAddress "$gateway" "$PREFIX" || true)
local base="${gateway%.*}"
local dhcp="$base.2-$base.253"
local line=" Bridge: $BRIDGE | Gateway: $display | DHCP: $dhcp"
if [[ "$PREFIX" != "24" ]]; then
line+=" | Subnet: $subnet"
fi
local mtu
mtu=$(getMTU "$BRIDGE")
if [ -n "$mtu" ] && [[ "$mtu" != "0" && "$mtu" != "1500" ]]; then
line+=" | MTU: $mtu"
fi
echo "$line"
echo
return 0
}
initializeNetwork() {
detectInterface
validateInterface
validateMask
detectAddresses
validateAddresses
detectAdapter
validateAdapter
configureMTU
configureMAC
showHostInfo
closeInterfaces
return 0
}
blockLicense() {
# Block connection attempts to license server
sed -i -E \
'/^[[:space:]]*[^#]*[[:space:]]shop\.maurer-it\.com([[:space:]]|$)/d' \
/etc/hosts 2>/dev/null || true
printf '%s\n' \
'127.0.0.1 shop.maurer-it.com' \
'::1 shop.maurer-it.com' >> /etc/hosts 2>/dev/null || true
return 0
}
# ######################################
# Configure Network
# ######################################
blockLicense
disabled "$NETWORK" && return 0
if ! isNAT; then
error "Unrecognized NETWORK value: \"$NETWORK\""
exit 48
fi
msg="Initializing network..."
enabled "$DEBUG" && info "$msg"
initializeNetwork
# Configure NAT networking
if ! configureNAT; then
closeInterfaces
error "failed to setup NAT networking!"
[[ "$DEBUG" != [Yy1]* ]] && exit 48
else
enabled "$DEBUG" && info "Initialized network successfully..."
fi
return 0