From 245c57449c91a910f9f3e6a3ea4a09de4acd6aaa Mon Sep 17 00:00:00 2001 From: MengMengCode <227010654+MengMengCode@users.noreply.github.com> Date: Sun, 7 Jun 2026 09:24:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=AF=E6=8C=81KVM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/kvm/kvm.go | 296 ++++++++++++++++++++------- backend/internal/server/web/.gitkeep | 1 - backend/main.go | 1 + 3 files changed, 225 insertions(+), 73 deletions(-) diff --git a/backend/internal/kvm/kvm.go b/backend/internal/kvm/kvm.go index c2f9f63..25b2af3 100644 --- a/backend/internal/kvm/kvm.go +++ b/backend/internal/kvm/kvm.go @@ -319,7 +319,9 @@ func (m *Manager) StartContainer(id int) error { if status != "running" { cmd := exec.Command("virsh", "start", name) if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("virsh start failed: %v, output: %s", err, string(output)) + if !strings.Contains(strings.ToLower(string(output)), "domain is already active") { + return fmt.Errorf("virsh start failed: %v, output: %s", err, string(output)) + } } } config.UpdateContainerStatus(id, "running") @@ -339,7 +341,14 @@ func (m *Manager) StartContainer(id int) error { if err := lxc.NewManager().ApplyPortMappings(id); err != nil { return err } - return m.EnsureSSH(id) + if c.IPv6 != "" { + if err := m.applyIPv6Runtime(c); err != nil { + return err + } + } else { + ensureKVMIPv6DenyRule("virbr0", c.MACAddress) + } + return nil } func (m *Manager) StopContainer(id int) error { @@ -384,6 +393,7 @@ func (m *Manager) DestroyContainer(id int) error { return fmt.Errorf("container not found: %d", id) } name := c.VirshName() + removeKVMIPv6Runtime(c) _ = m.StopContainer(id) _ = undefineDomain(name) if err := os.RemoveAll(m.instanceDir(name)); err != nil { @@ -447,13 +457,22 @@ func (m *Manager) ResetSSHPassword(id int) (string, error) { if c == nil { return "", fmt.Errorf("container not found: %d", id) } - if c.Status != "running" || c.IP == "" || c.SSHPassword == "" { - return "", fmt.Errorf("KVM VM must be running with SSH ready before password reset") + if c.Status != "running" { + return "", fmt.Errorf("KVM VM must be running before password reset") + } + password := generateRandomString(16) + if err := runKVMGuestAgentSSHSetup(c.VirshName(), password); err == nil { + c.SSHPassword = password + c.SSHHostKey = "" + config.SaveConfig() + return password, nil + } + if c.IP == "" || c.SSHPassword == "" { + return "", fmt.Errorf("KVM guest agent is not ready and saved SSH credentials are unavailable") } if err := m.EnsureSSH(id); err != nil { return "", err } - password := generateRandomString(16) client, err := ssh.Dial("tcp", net.JoinHostPort(c.IP, "22"), &ssh.ClientConfig{ User: "root", Auth: []ssh.AuthMethod{ssh.Password(c.SSHPassword)}, @@ -1012,7 +1031,11 @@ func createOverlayDisk(base, target string, diskGB int) error { } func createSeedISO(seedPath, instanceID, hostname, password, mac, ipv6 string) error { - setupScript := indentScript(kvmSSHSetupScript(password), 4) + guestSetup := kvmSSHSetupScript(password) + if strings.TrimSpace(ipv6) != "" { + guestSetup += "\n" + kvmIPv6SetupScript(ipv6) + } + setupScript := indentScript(guestSetup, 4) userData := fmt.Sprintf(`#cloud-config preserve_hostname: false hostname: %s @@ -1041,6 +1064,7 @@ runcmd: routes: - to: default via: %s + on-link: true metric: 100`, ipv6, ipv6GatewayLinkLocal) } networkConfig := fmt.Sprintf(`version: 2 @@ -1315,7 +1339,10 @@ func runKVMGuestAgentSSHSetup(name string, password string) error { } func runKVMSSHSetup(client *ssh.Client, password string) error { - script := kvmSSHSetupScript(password) + return runKVMSSHScript(client, kvmSSHSetupScript(password), "KVM SSH", 150*time.Second) +} + +func runKVMSSHScript(client *ssh.Client, script string, description string, timeout time.Duration) error { session, err := client.NewSession() if err != nil { return err @@ -1331,12 +1358,12 @@ func runKVMSSHSetup(client *ssh.Client, password string) error { select { case err := <-done: if err != nil { - return fmt.Errorf("failed to configure KVM SSH: %v, output: %s", err, string(output)) + return fmt.Errorf("failed to configure %s: %v, output: %s", description, err, string(output)) } return nil - case <-time.After(150 * time.Second): + case <-time.After(timeout): _ = session.Close() - return fmt.Errorf("timed out configuring KVM SSH after 150s") + return fmt.Errorf("timed out configuring %s after %s", description, timeout) } } @@ -1488,6 +1515,37 @@ func (m *Manager) StartUsageMonitor() { }() } +func (m *Manager) StartIPv6Guard() { + go func() { + m.applyIPv6Guards() + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + for range ticker.C { + m.applyIPv6Guards() + } + }() +} + +func (m *Manager) applyIPv6Guards() { + for i := range config.AppConfig.Containers { + c := &config.AppConfig.Containers[i] + if !c.IsKVM() || c.MACAddress == "" { + continue + } + if c.IPv6 == "" { + ensureKVMIPv6DenyRule("virbr0", c.MACAddress) + continue + } + if err := m.applyIPv6HostRuntime(c); err != nil { + fmt.Printf("Warning: failed to refresh KVM IPv6 guard for %s: %v\n", c.Name, err) + continue + } + if c.Status == "running" { + _ = m.applyGuestIPv6Runtime(c) + } + } +} + func (m *Manager) updateAllRates() { usageMu.Lock() defer usageMu.Unlock() @@ -1836,6 +1894,22 @@ func (m *Manager) AssignIPv6(id int) (*config.Container, error) { } func (m *Manager) applyIPv6Runtime(c *config.Container) error { + if c == nil || c.IPv6 == "" { + return nil + } + if err := m.applyIPv6HostRuntime(c); err != nil { + return err + } + if c.Status == "running" { + if err := m.applyGuestIPv6Runtime(c); err != nil { + fmt.Printf("Warning: failed to apply KVM guest IPv6 for %s: %v\n", c.Name, err) + } + } + ensureKVMIPv6NAT66(c.IPv6, c.IPv6Interface) + return nil +} + +func (m *Manager) applyIPv6HostRuntime(c *config.Container) error { if c == nil || c.IPv6 == "" { return nil } @@ -1861,17 +1935,7 @@ func (m *Manager) applyIPv6Runtime(c *config.Container) error { return fmt.Errorf("failed to add IPv6 proxy NDP: %v, output: %s", err, string(out)) } ensureKVMIPv6ForwardRules(c.IPv6, bridge) - if c.Status == "running" { - if err := m.applyGuestIPv6(c); err != nil { - return err - } - if !m.guestIPv6ConnectivityOK(c) { - ensureKVMIPv6NAT66(c.IPv6, c.IPv6Interface) - if !m.guestIPv6ConnectivityOK(c) { - fmt.Printf("Warning: IPv6 assigned for KVM VM %s, but guest connectivity test did not pass immediately\n", c.Name) - } - } - } + ensureKVMIPv6AntiSpoofRules(c.IPv6, bridge, c.MACAddress) return nil } @@ -1896,6 +1960,29 @@ func ensureKVMIPv6ForwardRules(ipv6 string, bridge string) { } } +func ensureKVMIPv6AntiSpoofRules(ipv6 string, bridge string, mac string) { + if ipv6 == "" || bridge == "" || mac == "" { + return + } + mac = strings.ToLower(strings.TrimSpace(mac)) + acceptRule := []string{"FORWARD", "-i", bridge, "-m", "mac", "--mac-source", mac, "-s", ipv6 + "/128", "-j", "ACCEPT"} + dropRule := []string{"FORWARD", "-i", bridge, "-m", "mac", "--mac-source", mac, "-j", "DROP"} + deleteIP6Rule(acceptRule) + deleteIP6Rule(dropRule) + insertIP6Rule(dropRule) + insertIP6Rule(acceptRule) +} + +func ensureKVMIPv6DenyRule(bridge string, mac string) { + if bridge == "" || mac == "" { + return + } + mac = strings.ToLower(strings.TrimSpace(mac)) + rule := []string{"FORWARD", "-i", bridge, "-m", "mac", "--mac-source", mac, "-j", "DROP"} + deleteIP6Rule(rule) + insertIP6Rule(rule) +} + func ensureKVMIPv6NAT66(ipv6 string, uplink string) { if ipv6 == "" || uplink == "" { return @@ -1908,37 +1995,130 @@ func ensureKVMIPv6NAT66(ipv6 string, uplink string) { } } +func removeKVMIPv6Runtime(c *config.Container) { + if c == nil { + return + } + bridge := "virbr0" + removeKVMIPv6DenyRule(bridge, c.MACAddress) + if c.IPv6 == "" { + return + } + removeKVMIPv6NAT66(c.IPv6, c.IPv6Interface) + removeKVMIPv6ForwardRules(c.IPv6, bridge) + removeKVMIPv6AntiSpoofRules(c.IPv6, bridge, c.MACAddress) + if c.IPv6Interface != "" { + _ = exec.Command("ip", "-6", "neigh", "del", "proxy", c.IPv6, "dev", c.IPv6Interface).Run() + } + _ = exec.Command("ip", "-6", "route", "del", c.IPv6+"/128", "dev", bridge).Run() +} + +func removeKVMIPv6ForwardRules(ipv6 string, bridge string) { + if ipv6 == "" || bridge == "" { + return + } + rules := [][]string{ + {"FORWARD", "-i", bridge, "-s", ipv6 + "/128", "-j", "ACCEPT"}, + {"FORWARD", "-o", bridge, "-d", ipv6 + "/128", "-j", "ACCEPT"}, + } + for _, rule := range rules { + deleteIP6Rule(rule) + } +} + +func removeKVMIPv6AntiSpoofRules(ipv6 string, bridge string, mac string) { + if ipv6 == "" || bridge == "" || mac == "" { + return + } + mac = strings.ToLower(strings.TrimSpace(mac)) + deleteIP6Rule([]string{"FORWARD", "-i", bridge, "-m", "mac", "--mac-source", mac, "-s", ipv6 + "/128", "-j", "ACCEPT"}) + deleteIP6Rule([]string{"FORWARD", "-i", bridge, "-m", "mac", "--mac-source", mac, "-j", "DROP"}) +} + +func removeKVMIPv6DenyRule(bridge string, mac string) { + if bridge == "" || mac == "" { + return + } + mac = strings.ToLower(strings.TrimSpace(mac)) + deleteIP6Rule([]string{"FORWARD", "-i", bridge, "-m", "mac", "--mac-source", mac, "-j", "DROP"}) +} + +func removeKVMIPv6NAT66(ipv6 string, uplink string) { + if ipv6 == "" || uplink == "" { + return + } + rule := []string{"POSTROUTING", "-s", ipv6 + "/128", "-o", uplink, "-j", "MASQUERADE"} + for { + del := append([]string{"-t", "nat", "-D"}, rule...) + if exec.Command("ip6tables", del...).Run() != nil { + return + } + } +} + +func insertIP6Rule(rule []string) { + if len(rule) == 0 { + return + } + add := append([]string{"-I"}, append([]string{rule[0], "1"}, rule[1:]...)...) + _ = exec.Command("ip6tables", add...).Run() +} + +func deleteIP6Rule(rule []string) { + for { + del := append([]string{"-D"}, rule...) + if exec.Command("ip6tables", del...).Run() != nil { + return + } + } +} + func (m *Manager) applyGuestIPv6(c *config.Container) error { if c == nil || c.IPv6 == "" { return nil } script := kvmIPv6SetupScript(c.IPv6) - if c.IP != "" && c.SSHPassword != "" { - client, err := ssh.Dial("tcp", net.JoinHostPort(c.IP, "22"), &ssh.ClientConfig{ - User: "root", - Auth: []ssh.AuthMethod{ssh.Password(c.SSHPassword)}, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - Timeout: 8 * time.Second, - }) - if err == nil { - defer client.Close() - session, err := client.NewSession() - if err != nil { - return err - } - defer session.Close() - if output, err := session.CombinedOutput(script); err != nil { - return fmt.Errorf("failed to apply guest IPv6 over SSH: %v, output: %s", err, string(output)) - } - return nil - } - } if err := qemuGuestPing(c.VirshName()); err != nil { - return fmt.Errorf("failed to apply guest IPv6: SSH unavailable and %w", err) + return err } return qemuGuestExec(c.VirshName(), script, 60*time.Second) } +func (m *Manager) applyGuestIPv6Runtime(c *config.Container) error { + if c == nil || c.IPv6 == "" { + return nil + } + qgaErr := m.applyGuestIPv6(c) + if qgaErr == nil { + return nil + } + sshErr := m.applyGuestIPv6OverSSH(c) + if sshErr == nil { + return nil + } + return fmt.Errorf("QGA: %v; SSH: %v", qgaErr, sshErr) +} + +func (m *Manager) applyGuestIPv6OverSSH(c *config.Container) error { + if c == nil || c.IPv6 == "" { + return nil + } + if c.IP == "" || c.SSHPassword == "" { + return fmt.Errorf("missing guest IPv4 or SSH password") + } + client, err := ssh.Dial("tcp", net.JoinHostPort(c.IP, "22"), &ssh.ClientConfig{ + User: "root", + Auth: []ssh.AuthMethod{ssh.Password(c.SSHPassword)}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 8 * time.Second, + }) + if err != nil { + return err + } + defer client.Close() + return runKVMSSHScript(client, kvmIPv6SetupScript(c.IPv6), "KVM IPv6", 60*time.Second) +} + func kvmIPv6SetupScript(ipv6 string) string { return `set -eu IPV6_ADDR=` + shellQuote(ipv6) + ` @@ -1955,7 +2135,7 @@ sysctl -w net.ipv6.conf.all.disable_ipv6=0 >/dev/null 2>&1 || true sysctl -w net.ipv6.conf.default.disable_ipv6=0 >/dev/null 2>&1 || true sysctl -w net.ipv6.conf."$IFACE".disable_ipv6=0 >/dev/null 2>&1 || true ip -6 addr replace "$IPV6_ADDR/128" dev "$IFACE" -ip -6 route replace default via "$IPV6_GW" dev "$IFACE" metric 100 +ip -6 route replace default via "$IPV6_GW" dev "$IFACE" onlink metric 100 mkdir -p /usr/local/sbin /etc/systemd/system /etc/network/if-up.d /etc/local.d cat > /usr/local/sbin/clicd-kvm-ipv6-init <<'EOF' #!/bin/sh @@ -1971,7 +2151,7 @@ sysctl -w net.ipv6.conf.all.disable_ipv6=0 >/dev/null 2>&1 || true sysctl -w net.ipv6.conf.default.disable_ipv6=0 >/dev/null 2>&1 || true sysctl -w net.ipv6.conf."$IFACE".disable_ipv6=0 >/dev/null 2>&1 || true ip -6 addr replace "$IPV6_ADDR/128" dev "$IFACE" -ip -6 route replace default via "$IPV6_GW" dev "$IFACE" metric 100 +ip -6 route replace default via "$IPV6_GW" dev "$IFACE" onlink metric 100 EOF chmod +x /usr/local/sbin/clicd-kvm-ipv6-init cat > /etc/systemd/system/clicd-kvm-ipv6.service <<'EOF' @@ -2008,34 +2188,6 @@ chmod +x /etc/network/if-up.d/clicd-kvm-ipv6 ` } -func (m *Manager) guestIPv6ConnectivityOK(c *config.Container) bool { - if c == nil || c.IP == "" || c.SSHPassword == "" { - return false - } - client, err := ssh.Dial("tcp", net.JoinHostPort(c.IP, "22"), &ssh.ClientConfig{ - User: "root", - Auth: []ssh.AuthMethod{ssh.Password(c.SSHPassword)}, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - Timeout: 8 * time.Second, - }) - if err != nil { - return false - } - defer client.Close() - for _, target := range []string{"2606:4700:4700::1111", "2001:4860:4860::8888"} { - session, err := client.NewSession() - if err != nil { - continue - } - err = session.Run("ping -6 -c 1 -W 2 " + shellQuote(target)) - _ = session.Close() - if err == nil { - return true - } - } - return false -} - func (m *Manager) allocateIPv6ForContainer(id int) (string, int, string, error) { prefixes := lxc.DetectPublicIPv6Prefixes() if len(prefixes) == 0 { diff --git a/backend/internal/server/web/.gitkeep b/backend/internal/server/web/.gitkeep index 8b13789..e69de29 100644 --- a/backend/internal/server/web/.gitkeep +++ b/backend/internal/server/web/.gitkeep @@ -1 +0,0 @@ - diff --git a/backend/main.go b/backend/main.go index b2da22b..542fced 100644 --- a/backend/main.go +++ b/backend/main.go @@ -64,6 +64,7 @@ func main() { // Start usage monitors (computes CPU/network/disk rates every 5s) manager.StartUsageMonitor() kvmManager.StartUsageMonitor() + kvmManager.StartIPv6Guard() // Start scheduled snapshot scanners. manager.StartSnapshotScheduler()