远程给 Debian 12 装 KVM + Proxmox VE,最怕的就是切桥接那一刻把自己关在门外。
其实可以做到零断网 🚀 —— 腾讯云实测:br0 切换前后内网 IP、出口公网 IP 全部不变,本机每 15 秒探活全程零中断。
关键不在于"桥接本身",而在于顺序:先把看门狗武装好,再去动网络。
本文把全过程整理成可直接复制粘贴的命令,共 6 个阶段 👇
💡 三条铁律(先记住这三条)
- 先武装,再动网 —— 任何网络变更前,看门狗必须已经在跑。顺序反了就没有退路。
- 只用
ifreload -a——ifdown/ifup和systemctl restart networking必然断网,禁用。 - 高危动作 detached 跑 ——
setsid nohup ... </dev/null &,SSH 断了脚本照样跑完并自己回滚。
🎯 为什么不会断网
- 云主机:公网 IP 是 NAT 映射到内网 IP,DHCP 按 MAC 分配。
br0沿用原网卡 MAC,就拿到同一个内网 IP,NAT 映射不变。 - 物理机:公网 IP 是静态直配,跟 MAC 无关。br0 写成
inet static原样搬过去即可。
- cloud-init ——
/etc/network/interfaces.d/50-cloud-init文件头自己写着"重启后不保留",它才是"重启就失联"的真凶。 - 主机名指向回环
127.0.1.1—— PVE 硬性要求解析到非回环 IP,不修会导致集群服务异常。 - Proxmox 官方源在国内基本不可用 —— 实测 935 B/s,装 1.5GB 要 20 天。必须换清华 TUNA 镜像(1.2 MB/s,快 1300 倍)。
阶段 0 · 侦察(只读,不改任何东西)
功能:摸清家底 —— 网卡名、CIDR、网关、MAC、有没有 /dev/kvm,并判定 IP 是静态还是 DHCP。
这个判定是整个方案的分水岭,它决定后面桥接怎么写、回滚怎么走。
mkdir -p /root/kvm-bridge && cd /root/kvm-bridge
echo "===== 1. 网卡与地址(记下来:网卡名 / CIDR / 网关)====="
ip -br addr
ip route
echo "===== 2. 物理网卡 MAC(后面必须沿用)====="
for d in /sys/class/net/*; do
d=${d##*/}
[ "$(cat /sys/class/net/$d/type 2>/dev/null)" = 1 ] && echo " $d $(cat /sys/class/net/$d/address)"
done
echo "===== 3. 网络配置来源(判定 static / dhcp 的关键)====="
cat /etc/network/interfaces
echo "--- interfaces.d ---"
ls -l /etc/network/interfaces.d/ 2>/dev/null && grep -r . /etc/network/interfaces.d/ 2>/dev/null
echo "--- iface 写法 ---"
grep -rE 'iface[[:space:]]+[a-z0-9]+[[:space:]]+inet[[:space:]]+(dhcp|static)' \
/etc/network/interfaces /etc/network/interfaces.d/ 2>/dev/null || echo "(无)"
echo "===== 4. 虚拟化支持(有无 /dev/kvm 决定能否跑 VM)====="
echo -n " vmx 标志数: "; grep -cw vmx /proc/cpuinfo
ls -l /dev/kvm 2>/dev/null || echo " 无 /dev/kvm —— PVE 能装能管,但跑不了硬件加速 VM"
echo "===== 5. 主机名解析(PVE 要求非回环)====="
hostname
echo -n " hostname -i => "; hostname --ip-address 2>/dev/null
cat /etc/hosts
echo "===== 6. 网络工具与磁盘 ====="
command -v ifreload || echo " 无 ifreload(装 PVE 后才有,属 ifupdown2)"
df -h / | tail -1
free -m | head -2
echo "===== 7. 兜底通道自检 ====="
echo " 云主机:确认控制台 VNC 可用"
echo " 物理机:确认 IPMI/iDRAC/iLO 能登录(ipmitool chassis status 或直接开管理页)"通过标准:能明确回答三件事 —— ① 网卡名 ② 完整 CIDR + 网关 ③ 是 static 还是 dhcp。
阶段 1 · 落盘看门狗 + 备份加固(不动网络)
功能:先把"安全网"织好。本阶段完全不碰网络配置,只做三件事 —— 全量备份、切断干扰源(cloud-init / hosts 回环)、部署并武装看门狗。
1.1 写入看门狗(整段粘贴,一次成型)
功能:落盘 netguard.sh + systemd 服务。它是"死手开关"—— 每 15 秒多目标投票探测连通性,
连续不通超过窗口期(开机 120s / 平时 300s)就自动三级回滚。
- 为什么必须是 systemd 常驻:用
nohup挂在 SSH 里,SSH 一断它跟着死,这是最经典的翻车点。 - 为什么多目标投票:只 ping 一个 IP 会误判,误判就会把好配置回滚掉。
cat > /root/kvm-bridge/netguard.sh <<'SCRIPT_EOF'
#!/bin/bash
# =============================================================================
# netguard — 网络变更看门狗 / dead-man switch 【云主机 / 物理机 通用】
# run/arm/disarm/status/check/rollback
# 回滚策略由 /etc/netguard/config 里的 IP_MODE 决定:
# static(物理机公网 IP 直配):① 静态写回 → ② DHCP 兜底 → ③ 保留现场不重启
# dhcp (云主机 NAT 场景): ① 裸 DHCP → ② 重写配置 → ③ 静态写回
# 【为什么静态场景不能先试 DHCP】机房通常不提供 DHCP,dhclient 空跑 30s 失败,
# 而被 flush 掉的原地址不会回来 —— 把"抖动"升级成"确定断网"。
# =============================================================================
set -u
CONF=/etc/netguard/config
STATE_DIR=/var/lib/netguard
LOG=/var/log/netguard.log
ARMED=$STATE_DIR/armed
FAILSINCE=$STATE_DIR/fail_since
ESCALATE=$STATE_DIR/ESCALATE
mkdir -p "$STATE_DIR"
touch "$LOG" 2>/dev/null || true
log() { echo "$(date '+%F %T') [$1] $2" >> "$LOG"; echo "[$1] $2" >&2; }
[ -f "$CONF" ] || { echo "missing $CONF" >&2; exit 1; }
. "$CONF"
: "${IP_MODE:=dhcp}"
: "${TARGETS:=icmp:114.114.114.114 icmp:223.5.5.5 icmp:119.29.29.29}"
: "${MIN_OK:=2}"
: "${FAIL_WINDOW:=300}"
: "${FAIL_WINDOW_BOOT:=120}"
: "${CHECK_INTERVAL:=15}"
: "${BACKUP_DIR:=}"
: "${ORIG_IP:=}"; : "${ORIG_CIDR:=}"; : "${ORIG_GW:=}"; : "${ORIG_DNS:=}"
detect_phys() {
local want="" dev m
[ -f "$STATE_DIR/phys_mac" ] && want=$(cat "$STATE_DIR/phys_mac")
if [ -n "$want" ]; then
for dev in /sys/class/net/*; do
dev=${dev##*/}
case "$dev" in lo|br*|vmbr*|tap*|veth*|fw*|bond*) continue ;; esac
m=$(cat "/sys/class/net/$dev/address" 2>/dev/null)
[ "$m" = "$want" ] && { echo "$dev"; return; }
done
fi
for dev in /sys/class/net/*; do
dev=${dev##*/}
case "$dev" in lo|br*|vmbr*|tap*|veth*|fw*|bond*) continue ;; esac
[ "$(cat /sys/class/net/$dev/type 2>/dev/null)" = 1 ] && { echo "$dev"; return; }
done
echo eth0
}
net_ok() {
local ok=0 t
for t in $TARGETS; do
case "$t" in
icmp:*) ping -c1 -W2 "${t#icmp:}" >/dev/null 2>&1 && ok=$((ok+1)) ;;
http:*) curl -s -m 4 -o /dev/null "${t#http:}" && ok=$((ok+1)) ;;
dns:*) getent hosts "${t#dns:}" >/dev/null 2>&1 && ok=$((ok+1)) ;;
esac
done
[ "$ok" -ge "$MIN_OK" ]
}
snapshot() {
{ echo "=== $(date '+%F %T') phase=$1"
ip -o addr; ip -o link show | awk '{print $2,$17,$(NF-1)}'; ip route
echo "--- resolve"; cat /etc/resolv.conf
} >> "$STATE_DIR/forensics.log" 2>&1
}
tear_bridge() {
if ip link show br0 >/dev/null 2>&1; then
ip addr flush dev br0 2>/dev/null
ip link set br0 down 2>/dev/null
ip link del br0 2>/dev/null
log WARN "已拆除 br0"
fi
rm -f /etc/network/interfaces.d/netguard-br0 /etc/network/interfaces.d/50-cloud-init 2>/dev/null
}
restore_conf_files() {
if [ -n "$BACKUP_DIR" ] && [ -f "$BACKUP_DIR/network/interfaces" ]; then
cp -a "$BACKUP_DIR/network/interfaces" /etc/network/interfaces 2>/dev/null \
&& log INFO "已从备份恢复 /etc/network/interfaces"
fi
}
restore_static() {
local dev; dev=$(detect_phys)
if [ -z "$ORIG_CIDR" ] || [ -z "$ORIG_GW" ]; then
log WARN "缺少 ORIG_CIDR/ORIG_GW,无法静态回写"
return 1
fi
log WARN "静态回写:拆除 br0,把 $ORIG_CIDR gw $ORIG_GW 写回物理网卡 $dev"
snapshot STATIC-before
tear_bridge
restore_conf_files
ip link set "$dev" up
ip addr flush dev "$dev" 2>/dev/null
ip addr add "$ORIG_CIDR" dev "$dev"
ip route replace default via "$ORIG_GW" dev "$dev"
printf 'nameserver %s\n' ${ORIG_DNS:-223.5.5.5} > /etc/resolv.conf
sleep 5
snapshot STATIC-after
net_ok
}
restore_dhcp_raw() {
local dev; dev=$(detect_phys)
log WARN "裸 DHCP:拆除 br0,物理网卡 $dev 直接 dhclient"
snapshot DHCPRAW-before
tear_bridge
pkill -f 'dhclient' 2>/dev/null
sleep 2
ip link set "$dev" up
ip addr flush dev "$dev" 2>/dev/null
timeout 30 dhclient -4 -pf "/run/dhclient.$dev.pid" -lf "/var/lib/dhcp/dhclient.$dev.leases" "$dev" >>"$LOG" 2>&1
[ -n "$ORIG_GW" ] && ip route replace default via "$ORIG_GW" dev "$dev" 2>/dev/null
printf 'nameserver %s\n' ${ORIG_DNS:-223.5.5.5} > /etc/resolv.conf
sleep 5
snapshot DHCPRAW-after
net_ok
}
restore_dhcp_conf() {
local dev; dev=$(detect_phys)
log WARN "重写 interfaces 为极简 DHCP($dev),重启 networking"
cp -a /etc/network/interfaces "/etc/network/interfaces.netguard-l2" 2>/dev/null
cat > /etc/network/interfaces <<EOF
# netguard fallback $(date '+%F %T')
auto lo
iface lo inet loopback
auto $dev
iface $dev inet dhcp
EOF
mkdir -p /etc/cloud/cloud.cfg.d
printf 'network: {config: disabled}\n' > /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
pkill -f 'dhclient' 2>/dev/null; sleep 2
if command -v ifreload >/dev/null 2>&1; then
timeout 60 ifreload -a >>"$LOG" 2>&1 || true
else
timeout 60 /etc/init.d/networking restart >>"$LOG" 2>&1 || true
fi
sleep 8
snapshot DHCPCONF-after
net_ok
}
do_rollback() {
local why="$1"
log ERROR "判定断网($why),进入回滚编排(IP_MODE=$IP_MODE)"
if [ "$IP_MODE" = "static" ]; then
if restore_static; then
log INFO "静态回写成功,网络恢复"; touch "$STATE_DIR/RECOVERED"; return 0
fi
log WARN "静态回写未恢复,尝试 DHCP 兜底"
if restore_dhcp_raw; then
log INFO "DHCP 兜底成功"; touch "$STATE_DIR/RECOVERED"; return 0
fi
else
if restore_dhcp_raw; then
log INFO "L1(DHCP) 回滚成功,网络恢复"; touch "$STATE_DIR/RECOVERED"; return 0
fi
log WARN "L1 未恢复,进入 L2"
if restore_dhcp_conf; then
log INFO "L2 回滚成功"; touch "$STATE_DIR/RECOVERED"; return 0
fi
log WARN "L2 未恢复,进入 L3 静态写回"
if restore_static; then
log INFO "L3 回滚成功"; touch "$STATE_DIR/RECOVERED"; return 0
fi
fi
log ERROR "回滚全部失败,保留现场,【不重启】。请用 IPMI/iDRAC 带外管理、云控制台 VNC、或机房人工 介入。"
{ echo "ESCALATE $(date '+%F %T') IP_MODE=$IP_MODE"
ip -o addr; ip route; ip -o link; cat /etc/network/interfaces
} > "$ESCALATE"
return 1
}
main_loop() {
local window="$FAIL_WINDOW"
if [ -f /etc/netguard/AUTOARM ] && [ ! -f "$ARMED" ]; then
cmd_arm >/dev/null 2>&1
log INFO "检测到 /etc/netguard/AUTOARM,开机自动武装"
fi
local up_s; up_s=$(cut -d. -f1 /proc/uptime)
if [ "$up_s" -lt 600 ]; then
window="$FAIL_WINDOW_BOOT"
log INFO "开机自检模式:窗口缩短为 ${window}s,先宽限 45s 等网络就绪"
sleep 45
fi
log INFO "netguard 启动,IP_MODE=$IP_MODE,检查间隔 ${CHECK_INTERVAL}s,判死窗口 ${window}s(uptime ${up_s}s)"
while true; do
if [ ! -f "$ARMED" ]; then
sleep 30
window="$FAIL_WINDOW"
continue
fi
if [ -f "$STATE_DIR/HANDLED" ]; then
sleep 30
continue
fi
if net_ok; then
[ -f "$FAILSINCE" ] && log INFO "连通性已恢复,计时清零"
rm -f "$FAILSINCE"
else
if [ ! -f "$FAILSINCE" ]; then
date +%s > "$FAILSINCE"
log WARN "探测失败,开始计时"
else
local since now el
since=$(cat "$FAILSINCE"); now=$(date +%s); el=$((now - since))
log WARN "仍不通,已持续 ${el}s / ${window}s"
if [ "$el" -ge "$window" ]; then
if do_rollback "持续 ${el}s 不通"; then
log INFO "回滚完成,保持常驻继续观察"
fi
touch "$STATE_DIR/HANDLED"
fi
fi
fi
sleep "$CHECK_INTERVAL"
done
}
cmd_arm() {
rm -f "$FAILSINCE" "$STATE_DIR/HANDLED" "$ESCALATE" "$STATE_DIR/RECOVERED"
date +%s > "$ARMED"
log INFO "已武装(armed)"
}
cmd_disarm() { rm -f "$ARMED"; log INFO "已解除武装(disarm),日志保留于 $LOG"; }
cmd_status() {
echo "armed : $([ -f "$ARMED" ] && echo YES || echo no)"
echo "handled : $([ -f "$STATE_DIR/HANDLED" ] && echo YES || echo no)"
echo "recovered : $([ -f "$STATE_DIR/RECOVERED" ] && echo YES || echo no)"
echo "escalate : $([ -f "$ESCALATE" ] && echo YES || echo no)"
echo "fail_since : $(cat "$FAILSINCE" 2>/dev/null || echo -)"
echo "ip_mode : $IP_MODE"
echo "orig : ${ORIG_CIDR:-?} gw ${ORIG_GW:-?}"
echo "phys : $(detect_phys) mac=[$(cat "$STATE_DIR/phys_mac" 2>/dev/null)]"
echo "net_ok : $(net_ok && echo YES || echo NO)"
}
case "${1:-status}" in
run) main_loop ;;
arm) cmd_arm ;;
disarm) cmd_disarm ;;
status) cmd_status ;;
check) net_ok && echo OK || echo FAIL ;;
rollback) do_rollback "manual" ;;
*) echo "usage: $0 {run|arm|disarm|status|check|rollback}" ;;
esac
SCRIPT_EOF
cat > /root/kvm-bridge/netguard.service <<'SCRIPT_EOF'
[Unit]
Description=netguard network dead-man switch
After=network.target
Wants=network.target
[Service]
Type=simple
ExecStart=/usr/local/sbin/netguard.sh run
Restart=always
RestartSec=5
Nice=-10
KillMode=process
TimeoutStopSec=10
[Install]
WantedBy=multi-user.target
SCRIPT_EOF
chmod +x /root/kvm-bridge/netguard.sh
bash -n /root/kvm-bridge/netguard.sh && echo "netguard.sh 语法 OK"
wc -l /root/kvm-bridge/netguard.sh1.2 写入阶段 1 脚本
功能:生成备份加固脚本。四件事 —— ① 全量备份 /etc/network、/etc/hosts、grub 等;
② 自动判定 static / dhcp(四证据:interfaces 写法、dhclient 租约、cloud-init 配置、networkd 配置);
③ 禁掉 cloud-init 管网络 + 修 hosts 回环;④ 写入 /etc/netguard/config 并武装看门狗。
cat > /root/kvm-bridge/01-backup-harden.sh <<'SCRIPT_EOF'
#!/bin/bash
# 阶段 1:备份 + 加固 + 部署看门狗 【完全不修改任何网络配置】
# 自动判定 IP_MODE(static/dhcp)→ 决定后续桥接配置与回滚方向
# 可用 IP_MODE=static 环境变量强制覆盖自动判定
set -u
TS=$(date +%Y%m%d-%H%M%S)
NG=/root/netguard
BK=$NG/backup-$TS
PHYS=$(ip -o -4 route show default | awk '{print $5}' | head -1)
if [ -z "$PHYS" ]; then
PHYS=$(ip -o -4 addr show | awk '$2!="lo"{print $2; exit}')
fi
[ -n "$PHYS" ] || { echo "!! 无法识别物理网卡,中止"; exit 1; }
PMAC=$(cat "/sys/class/net/$PHYS/address")
OIP=$(ip -o -4 addr show dev "$PHYS" | awk '{print $4}' | head -1)
[ -n "$OIP" ] || { echo "!! $PHYS 上没有 IPv4 地址,中止(先确认网卡选对了)"; exit 1; }
OGW=$(ip -o -4 route show default | awk '{print $3}' | head -1)
ODNS=$(awk '/^nameserver/{printf "%s ",$2}' /etc/resolv.conf)
HOST=$(hostname)
echo "### 探测: phys=$PHYS mac=$PMAC ip=$OIP gw=$OGW dns=[$ODNS] host=$HOST"
# ---------- 判定 IP 来源:static 还是 dhcp(分水岭)----------
detect_ip_mode() {
local iponly forced
iponly=${OIP%%/*}
forced="${IP_MODE:-}"
if [ -n "$forced" ]; then echo "$forced"; return; fi
if grep -rqsE "^[[:space:]]*iface[[:space:]]+${PHYS}[[:space:]]+inet[[:space:]]+dhcp" \
/etc/network/interfaces /etc/network/interfaces.d/ 2>/dev/null; then
echo dhcp; return
fi
for lf in /var/lib/dhcp/dhclient*.leases /var/lib/dhcpcd/*.lease; do
[ -f "$lf" ] || continue
if grep -qs "$iponly" "$lf"; then echo dhcp; return; fi
done
for cf in /etc/network/interfaces.d/50-cloud-init /etc/network/interfaces.d/90-cloud-init; do
[ -f "$cf" ] || continue
if grep -qs dhcp "$cf"; then echo dhcp; return; fi
done
if grep -rqs 'DHCP=yes\|DHCP=ipv4\|method=auto' /etc/systemd/network/ 2>/dev/null; then
echo dhcp; return
fi
echo static
}
IP_MODE_DETECTED=$(detect_ip_mode)
echo "### IP 来源判定: IP_MODE=$IP_MODE_DETECTED"
case "$IP_MODE_DETECTED" in
static) echo " → 静态地址直配:回滚策略 = 静态写回(拆桥 + 原 IP 写回物理网卡)" ;;
dhcp) echo " → DHCP 动态地址:回滚策略 = 裸 DHCP 优先(按 MAC 拿回同一地址)" ;;
esac
# ---------- 探测目标自适应 ----------
TARGETS_VAL="icmp:114.114.114.114 icmp:223.5.5.5 icmp:119.29.29.29"
if curl -s -m 4 -o /dev/null http://169.254.0.23/latest/meta-data/instance-id 2>/dev/null; then
TARGETS_VAL="$TARGETS_VAL http:http://169.254.0.23/latest/meta-data/instance-id"
echo "### 云厂商 metadata(169.254.0.23) 可达 → 纳入探测目标"
else
echo "### 无云厂商 metadata → 探测目标仅用公网 DNS(物理机场景正常)"
fi
echo "### TARGETS = $TARGETS_VAL"
# ---------- 全量备份(逐项判存在)----------
mkdir -p "$BK" "$NG"
bk() { if [ -e "$1" ]; then cp -a "$1" "$BK/$2" 2>/dev/null && echo " [备份] $1"; else echo " [跳过] $1(不存在)"; fi; }
echo "### 全量备份 -> $BK"
bk /etc/network network
bk /etc/hosts hosts
bk /etc/resolv.conf resolv.conf
bk /etc/cloud cloud
bk /etc/default/grub grub
bk /etc/fstab fstab
bk /etc/systemd/network systemd-network
bk /etc/netplan netplan
bk /etc/NetworkManager NetworkManager
bk /etc/apt/sources.list sources.list
bk /etc/apt/sources.list.d sources.list.d
{
echo "=== snapshot $TS"
echo "phys=$PHYS mac=$PMAC ip=$OIP gw=$OGW dns=$ODNS host=$HOST ip_mode=$IP_MODE_DETECTED"
echo "--- ip addr"; ip -o addr
echo "--- ip link"; ip -o link show
echo "--- ip route"; ip route
echo "--- cmdline"; cat /proc/cmdline
echo "--- lsblk"; lsblk
echo "--- os"; head -3 /etc/os-release; uname -r
echo "--- pkgs"; dpkg -l
echo "--- crontab"; crontab -l 2>/dev/null
echo "--- cloud.cfg.d"; ls -l /etc/cloud/cloud.cfg.d/ 2>/dev/null
echo "--- 网卡命名策略"; ls -l /etc/systemd/network/*.link 2>/dev/null
echo "--- 是否 ifupdown2"; command -v ifreload || echo "(无 ifreload)"
} > "$BK/SNAPSHOT.txt" 2>&1
echo "$BK" > "$NG/BACKUP_PATH"
echo "$PMAC" > "$NG/ORIG_MAC"
echo "$OIP" > "$NG/ORIG_CIDR"
echo "$OGW" > "$NG/ORIG_GW"
echo "$ODNS" > "$NG/ORIG_DNS"
echo "$PHYS" > "$NG/ORIG_DEV"
# ---------- 加固 A:禁止 cloud-init 覆盖网络 ----------
if [ -d /etc/cloud ]; then
mkdir -p /etc/cloud/cloud.cfg.d
if [ ! -f /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg ]; then
printf 'network: {config: disabled}\n' > /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
echo "### 已写入 99-disable-network-config.cfg"
else
echo "### 99-disable-network-config.cfg 已存在"
fi
cat /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
else
echo "### 无 /etc/cloud(物理机常见),无需处理 cloud-init"
fi
# ---------- 加固 B:主机名解析到非回环 IP ----------
if ! grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[[:space:]]+$HOST" /etc/hosts; then
cp -a /etc/hosts /etc/hosts.netguard-orig
{
echo "127.0.0.1 localhost"
printf '%s\t%s.local %s\n' "${OIP%%/*}" "$HOST" "$HOST"
echo "::1 ip6-localhost ip6-loopback"
echo "fe00::0 ip6-localnet"
echo "ff00::0 ip6-mcastprefix"
echo "ff02::1 ip6-allnodes"
echo "ff02::2 ip6-allrouters"
echo "ff02::3 ip6-allhosts"
} > /etc/hosts
echo "### 已重写 /etc/hosts"
else
echo "### /etc/hosts 已含非回环解析,跳过"
fi
grep -E "$HOST|localhost" /etc/hosts
echo "--- hostname --ip-address => $(hostname --ip-address 2>/dev/null)"
# ---------- 部署看门狗 ----------
install -m 0755 /root/kvm-bridge/netguard.sh /usr/local/sbin/netguard.sh
install -m 0644 /root/kvm-bridge/netguard.service /etc/systemd/system/netguard.service
mkdir -p /etc/netguard
cat > /etc/netguard/config <<EOF
# netguard config ($TS) IP_MODE=$IP_MODE_DETECTED
BACKUP_DIR="$BK"
ORIG_DEV="$PHYS"
ORIG_IP="${OIP%%/*}"
ORIG_CIDR="$OIP"
ORIG_GW="$OGW"
ORIG_DNS="$ODNS"
# static = 物理机公网 IP 直配 → 回滚走"静态写回"
# dhcp = 云主机 NAT 场景 → 回滚走"裸 DHCP 优先"
IP_MODE="$IP_MODE_DETECTED"
TARGETS="$TARGETS_VAL"
MIN_OK=2
FAIL_WINDOW=300
FAIL_WINDOW_BOOT=120
CHECK_INTERVAL=15
EOF
cat /etc/netguard/config
mkdir -p /var/lib/netguard
echo "$PMAC" > /var/lib/netguard/phys_mac
systemctl daemon-reload
systemctl enable netguard.service
systemctl restart netguard.service
sleep 3
# ---------- 切换能力预检 ----------
echo
echo "### ==== 桥接能力预检 ===="
if command -v ifreload >/dev/null 2>&1; then
echo "ifreload : 可用(ifupdown2 已装)→ 阶段4 可零中断增量切换 OK"
else
echo "ifreload : 【不可用】当前是 ifupdown 而非 ifupdown2"
echo " 物理机场景请【先跑阶段 2+3 装 PVE】(会自动换成 ifupdown2),"
echo " 否则阶段 4 的 ifreload -a 会直接失败。"
fi
if [ "$IP_MODE_DETECTED" = "static" ]; then
echo "MAC 提示 : 物理机常见坑 —— 部分机房要求报备 MAC / 交换机端口安全。"
echo " 桥接沿用原 MAC(本方案已强制 hwaddress)通常无碍,"
echo " 但若机房做过 IP-MAC 绑定,动手前先问运维确认。"
fi
if systemctl is-active --quiet systemd-networkd 2>/dev/null; then
echo "警告 : systemd-networkd 正在运行,它可能接管网卡、覆盖 ifupdown 配置。"
fi
echo "备份目录 : $BK"
echo
echo "### ==== 汇总 ===="
systemctl is-active netguard.service && echo "netguard : active"
/usr/local/sbin/netguard.sh check
/usr/local/sbin/netguard.sh arm
/usr/local/sbin/netguard.sh status
echo "### 阶段1 完成,网络未做任何改动"
SCRIPT_EOF
chmod +x /root/kvm-bridge/01-backup-harden.sh
bash -n /root/kvm-bridge/01-backup-harden.sh && echo "01 语法 OK"1.3 执行阶段 1
功能:跑起来。执行完必须逐条核对通过标准,不符合就停下来查。
bash /root/kvm-bridge/01-backup-harden.sh通过标准(三条全中才继续):
阶段 2 · 接入 PVE 源(自动测速选镜像)
功能:只写源 + 预演,不装任何包。脚本会依次给清华、中科大、腾讯、阿里云、官方源各下 5MB 样本测速,
自动选最快的那个写入 /etc/apt/sources.list.d/pve-install-repo.list,然后跑一遍 apt 预演,让你先看清楚会动哪些包。
注意:Range 请求成功返回的是 206 不是 200,脚本两个都认 —— 只认 200 会永远选不出镜像。
cat > /root/kvm-bridge/02-pve-repo.sh <<'SCRIPT_EOF'
#!/bin/bash
# 阶段 2:接入 Proxmox 仓库 —— 镜像源自动测速选优 + apt 预演
set -u
export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_SUSPEND=1
CODE=bookworm
SRC=/etc/apt/sources.list.d/pve-install-repo.list
echo "### 1) 回看备份"
BK=$(cat /root/netguard/BACKUP_PATH 2>/dev/null || echo "")
[ -n "$BK" ] && { ls -l "$BK"; echo "--- 原始 50-cloud-init:"; cat "$BK/network/interfaces.d/50-cloud-init" 2>/dev/null; }
echo
echo "### 2) 测速选源(各下载 5MB 样本)"
MIRRORS="
https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian/pve
https://mirrors.ustc.edu.cn/proxmox/debian/pve
https://mirrors.cloud.tencent.com/proxmox/debian/pve
https://mirrors.aliyun.com/proxmox/debian/pve
http://download.proxmox.com/debian/pve
"
BEST=""; BEST_SPD=0
for m in $MIRRORS; do
r=$(curl -s --max-time 15 -o /dev/null \
-w '%{http_code} %{speed_download}' \
-r 0-5242880 "$m/dists/$CODE/pve-no-subscription/binary-amd64/Packages" 2>/dev/null)
code=$(echo "$r" | awk '{print $1}')
spd=$(echo "$r" | awk '{print int($2)}')
printf ' %-70s code=%s speed=%s B/s\n' "$m" "$code" "${spd:-0}"
# Range 请求成功返回 206 而非 200,两个都算可用
case "$code" in
200|206) if [ "${spd:-0}" -gt "$BEST_SPD" ]; then BEST="$m"; BEST_SPD="$spd"; fi ;;
esac
done
if [ -z "$BEST" ]; then echo "!! 所有镜像源均不可用,请检查网络"; exit 1; fi
echo " => 选用: $BEST ($BEST_SPD B/s)"
echo
echo "### 3) 写入 PVE 源 + 官方签名密钥"
echo "deb [arch=amd64] $BEST $CODE pve-no-subscription" > "$SRC"
cat "$SRC"
wget -q https://enterprise.proxmox.com/debian/proxmox-release-$CODE.gpg \
-O /etc/apt/trusted.gpg.d/proxmox-release-$CODE.gpg
echo "--- 密钥 sha512(期望值见下方)"
sha512sum /etc/apt/trusted.gpg.d/proxmox-release-$CODE.gpg
echo " 期望: 7da6fe34168adc6e479327ba517796d4702fa2f8b4f0a9833f5ea6e6b48f6507a6da403a274fe201595edc86a84463d50383d07f64bdde2e3658108db7d6dc87"
echo
echo "### 4) postfix 非交互预置"
printf 'postfix postfix/main_mailer_type select Local only\n' | debconf-set-selections
printf 'postfix postfix/mailname string %s\n' "$(hostname)" | debconf-set-selections
echo
echo "### 5) apt update"
apt-get update 2>&1 | tail -8
echo
echo "### 6) 候选版本"
apt-cache policy proxmox-ve ifupdown2 proxmox-default-kernel | sed -n '1,40p'
echo
echo "### 7) 预演:会【移除】哪些包(重点看 ifupdown / ntp)"
apt-get -s install -y proxmox-ve postfix open-iscsi chrony 2>&1 | sed -n '/REMOVED/,/^$/p'
echo "### 8) 预演:新装/升级包数量"
apt-get -s install -y proxmox-ve postfix open-iscsi chrony 2>&1 | grep -cE '^Inst' || true
echo "### 9) 预演:ifupdown 相关动作(会 ifupdown -> ifupdown2,属高风险步)"
apt-get -s install -y proxmox-ve postfix open-iscsi chrony 2>&1 | grep -E 'ifupdown' || echo "(无 ifupdown 动作)"
SCRIPT_EOF
chmod +x /root/kvm-bridge/02-pve-repo.sh
bash -n /root/kvm-bridge/02-pve-repo.sh && echo "02 语法 OK"bash /root/kvm-bridge/02-pve-repo.sh通过标准:=> 选用: 后面是 mirrors.tuna.tsinghua.edu.cn(或其他 >500 kB/s 的源);预演里能看到 Remv ifupdown。
阶段 3 · 安装 KVM + Proxmox VE(后台跑,不怕断线)
功能:真正开装。用 setsid nohup 脱离 SSH 会话,即使终端断了安装照样跑完。
约 563 个包,其中 ifupdown → ifupdown2、ntp → chrony(这是唯一会动到网络工具链的一步)。
本阶段网络零改动,SSH 不应断。
cat > /root/kvm-bridge/03-install-pve.sh <<'SCRIPT_EOF'
#!/bin/bash
# 阶段 3:安装 KVM + PVE(detached,脱离 SSH 会话)
set -u
export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_SUSPEND=1
echo "### 当前 PVE 源"; cat /etc/apt/sources.list.d/pve-install-repo.list
echo "### 镜像源可达性"; apt-cache policy proxmox-ve | head -4
cat > /root/netguard/pve-install.sh <<'EOS'
#!/bin/bash
export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_SUSPEND=1
echo "INSTALL_START $(date '+%F %T')"
apt-get -y \
-o Dpkg::Options::=--force-confdef \
-o Dpkg::Options::=--force-confold \
-o Acquire::Retries=3 \
install proxmox-ve postfix open-iscsi chrony
echo "APT_EXIT=$?"
echo "INSTALL_DONE $(date '+%F %T')"
EOS
chmod +x /root/netguard/pve-install.sh
rm -f /var/log/pve-install.log
# setsid + nohup + </dev/null:彻底脱离 SSH 会话,会话断开也不影响安装
setsid nohup /root/netguard/pve-install.sh > /var/log/pve-install.log 2>&1 < /dev/null &
sleep 25
echo "### 后台安装已启动,前 25 秒输出:"
tail -n 25 /var/log/pve-install.log
echo "### 进程:"
pgrep -af 'apt-get|dpkg' | head -5
SCRIPT_EOF
chmod +x /root/kvm-bridge/03-install-pve.sh
bash -n /root/kvm-bridge/03-install-pve.sh && echo "03 语法 OK"bash /root/kvm-bridge/03-install-pve.sh
# 轮询进度(可反复执行,看到 INSTALL_DONE 就停)
for i in $(seq 1 60); do
grep -q INSTALL_DONE /var/log/pve-install.log && break
echo "[$(date '+%T')] 已下载: $(du -sh /var/cache/apt/archives 2>/dev/null | cut -f1)"
sleep 20
done
grep -E 'INSTALL_START|INSTALL_DONE|APT_EXIT' /var/log/pve-install.log
grep -nE '^E:|dpkg: error|Errors were encountered' /var/log/pve-install.log | tail -10通过标准:APT_EXIT=0,且 dpkg: error 一条都没有。此阶段网络零改动,SSH 不应断。
阶段 4 · 切换 br0 桥接(唯一高危步骤)
功能:全场唯一会改网络的一步,也是唯一可能断网的一步。脚本做的事 👇
- 自动探测物理网卡(多网卡时优先跟随默认路由所在的那张,避免挑错)
- 按
IP_MODE生成inet static(含 CIDR→点分掩码转换)或inet dhcp - 强制
hwaddress ether沿用原 MAC —— 这是云机不断网的核心 - 新
interfaces故意不 sourceinterfaces.d/*—— 防止 cloud-init 复活 ifreload -a增量应用,然后自检 75 秒,失败立即触发回滚(不等 5 分钟窗口)
cat > /root/kvm-bridge/04-switch-bridge.sh <<'SCRIPT_EOF'
#!/bin/bash
# 阶段 4:切换到 br0 桥接 【高危 · 目标是零断网】
# 按 /etc/netguard/config 的 IP_MODE 自适应生成 static 或 dhcp 配置
set -u
LOG=/root/netguard/switch.log
mkdir -p /root/netguard
exec >> "$LOG" 2>&1
TS=$(date '+%F %T')
echo
echo "================ SWITCH START $TS ================"
NG=/usr/local/sbin/netguard.sh
# 读阶段1 记录的场景参数
if [ -f /etc/netguard/config ]; then
. /etc/netguard/config
fi
IP_MODE="${IP_MODE:-}"
ORIG_CIDR="${ORIG_CIDR:-}"
ORIG_GW="${ORIG_GW:-}"
ORIG_DNS="${ORIG_DNS:-}"
echo "preflight: IP_MODE=${IP_MODE:-<未设>} ORIG_CIDR=${ORIG_CIDR:-<未设>} ORIG_GW=${ORIG_GW:-<未设>}"
if ! command -v ifreload >/dev/null 2>&1; then
echo "!! 没有 ifreload(需要 ifupdown2)—— 无法做零中断增量切换,中止。"
echo "!! 请先跑阶段 2+3 安装 Proxmox VE(会自动把 ifupdown 换成 ifupdown2)。"
exit 1
fi
# 自动探测物理网卡
DEV=""
for d in /sys/class/net/*; do
d=${d##*/}
case "$d" in lo|br*|vmbr*|tap*|veth*|fw*|bond*) continue ;; esac
[ "$(cat "/sys/class/net/$d/type" 2>/dev/null)" = 1 ] && { DEV=$d; break; }
done
[ -n "$DEV" ] || { echo "!! 未找到物理网卡,中止"; exit 1; }
# 多网卡时以"承载默认路由的那张"为准,避免挑错
DEV_R=$(ip -o -4 route show default | awk '{print $5}' | head -1)
if [ -n "$DEV_R" ] && [ -e "/sys/class/net/$DEV_R" ]; then
case "$DEV_R" in lo|br*|vmbr*|tap*|veth*|fw*|bond*) ;; *) DEV="$DEV_R" ;; esac
fi
MAC=$(cat "/sys/class/net/$DEV/address")
CUR=$(ip -o -4 addr show dev "$DEV" | awk '{print $4}' | head -1)
CURGW=$(ip -o -4 route show default | awk '{print $3}' | head -1)
echo "preflight: dev=$DEV mac=$MAC addr=$CUR gw=$CURGW"
echo "preflight: route => $(ip route | tr '\n' '|')"
echo "preflight: netguard => $($NG status | tr '\n' ' ')"
[ -n "$CUR" ] && ORIG_CIDR="$CUR"
[ -n "$CURGW" ] && ORIG_GW="$CURGW"
cidr2mask() {
local p=$1 o i m=""
for i in 0 1 2 3; do
if [ "$p" -ge 8 ]; then o=255; p=$((p-8))
elif [ "$p" -le 0 ]; then o=0
else o=$((256 - (1 << (8-p)) )); p=0
fi
m="$m.$o"
done
echo "${m#.}"
}
$NG arm
# 停掉会和我们抢网卡的 DHCP 客户端
pkill -f dhclient 2>/dev/null
sleep 2
echo "dhclient after kill: $(pgrep -c -f dhclient 2>/dev/null || echo 0)"
if systemctl is-active --quiet systemd-networkd 2>/dev/null; then
echo "!! 注意:systemd-networkd 仍在运行,可能覆盖配置。若切换失败优先怀疑它。"
fi
# 写桥接配置(故意不 source interfaces.d/*)
cp -a /etc/network/interfaces /root/netguard/interfaces.before-bridge
mv -f /etc/network/interfaces.d/50-cloud-init /root/netguard/50-cloud-init.disabled 2>/dev/null
DNS_LIST="${ORIG_DNS:-119.29.29.29 223.5.5.5}"
{
echo "# Managed by netguard bridge switch $TS IP_MODE=${IP_MODE:-auto}"
echo "# 故意不 source /etc/network/interfaces.d/* —— 防止 cloud-init 重新生成的配置被读取"
echo "auto lo"
echo "iface lo inet loopback"
echo
echo "auto $DEV"
echo "iface $DEV inet manual"
echo
echo "auto br0"
if [ "$IP_MODE" = "static" ]; then
ADDR_ONLY="${ORIG_CIDR%%/*}"
PREFIX="${ORIG_CIDR##*/}"
if [ -z "$ORIG_CIDR" ] || [ "$PREFIX" = "$ORIG_CIDR" ]; then
echo "!! 无法解析静态地址(ORIG_CIDR=$ORIG_CIDR),中止"
exit 1
fi
echo "iface br0 inet static"
echo " address $ADDR_ONLY"
echo " netmask $(cidr2mask "$PREFIX")"
[ -n "$ORIG_GW" ] && echo " gateway $ORIG_GW"
else
echo "iface br0 inet dhcp"
fi
echo " bridge-ports $DEV"
echo " bridge-stp off"
echo " bridge-fd 0"
echo " hwaddress ether $MAC"
echo " dns-nameservers $DNS_LIST"
} > /etc/network/interfaces
echo "--- 新配置 ---"
cat /etc/network/interfaces
echo "--- ifreload -a @ $(date '+%T')"
ifreload -a
RC=$?
echo "--- ifreload rc=$RC @ $(date '+%T')"
# 自检:最多 75 秒
OK=no
for i in $(seq 1 15); do
sleep 5
if [ "$($NG check 2>/dev/null)" = "OK" ]; then OK=yes; break; fi
done
echo "--- 自检结果: $OK @ $(date '+%T')"
{
echo "--- ip -br addr"; ip -br addr
echo "--- ip route"; ip route
echo "--- bridge link"; bridge link 2>/dev/null || ip -o link show master br0
echo "--- resolv.conf"; cat /etc/resolv.conf
echo "--- 出口 IP"
curl -s -m 8 https://ifconfig.me 2>/dev/null; echo
curl -s -m 5 http://169.254.0.23/latest/meta-data/instance-id 2>/dev/null; echo
}
if [ "$OK" = "yes" ]; then
IP=$(ip -o -4 addr show dev br0 | awk '{print $4}')
echo "SUCCESS br0=$IP"
HOSTN=$(hostname)
sed -i -E "s|^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+([[:space:]]+.*${HOSTN}.*)\$|${IP%%/*}\1|" /etc/hosts 2>/dev/null
grep -E "$HOSTN" /etc/hosts || true
else
echo "FAILED —— 立即触发 netguard 回滚(不等 5 分钟窗口)"
$NG rollback
sleep 5
echo "回滚后: $($NG check)"
fi
echo "================ SWITCH END $(date '+%F %T') ================"
SCRIPT_EOF
chmod +x /root/kvm-bridge/04-switch-bridge.sh
bash -n /root/kvm-bridge/04-switch-bridge.sh && echo "04 语法 OK"/usr/local/sbin/netguard.sh status
# detached 执行:即使 SSH 断了,脚本也会跑完并自己回滚
setsid nohup bash /root/kvm-bridge/04-switch-bridge.sh >/dev/null 2>&1 </dev/null &
sleep 20
tail -40 /root/netguard/switch.log通过标准:日志里 SUCCESS br0=<你的原 IP>,且 ifreload rc=0。
阶段 5 · 验证
功能:端到端复核六个区块 —— 网络、外部连通性、Proxmox、配置卫生、看门狗、磁盘。
重点核对 br0 的 MAC 是否等于物理网卡原 MAC、出口公网 IP 是否没变。
echo "=================== 1. 网络 ==================="
ip -br addr
echo "--- 路由 ---"; ip route
echo "--- 桥接关系 ---"; bridge link 2>/dev/null; ip -o link show master br0 2>/dev/null
echo "--- MAC 核对(br0 应等于物理网卡原 MAC)---"
PHYS=$(ip -o -4 route show default | awk '{print $5}' | head -1)
for d in $PHYS br0; do
[ -e "/sys/class/net/$d" ] && echo " $d mac=$(cat /sys/class/net/$d/address)"
done
echo
echo "=================== 2. 外部连通性 ==================="
echo -n "出口公网 IP : "; curl -s -m 8 https://ifconfig.me; echo
echo -n "DNS 解析 : "; getent hosts mirrors.tuna.tsinghua.edu.cn >/dev/null 2>&1 && echo OK || echo FAIL
echo -n "看门狗单测 : "; /usr/local/sbin/netguard.sh check
echo
echo "=================== 3. Proxmox ==================="
pveversion 2>&1 | head -12
echo "--- 8006 监听 ---"; ss -tlnp 2>/dev/null | grep -E '8006|3128' || echo "(未监听)"
echo "--- 服务状态 ---"
for s in pveproxy pvedaemon pvestatd pve-cluster pve-firewall; do
printf ' %-14s %s\n' "$s" "$(systemctl is-active $s 2>&1)"
done
echo "--- 主机名解析(PVE 要求非回环)---"
echo " hostname -f => $(hostname -f)"
echo " hostname -i => $(hostname -i)"
echo
echo "=================== 4. 网络配置卫生 ==================="
echo "--- interfaces.d 内容(应为空/无 cloud-init)---"
ls -A /etc/network/interfaces.d/ 2>/dev/null || echo "(空)"
echo "--- 网络管理工具 ---"
dpkg -l 2>/dev/null | grep -E 'ifupdown|bridge-utils' | awk '{print " "$2, $3}'
echo
echo "=================== 5. 看门狗 ==================="
/usr/local/sbin/netguard.sh status
echo "--- 最近日志(应无 WARN/ERROR)---"
tail -n 15 /var/log/netguard.log 2>/dev/null
echo
echo "=================== 6. 磁盘 ==================="
df -h / /boot 2>/dev/null通过标准:br0 与物理网卡 MAC 一致、出口 IP 不变、pveversion 有输出、8006 在监听、看门狗日志无 WARN/ERROR。
稳定观察后解除武装
功能:确认长期稳定后关掉自动回滚。想保留"重启自动重新武装"的保护,只做 disarm、别删 /etc/netguard/AUTOARM。
# 建议至少观察 15 分钟,确认无异常再执行
/usr/local/sbin/netguard.sh disarm
rm -f /etc/netguard/AUTOARM
/usr/local/sbin/netguard.sh status附 · 出事救援
功能:真出事时的处置命令。优先级:看状态日志 → 手动立即回滚 → 带外(IPMI/VNC)手工恢复。
实时看门狗状态与日志
/usr/local/sbin/netguard.sh status
tail -50 /var/log/netguard.log
tail -50 /root/netguard/switch.log
cat /var/lib/netguard/forensics.log 2>/dev/null | tail -60手动立即回滚(不等 5 分钟窗口)
/usr/local/sbin/netguard.sh rollback从 IPMI / 控制台手工恢复(自动救援全失败时)
# 1) 恢复网络配置为原始状态
cp -a /root/netguard/backup-*/network/interfaces /etc/network/interfaces
# 2) 拆掉桥,让物理网卡自己起来
ip addr flush dev br0 2>/dev/null
ip link set br0 down 2>/dev/null
ip link del br0 2>/dev/null
# 3) 重启网络
systemctl restart networking
# 4) 验证
ip -br addr; ip route; ping -c2 114.114.114.114静态机器临时手工写回(不进 ifupdown,最快恢复连通)
DEV=$(ip -o -4 route show default | awk '{print $5}' | head -1)
ip link set $DEV up
ip addr flush dev $DEV
ip addr add <CIDR> dev $DEV # 例如 203.0.113.5/28
ip route replace default via <网关> dev $DEV
echo "nameserver 223.5.5.5" > /etc/resolv.conf
ip -br addr; ping -c2 114.114.114.114关掉看门狗 / 还原
# 关保护
/usr/local/sbin/netguard.sh disarm
rm -f /etc/netguard/AUTOARM
systemctl disable --now netguard.service
# 还原到未装 PVE 前(谨慎,确认后再做)
# cp -a /root/netguard/backup-*/network/interfaces /etc/network/interfaces
# systemctl restart networking
# 卸载 PVE(激进操作)
# apt-get -y purge proxmox-ve postfix open-iscsi chrony
# apt-get -y install ifupdown && apt-get -y purge ifupdown2
# apt-get -y autoremove📌 一页速记
功能:下次直接照这段跑,严格按序别跳步。
# 阶段顺序(严格按序,别跳)
bash /root/kvm-bridge/01-backup-harden.sh # 备份+加固+武装看门狗(不动网络)
bash /root/kvm-bridge/02-pve-repo.sh # 测速选源+预演
bash /root/kvm-bridge/03-install-pve.sh # 后台装 PVE
bash /root/kvm-bridge/04-switch-bridge.sh # 切桥接(detached 启动)
# 阶段5 直接粘贴上面的验证命令
# 看门狗
/usr/local/sbin/netguard.sh status|check|arm|disarm|rollback
# 唯一安全的改网命令
ifreload -a
# 高危动作前的底线
# 1) netguard status 显示 armed: YES
# 2) 带外通道(VNC / IPMI)已实测可用
# 3) 已抄下原 CIDR / 网关 / DNS⚠️ 最后三句提醒
- 动手前先确认带外通道能用 —— 云主机用控制台 VNC,物理机用 IPMI/iDRAC/iLO。这是所有兜底的底牌。
- 不要照搬任何 IP / MAC / 网关 —— 换机器前先跑阶段 0 重新核对。
- 物理机的
static分支未经真机实测 —— 第一次上物理机,挑一台有 IPMI 的先验证。

💬 评论 (0)
还没有评论,来抢沙发吧。