|
#!/usr/bin/env bash |
|
set -euo pipefail |
|
|
|
# Proxmox WireGuard + VXLAN manager (multi-node) |
|
# - Underlay: WireGuard wg0 |
|
# - Overlay: VXLAN VNI 200 over wg0, attached to vmbr3 (172.24.200.0/24) |
|
# - VXLAN config file: /etc/network/interfaces.d/vxlan |
|
# - Inventory: /etc/krip-vxlan/nodes.conf (simple CSV) |
|
# |
|
# Safe: does NOT touch vmbr0 or public routing. |
|
|
|
WG_IF="wg0" |
|
WG_DIR="/etc/wireguard" |
|
WG_CONF="${WG_DIR}/${WG_IF}.conf" |
|
WG_PORT_DEFAULT="51820" |
|
|
|
VXLAN_IF="vxlan200" |
|
VXLAN_FILE="/etc/network/interfaces.d/vxlan" |
|
VXLAN_VNI_DEFAULT="200" |
|
VXLAN_DSTPORT_DEFAULT="4789" |
|
|
|
BRIDGE="vmbr3" |
|
STATE_DIR="/etc/krip-vxlan" |
|
NODES_FILE="${STATE_DIR}/nodes.conf" |
|
|
|
# ---------- helpers ---------- |
|
err(){ echo "❌ $*" >&2; } |
|
ok(){ echo "✅ $*"; } |
|
warn(){ echo "⚠️ $*"; } |
|
info(){ echo "ℹ️ $*"; } |
|
|
|
require_root() { |
|
[[ $EUID -eq 0 ]] || { err "Run as root"; exit 1; } |
|
} |
|
|
|
have_cmd(){ command -v "$1" >/dev/null 2>&1; } |
|
|
|
pause(){ read -r -p "Press Enter to continue... " _; } |
|
|
|
ensure_dirs() { |
|
mkdir -p "$WG_DIR" "$STATE_DIR" |
|
chmod 700 "$STATE_DIR" |
|
} |
|
|
|
detect_primary_pubip() { |
|
# best effort: show current default route dev and its src (may be via vmbr0) |
|
local src |
|
src="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '/src/ {for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -n1 || true)" |
|
echo "$src" |
|
} |
|
|
|
bridge_exists() { |
|
ip link show "$BRIDGE" >/dev/null 2>&1 |
|
} |
|
|
|
wg_installed() { |
|
have_cmd wg && have_cmd wg-quick |
|
} |
|
|
|
vxlan_supported() { |
|
ip link add "${VXLAN_IF}_test" type vxlan id 999 dev lo dstport 4789 >/dev/null 2>&1 && \ |
|
ip link del "${VXLAN_IF}_test" >/dev/null 2>&1 |
|
} |
|
|
|
wg_is_up() { |
|
ip link show "$WG_IF" >/dev/null 2>&1 && ip link show "$WG_IF" | grep -q "UP" |
|
} |
|
|
|
vxlan_is_up() { |
|
ip link show "$VXLAN_IF" >/dev/null 2>&1 && ip link show "$VXLAN_IF" | grep -q "UP" |
|
} |
|
|
|
get_link_mtu() { |
|
ip -o link show "$1" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="mtu") print $(i+1)}' | head -n1 |
|
} |
|
|
|
file_has_line() { |
|
local f="$1"; shift |
|
local pat="$1" |
|
[[ -f "$f" ]] && grep -qE "$pat" "$f" |
|
} |
|
|
|
kv_get() { |
|
# read key=value from file (first match) |
|
local key="$1" file="$2" |
|
awk -F= -v k="$key" '$1==k {print $2; exit}' "$file" 2>/dev/null || true |
|
} |
|
|
|
# ---------- inventory ---------- |
|
# nodes.conf format (CSV): |
|
# name,wg_ip,public_ip,public_port,public_key |
|
ensure_nodes_file() { |
|
[[ -f "$NODES_FILE" ]] || { |
|
cat >"$NODES_FILE" <<'EOF' |
|
# name,wg_ip,public_ip,public_port,public_key |
|
EOF |
|
chmod 600 "$NODES_FILE" |
|
} |
|
} |
|
|
|
list_nodes() { |
|
ensure_nodes_file |
|
grep -v '^\s*#' "$NODES_FILE" | grep -v '^\s*$' || true |
|
} |
|
|
|
node_exists_by_name() { |
|
local name="$1" |
|
list_nodes | awk -F, -v n="$name" '$1==n {found=1} END{exit !found}' |
|
} |
|
|
|
add_node() { |
|
ensure_nodes_file |
|
local name="$1" wgip="$2" pubip="$3" pubport="$4" pubkey="$5" |
|
if node_exists_by_name "$name"; then |
|
warn "Node '$name' already exists in inventory." |
|
return 0 |
|
fi |
|
echo "${name},${wgip},${pubip},${pubport},${pubkey}" >>"$NODES_FILE" |
|
ok "Added node '$name' to inventory." |
|
} |
|
|
|
# ---------- WireGuard ---------- |
|
wg_generate_keys_if_missing() { |
|
local priv="${WG_DIR}/privatekey" |
|
local pub="${WG_DIR}/publickey" |
|
|
|
if [[ -f "$priv" && -f "$pub" ]]; then |
|
ok "WireGuard keys already exist." |
|
return 0 |
|
fi |
|
|
|
umask 077 |
|
wg genkey | tee "$priv" | wg pubkey > "$pub" |
|
ok "Generated WireGuard keypair." |
|
ok "Public key:" |
|
cat "$pub" |
|
} |
|
|
|
wg_init_conf_if_missing() { |
|
if [[ -f "$WG_CONF" ]]; then |
|
ok "$WG_CONF already exists." |
|
return 0 |
|
fi |
|
|
|
local privkey wg_ip listen_port |
|
privkey="$(cat "${WG_DIR}/privatekey")" |
|
read -r -p "Local WireGuard IP (e.g. 10.10.10.1/24): " wg_ip |
|
listen_port="${WG_PORT_DEFAULT}" |
|
read -r -p "WireGuard listen port [${WG_PORT_DEFAULT}]: " tmp || true |
|
[[ -n "${tmp:-}" ]] && listen_port="$tmp" |
|
|
|
cat >"$WG_CONF" <<EOF |
|
[Interface] |
|
Address = ${wg_ip} |
|
ListenPort = ${listen_port} |
|
PrivateKey = ${privkey} |
|
MTU = 1420 |
|
|
|
EOF |
|
chmod 600 "$WG_CONF" |
|
ok "Created $WG_CONF" |
|
} |
|
|
|
wg_add_peer_to_conf() { |
|
[[ -f "$WG_CONF" ]] || { err "Missing $WG_CONF. Run WireGuard setup first."; return 1; } |
|
|
|
local name wgip pubip pubport pubkey allowed extra_allowed |
|
read -r -p "Peer name (inventory key): " name |
|
read -r -p "Peer WG IP (e.g. 10.10.10.2): " wgip |
|
read -r -p "Peer Public IP (public ip or hostname): " pubip |
|
read -r -p "Peer Public port [${WG_PORT_DEFAULT}]: " pubport || true |
|
pubport="${pubport:-$WG_PORT_DEFAULT}" |
|
read -r -p "Peer WireGuard PUBLIC key: " pubkey |
|
|
|
# AllowedIPs: peer WG /32 (plus optional extra) |
|
allowed="${wgip}/32" |
|
read -r -p "Extra AllowedIPs (comma-separated) [empty]: " extra_allowed || true |
|
|
|
# idempotency: if pubkey already exists, skip |
|
if grep -q "PublicKey = ${pubkey}" "$WG_CONF"; then |
|
warn "Peer already present in $WG_CONF (same PublicKey)." |
|
else |
|
{ |
|
echo "[Peer]" |
|
echo "PublicKey = ${pubkey}" |
|
echo "Endpoint = ${pubip}:${pubport}" |
|
if [[ -n "${extra_allowed:-}" ]]; then |
|
echo "AllowedIPs = ${allowed}, ${extra_allowed}" |
|
else |
|
echo "AllowedIPs = ${allowed}" |
|
fi |
|
echo "PersistentKeepalive = 25" |
|
echo |
|
} >>"$WG_CONF" |
|
ok "Added peer to $WG_CONF" |
|
fi |
|
|
|
add_node "$name" "$wgip" "$pubip" "$pubport" "$pubkey" |
|
} |
|
|
|
wg_up() { |
|
[[ -f "$WG_CONF" ]] || { err "Missing $WG_CONF"; return 1; } |
|
systemctl enable --now "wg-quick@${WG_IF}" >/dev/null |
|
ok "wg-quick@${WG_IF} enabled and started." |
|
wg show || true |
|
} |
|
|
|
wg_down() { |
|
systemctl stop "wg-quick@${WG_IF}" >/dev/null 2>&1 || true |
|
ok "wg0 stopped." |
|
} |
|
|
|
# ---------- VXLAN ---------- |
|
vxlan_write_config() { |
|
bridge_exists || { err "Bridge $BRIDGE not found. Create vmbr3 first."; return 1; } |
|
[[ -f "$WG_CONF" ]] || { err "WireGuard not configured yet. Do WG first."; return 1; } |
|
|
|
local vni dstport local_wg_ip mtu |
|
vni="$VXLAN_VNI_DEFAULT" |
|
dstport="$VXLAN_DSTPORT_DEFAULT" |
|
|
|
read -r -p "VXLAN VNI [${VXLAN_VNI_DEFAULT}]: " tmp || true |
|
[[ -n "${tmp:-}" ]] && vni="$tmp" |
|
|
|
read -r -p "VXLAN UDP dstport [${VXLAN_DSTPORT_DEFAULT}]: " tmp || true |
|
[[ -n "${tmp:-}" ]] && dstport="$tmp" |
|
|
|
# derive local wg IP from wg0.conf Address (strip /xx) |
|
local_wg_ip="$(awk -F'=' '/^Address/ {gsub(/ /,"",$2); print $2}' "$WG_CONF" | head -n1 | cut -d/ -f1)" |
|
[[ -n "$local_wg_ip" ]] || { err "Could not detect local WG IP from $WG_CONF"; return 1; } |
|
|
|
# MTU: use kernel-determined (don’t force above what kernel allows). We’ll set to 1370 (safe in your setup). |
|
mtu="1370" |
|
read -r -p "VXLAN/Bridge MTU [1370 recommended]: " tmp || true |
|
[[ -n "${tmp:-}" ]] && mtu="$tmp" |
|
|
|
cat >"$VXLAN_FILE" <<EOF |
|
auto ${VXLAN_IF} |
|
iface ${VXLAN_IF} inet manual |
|
mtu ${mtu} |
|
pre-up ip link add ${VXLAN_IF} type vxlan id ${vni} dev ${WG_IF} local ${local_wg_ip} dstport ${dstport} nolearning |
|
up ip link set ${VXLAN_IF} master ${BRIDGE} |
|
up ip link set ${VXLAN_IF} up |
|
# Controlled flood to all peers (wildcard MAC) - entries added by script per peer: |
|
down ip link del ${VXLAN_IF} |
|
EOF |
|
|
|
chmod 644 "$VXLAN_FILE" |
|
ok "Wrote VXLAN config to $VXLAN_FILE" |
|
|
|
info "NOTE: You must ensure ${BRIDGE} MTU matches (${mtu}). Script can set it for you (menu option)." |
|
} |
|
|
|
vxlan_add_fdb_for_all_peers() { |
|
ensure_nodes_file |
|
[[ -f "$VXLAN_FILE" ]] || { err "Missing $VXLAN_FILE. Write VXLAN config first."; return 1; } |
|
[[ -f "$WG_CONF" ]] || { err "Missing $WG_CONF."; return 1; } |
|
|
|
# Ensure base del/add lines exist for each peer? We’ll append per-peer lines under the comment marker. |
|
# We'll rebuild the peer block each time to avoid duplicates. |
|
|
|
local tmpfile mtu |
|
mtu="$(awk '/^\s*mtu\s+/ {print $2; exit}' "$VXLAN_FILE" || true)" |
|
[[ -n "$mtu" ]] || mtu="1370" |
|
|
|
tmpfile="$(mktemp)" |
|
awk ' |
|
BEGIN{inpeers=0} |
|
{print} |
|
/Controlled flood to all peers/ {inpeers=1} |
|
/down ip link del/ { } |
|
' "$VXLAN_FILE" >"$tmpfile" |
|
|
|
# Remove any old peer lines we previously added (between marker and down) |
|
# We will reconstruct by stripping lines matching "bridge fdb" before "down". |
|
tmpfile2="$(mktemp)" |
|
awk ' |
|
BEGIN{skip=0} |
|
/Controlled flood to all peers/ {print; skip=1; next} |
|
skip==1 && /^\s*(up\s+)?bridge fdb / {next} |
|
{print} |
|
' "$tmpfile" >"$tmpfile2" |
|
|
|
# Now insert new lines right after marker |
|
tmpfile3="$(mktemp)" |
|
awk -v nodes="$NODES_FILE" ' |
|
function trim(s){gsub(/^[ \t]+|[ \t]+$/,"",s); return s} |
|
BEGIN{ |
|
while((getline line < nodes)>0){ |
|
if(line ~ /^[ \t]*#/ || line ~ /^[ \t]*$/) continue |
|
n[++cnt]=line |
|
} |
|
close(nodes) |
|
} |
|
/Controlled flood to all peers/ { |
|
print |
|
print " up bridge fdb del 00:00:00:00:00:00 dev vxlan200 2>/dev/null || true" |
|
for(i=1;i<=cnt;i++){ |
|
split(n[i],a,",") |
|
name=trim(a[1]); wgip=trim(a[2]) |
|
# dst should be peer WG IP (not public) |
|
# add wildcard flood for VLAN 1 |
|
print " up bridge fdb add 00:00:00:00:00:00 dev vxlan200 dst " wgip " self permanent vlan 1" |
|
} |
|
next |
|
} |
|
{print} |
|
' "$tmpfile2" >"$tmpfile3" |
|
|
|
mv "$tmpfile3" "$VXLAN_FILE" |
|
rm -f "$tmpfile" "$tmpfile2" 2>/dev/null || true |
|
|
|
ok "Updated FDB peer entries in $VXLAN_FILE from inventory ($NODES_FILE)." |
|
info "If you have N nodes, each node should have ALL other peers in its inventory." |
|
} |
|
|
|
vxlan_up() { |
|
[[ -f "$VXLAN_FILE" ]] || { err "Missing $VXLAN_FILE"; return 1; } |
|
wg_is_up || { err "WireGuard (${WG_IF}) is not UP. Start wg0 first."; return 1; } |
|
|
|
# bring only vxlan200, avoid full ifreload if user is nervous |
|
ifup "$VXLAN_IF" || true |
|
|
|
if vxlan_is_up; then |
|
ok "${VXLAN_IF} is UP." |
|
else |
|
err "${VXLAN_IF} did not come UP. Check 'ip -d link show ${VXLAN_IF}'" |
|
return 1 |
|
fi |
|
} |
|
|
|
vxlan_down() { |
|
ifdown "$VXLAN_IF" >/dev/null 2>&1 || true |
|
ip link del "$VXLAN_IF" >/dev/null 2>&1 || true |
|
ok "${VXLAN_IF} is DOWN/removed." |
|
} |
|
|
|
set_vmbr3_mtu() { |
|
bridge_exists || { err "Bridge $BRIDGE not found."; return 1; } |
|
local mtu |
|
read -r -p "Set ${BRIDGE} MTU to (recommended 1370): " mtu |
|
[[ -n "$mtu" ]] || { err "MTU required"; return 1; } |
|
|
|
ip link set dev "$BRIDGE" mtu "$mtu" || { err "Failed to set MTU on $BRIDGE"; return 1; } |
|
ok "Set $BRIDGE MTU to $mtu (runtime)." |
|
|
|
info "To persist, add 'mtu $mtu' to the ${BRIDGE} stanza in /etc/network/interfaces." |
|
} |
|
|
|
# ---------- checks / status ---------- |
|
status_report() { |
|
echo "---------------- STATUS ----------------" |
|
echo "Host: $(hostname)" |
|
echo "Date: $(date)" |
|
echo |
|
|
|
echo "[Bridge]" |
|
if bridge_exists; then |
|
ok "$BRIDGE exists (mtu=$(get_link_mtu "$BRIDGE"))" |
|
ip -br link show "$BRIDGE" || true |
|
ip -4 addr show "$BRIDGE" | sed -n '1,5p' || true |
|
else |
|
warn "$BRIDGE not found" |
|
fi |
|
echo |
|
|
|
echo "[WireGuard]" |
|
if [[ -f "$WG_CONF" ]]; then ok "$WG_CONF present"; else warn "$WG_CONF missing"; fi |
|
if wg_is_up; then ok "wg0 is UP"; else warn "wg0 is DOWN"; fi |
|
wg show 2>/dev/null || true |
|
echo |
|
|
|
echo "[VXLAN]" |
|
if [[ -f "$VXLAN_FILE" ]]; then ok "$VXLAN_FILE present"; else warn "$VXLAN_FILE missing"; fi |
|
if vxlan_is_up; then ok "vxlan200 is UP (mtu=$(get_link_mtu "$VXLAN_IF"))"; else warn "vxlan200 is DOWN"; fi |
|
ip -d link show "$VXLAN_IF" 2>/dev/null | sed -n '1,6p' || true |
|
echo |
|
|
|
echo "[Inventory]" |
|
ensure_nodes_file |
|
if list_nodes | grep -q .; then |
|
ok "nodes.conf has entries:" |
|
list_nodes | sed 's/^/ - /' |
|
else |
|
warn "nodes.conf empty" |
|
fi |
|
echo "----------------------------------------" |
|
} |
|
|
|
sanity_warn_order() { |
|
if ! bridge_exists; then |
|
warn "vmbr3 missing. Create it first (Proxmox UI) before VXLAN." |
|
fi |
|
if ! wg_installed; then |
|
warn "WireGuard tools missing. Install prereqs first." |
|
fi |
|
if [[ ! -f "$WG_CONF" ]]; then |
|
warn "wg0.conf missing. Run WireGuard setup." |
|
fi |
|
if [[ -f "$VXLAN_FILE" && ! -f "$WG_CONF" ]]; then |
|
warn "VXLAN config exists but wg0.conf missing. Order mismatch." |
|
fi |
|
} |
|
|
|
install_prereqs() { |
|
info "Installing packages..." |
|
apt-get update -y |
|
apt-get install -y wireguard iproute2 bridge-utils ifupdown2 lxc-utils tcpdump |
|
ok "Prereqs installed." |
|
} |
|
|
|
# ---------- menu ---------- |
|
menu() { |
|
while true; do |
|
clear |
|
echo "===============================================" |
|
echo " Proxmox WireGuard + VXLAN (vmbr3) Manager" |
|
echo "===============================================" |
|
echo "1) Install prerequisites" |
|
echo "2) Generate WireGuard keys (if missing)" |
|
echo "3) Create wg0.conf (if missing)" |
|
echo "4) Add peer to wg0.conf + inventory" |
|
echo "5) Bring UP WireGuard (wg0)" |
|
echo "6) Write VXLAN config (/etc/network/interfaces.d/vxlan)" |
|
echo "7) Update VXLAN FDB entries from inventory" |
|
echo "8) Bring UP VXLAN (vxlan200)" |
|
echo "9) Set vmbr3 MTU (runtime)" |
|
echo "10) Status report" |
|
echo "11) Bring DOWN VXLAN" |
|
echo "12) Bring DOWN WireGuard" |
|
echo "0) Exit" |
|
echo |
|
sanity_warn_order |
|
echo |
|
read -r -p "Select: " choice |
|
case "$choice" in |
|
1) install_prereqs; pause;; |
|
2) wg_generate_keys_if_missing; pause;; |
|
3) wg_init_conf_if_missing; pause;; |
|
4) wg_add_peer_to_conf; pause;; |
|
5) wg_up; pause;; |
|
6) vxlan_write_config; pause;; |
|
7) vxlan_add_fdb_for_all_peers; pause;; |
|
8) vxlan_up; pause;; |
|
9) set_vmbr3_mtu; pause;; |
|
10) status_report; pause;; |
|
11) vxlan_down; pause;; |
|
12) wg_down; pause;; |
|
0) exit 0;; |
|
*) warn "Invalid option"; pause;; |
|
esac |
|
done |
|
} |
|
|
|
# ---------- main ---------- |
|
require_root |
|
ensure_dirs |
|
ensure_nodes_file |
|
menu |