From 14d2192b054f9e93163388b772cf6ab942f5322c Mon Sep 17 00:00:00 2001 From: MengMengCode <227010654+MengMengCode@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:18:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=8D=B8=E8=BD=BD=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E8=A7=84=E5=88=99=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/cli/cli.go | 125 ++++++++++++++++++++++++++++++++ install.sh | 141 +++++++++++++++++++++++++++++++++++- 2 files changed, 265 insertions(+), 1 deletion(-) diff --git a/backend/internal/cli/cli.go b/backend/internal/cli/cli.go index 2238727..f37fa30 100644 --- a/backend/internal/cli/cli.go +++ b/backend/internal/cli/cli.go @@ -842,6 +842,8 @@ func removeKVMDomain(name string) { func cleanupCLICDNetworking() { removeCLICDNATRules() + cleanupCLICDIPv6Runtime() + cleanupCLICDIPv6BridgeRoutes() for _, bridge := range []string{"lxcbr0", "virbr0"} { deleteFilterRule("FORWARD", "-i", bridge, "-j", "ACCEPT") deleteFilterRule("FORWARD", "-o", bridge, "-j", "ACCEPT") @@ -850,6 +852,123 @@ func cleanupCLICDNetworking() { } } +func cleanupCLICDIPv6Runtime() { + if config.AppConfig == nil { + return + } + for _, c := range config.AppConfig.Containers { + cleanupCLICDContainerIPv6(c) + } +} + +func cleanupCLICDContainerIPv6(c config.Container) { + bridge := "lxcbr0" + if c.IsKVM() { + bridge = "virbr0" + } + mac := strings.ToLower(strings.TrimSpace(c.MACAddress)) + if mac != "" && bridge == "virbr0" { + deleteIP6FilterRule("FORWARD", "-i", bridge, "-m", "mac", "--mac-source", mac, "-j", "DROP") + } + if strings.TrimSpace(c.IPv6) == "" { + return + } + + addr := strings.TrimSpace(c.IPv6) + if slash := strings.Index(addr, "/"); slash >= 0 { + addr = addr[:slash] + } + source := strings.TrimSpace(c.IPv6) + if !strings.Contains(source, "/") { + source += "/128" + } + + deleteIP6NATSource(source) + deleteIP6FilterRule("FORWARD", "-i", bridge, "-s", source, "-j", "ACCEPT") + deleteIP6FilterRule("FORWARD", "-o", bridge, "-d", source, "-j", "ACCEPT") + if mac != "" && bridge == "virbr0" { + deleteIP6FilterRule("FORWARD", "-i", bridge, "-m", "mac", "--mac-source", mac, "-s", source, "-j", "ACCEPT") + deleteIP6FilterRule("FORWARD", "-i", bridge, "-m", "mac", "--mac-source", mac, "-j", "DROP") + } + + runQuiet("ip", "-6", "route", "del", source, "dev", bridge) + if strings.TrimSpace(c.IPv6Interface) != "" { + runQuiet("ip", "-6", "neigh", "del", "proxy", addr, "dev", c.IPv6Interface) + } +} + +func cleanupCLICDIPv6BridgeRoutes() { + if !commandExists("ip") { + return + } + for _, bridge := range []string{"lxcbr0", "virbr0"} { + out, err := exec.Command("ip", "-6", "route", "show", "dev", bridge).Output() + if err == nil { + for _, line := range strings.Split(string(out), "\n") { + fields := strings.Fields(line) + if len(fields) == 0 || !strings.HasSuffix(fields[0], "/128") { + continue + } + source := fields[0] + addr := strings.TrimSuffix(source, "/128") + deleteIP6NATSource(source) + deleteIP6FilterRule("FORWARD", "-i", bridge, "-s", source, "-j", "ACCEPT") + deleteIP6FilterRule("FORWARD", "-o", bridge, "-d", source, "-j", "ACCEPT") + removeProxyNDPForAddress(addr) + runQuiet("ip", "-6", "route", "del", source, "dev", bridge) + } + } + runQuiet("ip", "-6", "addr", "del", "fe80::1/64", "dev", bridge) + } +} + +func removeProxyNDPForAddress(addr string) { + out, err := exec.Command("ip", "-6", "neigh", "show", "proxy").Output() + if err != nil { + return + } + for _, line := range strings.Split(string(out), "\n") { + fields := strings.Fields(line) + if len(fields) == 0 || fields[0] != addr { + continue + } + for i := 0; i+1 < len(fields); i++ { + if fields[i] == "dev" { + runQuiet("ip", "-6", "neigh", "del", "proxy", addr, "dev", fields[i+1]) + } + } + } +} + +func deleteIP6NATSource(source string) { + if !commandExists("ip6tables") || strings.TrimSpace(source) == "" { + return + } + for { + out, err := exec.Command("ip6tables", "-t", "nat", "-S", "POSTROUTING").Output() + if err != nil { + return + } + deleted := false + for _, line := range strings.Split(string(out), "\n") { + if !strings.Contains(line, "-s "+source) || !strings.Contains(line, " -j MASQUERADE") { + continue + } + fields := strings.Fields(line) + if len(fields) == 0 || fields[0] != "-A" { + continue + } + fields[0] = "-D" + args := append([]string{"-t", "nat"}, fields...) + deleted = runCommandOK("ip6tables", args...) + break + } + if !deleted { + return + } + } +} + func removeCLICDNATRules() { if commandExists("iptables") { for { @@ -879,6 +998,12 @@ func deleteFilterRule(args ...string) { } } +func deleteIP6FilterRule(args ...string) { + fullArgs := append([]string{"-D"}, args...) + for runCommandOK("ip6tables", fullArgs...) { + } +} + func deleteIP6TablesBridgeRules(bridge string) { if !commandExists("ip6tables") { return diff --git a/install.sh b/install.sh index 1c8303f..a42cb6e 100644 --- a/install.sh +++ b/install.sh @@ -330,6 +330,143 @@ delete_filter_rule() { done } +delete_ip6_filter_rule() { + if ! has_cmd ip6tables; then + return + fi + + while ip6tables -D "$@" >/dev/null 2>&1; do + : + done +} + +delete_ip6tables_nat_source() { + source="$1" + if ! has_cmd ip6tables || [ -z "$source" ]; then + return + fi + + while :; do + rule="$( + ip6tables -t nat -S POSTROUTING 2>/dev/null | + grep -F -- "-s $source" | + grep -F -- " -j MASQUERADE" | + sed 's/^-A /-D /' | + head -n 1 + )" + [ -n "$rule" ] || break + # shellcheck disable=SC2086 + ip6tables -t nat $rule >/dev/null 2>&1 || break + done +} + +read_clicd_network_records() { + db="/root/.clicd/config.db" + legacy="/root/.clicd/config.json" + query="SELECT COALESCE(virtualization,''), COALESCE(ipv6,''), COALESCE(ipv6_interface,''), COALESCE(mac_address,'') FROM containers WHERE COALESCE(ipv6,'') <> '' OR COALESCE(mac_address,'') <> '';" + + if [ -f "$db" ] && has_cmd sqlite3; then + sqlite3 -separator '|' "$db" "$query" 2>/dev/null || true + elif [ -f "$db" ] && has_cmd python3; then + CLICD_DB="$db" python3 - <<'PY' 2>/dev/null || true +import os +import sqlite3 + +db = os.environ.get("CLICD_DB") +for row in sqlite3.connect(db).execute( + "SELECT COALESCE(virtualization,''), COALESCE(ipv6,''), COALESCE(ipv6_interface,''), COALESCE(mac_address,'') " + "FROM containers WHERE COALESCE(ipv6,'') <> '' OR COALESCE(mac_address,'') <> ''" +): + print("|".join("" if value is None else str(value) for value in row)) +PY + fi + + if [ -f "$legacy" ] && has_cmd python3; then + CLICD_LEGACY_CONFIG="$legacy" python3 - <<'PY' 2>/dev/null || true +import json +import os + +path = os.environ.get("CLICD_LEGACY_CONFIG") +with open(path, "r", encoding="utf-8") as f: + data = json.load(f) +for item in data.get("containers", []): + virt = item.get("virtualization", "") + ipv6 = item.get("ipv6", "") + uplink = item.get("ipv6_interface", "") + mac = item.get("mac_address", "") + if ipv6 or mac: + print("|".join(str(value or "") for value in (virt, ipv6, uplink, mac))) +PY + fi +} + +cleanup_clicd_ipv6_record() { + virt="$1" + ipv6="$2" + uplink="$3" + mac="$4" + bridge="lxcbr0" + if [ "$virt" = "kvm" ]; then + bridge="virbr0" + fi + mac="$(printf '%s' "$mac" | tr '[:upper:]' '[:lower:]')" + + if [ -n "$mac" ] && [ "$bridge" = "virbr0" ]; then + delete_ip6_filter_rule FORWARD -i "$bridge" -m mac --mac-source "$mac" -j DROP + fi + + [ -n "$ipv6" ] || return + addr="${ipv6%%/*}" + source="$ipv6" + case "$source" in + */*) ;; + *) source="$source/128" ;; + esac + + delete_ip6tables_nat_source "$source" + delete_ip6_filter_rule FORWARD -i "$bridge" -s "$source" -j ACCEPT + delete_ip6_filter_rule FORWARD -o "$bridge" -d "$source" -j ACCEPT + if [ -n "$mac" ] && [ "$bridge" = "virbr0" ]; then + delete_ip6_filter_rule FORWARD -i "$bridge" -m mac --mac-source "$mac" -s "$source" -j ACCEPT + delete_ip6_filter_rule FORWARD -i "$bridge" -m mac --mac-source "$mac" -j DROP + fi + + if has_cmd ip; then + ip -6 route del "$source" dev "$bridge" >/dev/null 2>&1 || true + if [ -n "$uplink" ]; then + ip -6 neigh del proxy "$addr" dev "$uplink" >/dev/null 2>&1 || true + fi + fi +} + +cleanup_clicd_ipv6_from_config() { + read_clicd_network_records | while IFS='|' read -r virt ipv6 uplink mac; do + cleanup_clicd_ipv6_record "$virt" "$ipv6" "$uplink" "$mac" + done +} + +cleanup_clicd_ipv6_bridge_routes() { + if ! has_cmd ip; then + return + fi + + for bridge in lxcbr0 virbr0; do + ip -6 route show dev "$bridge" 2>/dev/null | awk '$1 ~ /\/128$/ {print $1}' | while IFS= read -r source; do + [ -n "$source" ] || continue + addr="${source%%/*}" + delete_ip6tables_nat_source "$source" + delete_ip6_filter_rule FORWARD -i "$bridge" -s "$source" -j ACCEPT + delete_ip6_filter_rule FORWARD -o "$bridge" -d "$source" -j ACCEPT + ip -6 neigh show proxy 2>/dev/null | awk -v addr="$addr" '$1 == addr {for (i = 1; i < NF; i++) if ($i == "dev") print $(i + 1)}' | while IFS= read -r uplink; do + [ -n "$uplink" ] || continue + ip -6 neigh del proxy "$addr" dev "$uplink" >/dev/null 2>&1 || true + done + ip -6 route del "$source" dev "$bridge" >/dev/null 2>&1 || true + done + ip -6 addr del fe80::1/64 dev "$bridge" >/dev/null 2>&1 || true + done +} + delete_ip6tables_bridge_rules() { if ! has_cmd ip6tables; then return @@ -350,6 +487,8 @@ cleanup_clicd_networking() { delete_iptables_lines nat PREROUTING 'clicd-' delete_iptables_rule nat POSTROUTING -s 10.0.3.0/24 -o eth+ -j MASQUERADE delete_iptables_rule nat POSTROUTING -s 192.168.122.0/24 -o eth+ -j MASQUERADE + cleanup_clicd_ipv6_from_config + cleanup_clicd_ipv6_bridge_routes for bridge in lxcbr0 virbr0; do delete_filter_rule FORWARD -i "$bridge" -j ACCEPT @@ -488,7 +627,7 @@ uninstall_clicd() { echo "=====================================" echo " 已删除服务、二进制、SQLite/配置数据、CLICD LXC/KVM 实例、" echo " CLICD 镜像缓存、防火墙规则、主机钩子、配额记录和临时文件。" - echo " 已保留 /root/clicd-backups 和 LXC 全局缓存,避免误删生产备份/共享镜像。" + echo " 已保留 /root/clicd-backups 和非 CLICD 的 LXC 全局缓存,避免误删生产备份/共享镜像。" echo " 日志:$LOG_FILE" echo " 问题反馈:$ISSUE_URL" echo "====================================="