修复了一些已知问题

This commit is contained in:
MengMengCode
2026-06-10 10:35:08 +08:00
parent aed512cb09
commit 3cc8f7df7f
4 changed files with 152 additions and 21 deletions
+83 -19
View File
@@ -429,6 +429,7 @@ func (m *Manager) defineContainer(id int, vmName string, cfg lxc.ContainerConfig
ipv6Assignments = assigned ipv6Assignments = assigned
} }
ipv6List := configIPv6AssignmentAddresses(ipv6Assignments) ipv6List := configIPv6AssignmentAddresses(ipv6Assignments)
ipv4List := configIPv4AssignmentAddresses(publicIPv4s)
defaultHostIP := lxc.DefaultPortMappingHostIP(publicIPv4s) defaultHostIP := lxc.DefaultPortMappingHostIP(publicIPv4s)
var xml string var xml string
@@ -448,7 +449,7 @@ func (m *Manager) defineContainer(id int, vmName string, cfg lxc.ContainerConfig
} }
winAdminPassword = generateWindowsPassword() winAdminPassword = generateWindowsPassword()
unattendPath := filepath.Join(m.instanceDir(vmName), "unattend.iso") unattendPath := filepath.Join(m.instanceDir(vmName), "unattend.iso")
if err := createWindowsUnattendISO(unattendPath, cfg.Name, winAdminPassword, ipv6List); err != nil { if err := createWindowsUnattendISO(unattendPath, cfg.Name, winAdminPassword, ipv6List, ipv4List); err != nil {
return nil, err return nil, err
} }
xml = windowsDomainXML(vmName, int(cfg.VCPU), cfg.RAMMB, diskPath, ImagePath(image.ID), unattendPath, mac, cfg.IOSpeedMBps, cfg.NetworkBWMbps) xml = windowsDomainXML(vmName, int(cfg.VCPU), cfg.RAMMB, diskPath, ImagePath(image.ID), unattendPath, mac, cfg.IOSpeedMBps, cfg.NetworkBWMbps)
@@ -464,7 +465,7 @@ func (m *Manager) defineContainer(id int, vmName string, cfg lxc.ContainerConfig
if err := createOverlayDisk(ImagePath(image.ID), diskPath, cfg.DiskGB); err != nil { if err := createOverlayDisk(ImagePath(image.ID), diskPath, cfg.DiskGB); err != nil {
return nil, err return nil, err
} }
if err := createSeedISO(seedPath, vmName, cfg.Name, sshPassword, sshPublicKey, mac, ipv6List, *image); err != nil { if err := createSeedISO(seedPath, vmName, cfg.Name, sshPassword, sshPublicKey, mac, ipv6List, ipv4List, *image); err != nil {
return nil, err return nil, err
} }
xml = domainXML(vmName, int(cfg.VCPU), cfg.RAMMB, diskPath, seedPath, mac, cfg.IOSpeedMBps, cfg.NetworkBWMbps, image.Desktop != "") xml = domainXML(vmName, int(cfg.VCPU), cfg.RAMMB, diskPath, seedPath, mac, cfg.IOSpeedMBps, cfg.NetworkBWMbps, image.Desktop != "")
@@ -1389,7 +1390,7 @@ func (m *Manager) RefreshNetwork(id int) (string, error) {
if changed { if changed {
config.SaveConfig() config.SaveConfig()
} }
if c.Status == "running" && len(c.PortMappings) > 0 && shouldApplyPortMappings(id, changed) { if c.Status == "running" && shouldApplyPortMappings(id, changed) {
if err := lxc.NewManager().ApplyPortMappings(id); err != nil { if err := lxc.NewManager().ApplyPortMappings(id); err != nil {
return ip, err return ip, err
} }
@@ -1596,7 +1597,7 @@ func createEmptyDisk(target string, diskGB int) error {
return nil return nil
} }
func createWindowsUnattendISO(target, hostname, adminPassword string, ipv6s []string) error { func createWindowsUnattendISO(target, hostname, adminPassword string, ipv6s []string, ipv4s []string) error {
tool := firstAvailableCommand("genisoimage", "mkisofs", "xorriso") tool := firstAvailableCommand("genisoimage", "mkisofs", "xorriso")
if tool == "" { if tool == "" {
return fmt.Errorf("one of genisoimage, mkisofs, xorriso is required for Windows unattended setup") return fmt.Errorf("one of genisoimage, mkisofs, xorriso is required for Windows unattended setup")
@@ -1622,13 +1623,13 @@ func createWindowsUnattendISO(target, hostname, adminPassword string, ipv6s []st
if err := os.WriteFile(filepath.Join(setupScriptsDir, "SetupComplete.cmd"), []byte(windowsSetupCompleteCMD()), 0600); err != nil { if err := os.WriteFile(filepath.Join(setupScriptsDir, "SetupComplete.cmd"), []byte(windowsSetupCompleteCMD()), 0600); err != nil {
return err return err
} }
if err := os.WriteFile(filepath.Join(clicdDir, "FirstLogon.ps1"), []byte(windowsFirstLogonPowerShell(adminPassword, ipv6s)), 0600); err != nil { if err := os.WriteFile(filepath.Join(clicdDir, "FirstLogon.ps1"), []byte(windowsFirstLogonPowerShell(adminPassword, ipv6s, ipv4s)), 0600); err != nil {
return err return err
} }
if err := os.WriteFile(filepath.Join(dir, "SetupComplete.cmd"), []byte(windowsSetupCompleteCMD()), 0600); err != nil { if err := os.WriteFile(filepath.Join(dir, "SetupComplete.cmd"), []byte(windowsSetupCompleteCMD()), 0600); err != nil {
return err return err
} }
if err := os.WriteFile(filepath.Join(dir, "FirstLogon.ps1"), []byte(windowsFirstLogonPowerShell(adminPassword, ipv6s)), 0600); err != nil { if err := os.WriteFile(filepath.Join(dir, "FirstLogon.ps1"), []byte(windowsFirstLogonPowerShell(adminPassword, ipv6s, ipv4s)), 0600); err != nil {
return err return err
} }
_ = os.Remove(target) _ = os.Remove(target)
@@ -1738,7 +1739,7 @@ exit /b 0
` `
} }
func windowsFirstLogonPowerShell(adminPassword string, ipv6s []string) string { func windowsFirstLogonPowerShell(adminPassword string, ipv6s []string, ipv4s []string) string {
commands := []string{ commands := []string{
"$ErrorActionPreference='Continue'", "$ErrorActionPreference='Continue'",
"$ProgressPreference='SilentlyContinue'", "$ProgressPreference='SilentlyContinue'",
@@ -1774,6 +1775,12 @@ func windowsFirstLogonPowerShell(adminPassword string, ipv6s []string) string {
windowsIPv6PowerShell(ipv6s), windowsIPv6PowerShell(ipv6s),
) )
} }
ipv4s = normalizeKVMIPv4List(ipv4s)
if len(ipv4s) > 0 {
commands = append(commands,
windowsIPv4PowerShell(ipv4s),
)
}
commands = append(commands, commands = append(commands,
"New-Item -ItemType File -Force -Path 'C:\\CLICD\\init.done' | Out-Null", "New-Item -ItemType File -Force -Path 'C:\\CLICD\\init.done' | Out-Null",
"} finally { Stop-Transcript | Out-Null }", "} finally { Stop-Transcript | Out-Null }",
@@ -1806,11 +1813,47 @@ func windowsIPv6PowerShell(ipv6s []string) string {
}, "\r\n") }, "\r\n")
} }
func windowsIPv4PowerShell(ipv4s []string) string {
ipv4s = normalizeKVMIPv4List(ipv4s)
if len(ipv4s) == 0 {
return ""
}
quoted := make([]string, 0, len(ipv4s))
for _, ipv4 := range ipv4s {
quoted = append(quoted, "'"+strings.ReplaceAll(ipv4, "'", "''")+"'")
}
return strings.Join([]string{
"$clicdIPv4=@(" + strings.Join(quoted, ",") + ")",
"$iface=$null",
"for ($i=0; $i -lt 60 -and -not $iface; $i++) { $iface=Get-NetAdapter | Where-Object { $_.Status -eq 'Up' -and $_.HardwareInterface } | Sort-Object ifIndex | Select-Object -First 1; if (-not $iface) { Start-Sleep -Seconds 5 } }",
"if ($iface) {",
" foreach ($ip in $clicdIPv4) {",
" Get-NetIPAddress -InterfaceIndex $iface.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -eq $ip } | Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue",
" New-NetIPAddress -IPAddress $ip -PrefixLength 32 -InterfaceIndex $iface.ifIndex -SkipAsSource:$false -ErrorAction SilentlyContinue | Out-Null",
" }",
"}",
}, "\r\n")
}
func normalizeKVMIPv4List(values []string) []string {
seen := map[string]bool{}
result := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" || seen[value] {
continue
}
seen[value] = true
result = append(result, value)
}
return result
}
func shellQuoteWindows(value string) string { func shellQuoteWindows(value string) string {
return `"` + strings.ReplaceAll(value, `"`, `\"`) + `"` return `"` + strings.ReplaceAll(value, `"`, `\"`) + `"`
} }
func createSeedISO(seedPath, instanceID, hostname, password, publicKey, mac string, ipv6s []string, image Image) error { func createSeedISO(seedPath, instanceID, hostname, password, publicKey, mac string, ipv6s []string, ipv4s []string, image Image) error {
guestSetup := kvmSSHSetupScript(password, publicKey) guestSetup := kvmSSHSetupScript(password, publicKey)
if desktopSetup := kvmDesktopSetupScript(image); desktopSetup != "" { if desktopSetup := kvmDesktopSetupScript(image); desktopSetup != "" {
guestSetup += "\n" + desktopSetup guestSetup += "\n" + desktopSetup
@@ -1846,29 +1889,40 @@ runcmd:
%s %s
`, hostname, password, authorizedKeys, setupScript) `, hostname, password, authorizedKeys, setupScript)
metaData := fmt.Sprintf("instance-id: %s\nlocal-hostname: %s\n", instanceID, hostname) metaData := fmt.Sprintf("instance-id: %s\nlocal-hostname: %s\n", instanceID, hostname)
ipv6Block := ""
// Build static address block (IPv4 + IPv6)
ipv4s = normalizeKVMIPv4List(ipv4s)
addressBlock := ""
addressLines := make([]string, 0, len(ipv4s)+len(ipv6s))
for _, ipv4 := range ipv4s {
addressLines = append(addressLines, fmt.Sprintf(" - %s/32", ipv4))
}
for _, ipv6 := range ipv6s {
addressLines = append(addressLines, fmt.Sprintf(" - %s/128", ipv6))
}
if len(addressLines) > 0 {
addressBlock = fmt.Sprintf("\n addresses:\n%s", strings.Join(addressLines, "\n"))
}
// IPv6 routes (only needed when IPv6 addresses are configured)
ipv6RouteBlock := ""
if len(ipv6s) > 0 { if len(ipv6s) > 0 {
addressLines := make([]string, 0, len(ipv6s)) ipv6RouteBlock = fmt.Sprintf(`
for _, ipv6 := range ipv6s {
addressLines = append(addressLines, fmt.Sprintf(" - %s/128", ipv6))
}
ipv6Block = fmt.Sprintf(`
addresses:
%s
routes: routes:
- to: default - to: default
via: %s via: %s
on-link: true on-link: true
metric: 100`, strings.Join(addressLines, "\n"), ipv6GatewayLinkLocal) metric: 100`, ipv6GatewayLinkLocal)
} }
networkConfig := fmt.Sprintf(`version: 2 networkConfig := fmt.Sprintf(`version: 2
ethernets: ethernets:
nic0: nic0:
match: match:
macaddress: "%s" macaddress: "%s"
dhcp4: true dhcp4: true
dhcp6: false%s dhcp6: false%s%s
`, strings.ToLower(mac), ipv6Block) `, strings.ToLower(mac), addressBlock, ipv6RouteBlock)
dir := filepath.Dir(seedPath) dir := filepath.Dir(seedPath)
userPath := filepath.Join(dir, "user-data") userPath := filepath.Join(dir, "user-data")
metaPath := filepath.Join(dir, "meta-data") metaPath := filepath.Join(dir, "meta-data")
@@ -1900,6 +1954,16 @@ func configIPv6AssignmentAddresses(assignments []config.IPv6Assignment) []string
return values return values
} }
func configIPv4AssignmentAddresses(assignments []config.PublicIPv4Assignment) []string {
values := make([]string, 0, len(assignments))
for _, item := range assignments {
if strings.TrimSpace(item.Address) != "" {
values = append(values, strings.TrimSpace(item.Address))
}
}
return values
}
func normalizeKVMIPv6List(values []string) []string { func normalizeKVMIPv6List(values []string) []string {
seen := map[string]bool{} seen := map[string]bool{}
result := make([]string, 0, len(values)) result := make([]string, 0, len(values))
+49
View File
@@ -59,11 +59,60 @@ func (m *Manager) ApplyPortMappings(id int) error {
} }
} }
// When container has public IPv4 but no port mappings (independent IP mode),
// ensure inbound DNAT for standard service ports (SSH / RDP).
if len(c.PortMappings) == 0 && len(c.PublicIPv4s) > 0 {
ensureIndependentIPv4Ingress(c, tag)
}
applyIPv4EgressPolicy(c, bridge, subnet, tag) applyIPv4EgressPolicy(c, bridge, subnet, tag)
return nil return nil
} }
func ensureIndependentIPv4Ingress(c *config.Container, tag string) {
if c == nil || c.IP == "" || len(c.PublicIPv4s) == 0 {
return
}
type svcPort struct {
port int
protocol string
desc string
}
servicePorts := []svcPort{{port: 22, protocol: "tcp", desc: "SSH"}}
if strings.Contains(strings.ToLower(c.Template), "windows") {
servicePorts = []svcPort{{port: 3389, protocol: "tcp", desc: "RDP"}}
}
for _, assignment := range c.PublicIPv4s {
hostIP := strings.TrimSpace(assignment.Address)
if hostIP == "" {
continue
}
for _, svc := range servicePorts {
args := []string{
"-t", "nat",
"-I", "PREROUTING", "1",
"-d", hostIP,
"-p", svc.protocol,
"--dport", fmt.Sprintf("%d", svc.port),
"-j", "DNAT",
"--to-destination", fmt.Sprintf("%s:%d", c.IP, svc.port),
"-m", "comment", "--comment", fmt.Sprintf("clicd-%s-%s-%d", tag, natRuleIPTag(hostIP), svc.port),
}
cmd := exec.Command("iptables", args...)
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("Warning: failed to apply %s ingress %s:%d->%s:%d: %v, output: %s\n",
svc.desc, hostIP, svc.port, c.IP, svc.port, err, string(output))
continue
}
fmt.Printf("%s ingress: %s:%d -> %s:%d\n", svc.desc, hostIP, svc.port, c.IP, svc.port)
}
}
}
func applyIPv4EgressPolicy(c *config.Container, bridge, subnet, tag string) { func applyIPv4EgressPolicy(c *config.Container, bridge, subnet, tag string) {
if c == nil || strings.TrimSpace(c.IP) == "" { if c == nil || strings.TrimSpace(c.IP) == "" {
return return
+20 -2
View File
@@ -770,8 +770,26 @@ export default function ContainerDetail() {
if (ipv6List.length === 0 && container.ipv6) ipv6List.push(container.ipv6) if (ipv6List.length === 0 && container.ipv6) ipv6List.push(container.ipv6)
const maxVCPU = hostInfo?.cpu.cores || 64 const maxVCPU = hostInfo?.cpu.cores || 64
const maxRAMMB = hostInfo?.ram.total_mb ? Number(hostInfo.ram.total_mb) : undefined const maxRAMMB = hostInfo?.ram.total_mb ? Number(hostInfo.ram.total_mb) : undefined
const publicEndpoint = container.ssh_port > 0 ? `${publicHost}:${container.ssh_port}` : '-' const hasIndependentIPv4 = assignedIPv4List.length > 0
const sshCommand = container.ssh_port > 0 ? `ssh -p ${container.ssh_port} root@${publicHost}` : '' const hasIndependentIPv6 = ipv6List.length > 0
const hasIndependentIP = hasIndependentIPv4 || hasIndependentIPv6
const defaultConnPort = isWindows ? 3389 : 22
let publicEndpoint = '-'
let sshCommand = ''
if (container.ssh_port > 0) {
// NAT port mapping mode
publicEndpoint = `${publicHost}:${container.ssh_port}`
sshCommand = `ssh -p ${container.ssh_port} root@${publicHost}`
} else if (hasIndependentIP) {
// Direct connection via independent IPv4 or IPv6
const connIP = hasIndependentIPv4 ? assignedIPv4List[0] : `[${ipv6List[0]}]`
publicEndpoint = `${connIP}:${defaultConnPort}`
if (!isWindows) {
sshCommand = `ssh root@${connIP}`
}
}
const editingSSH = draft.index !== null && !!container.port_mappings?.[draft.index] && ( const editingSSH = draft.index !== null && !!container.port_mappings?.[draft.index] && (
container.port_mappings[draft.index].description === 'SSH' || container.port_mappings[draft.index].container_port === 22 || container.port_mappings[draft.index].description === 'SSH' || container.port_mappings[draft.index].container_port === 22 ||
container.port_mappings[draft.index].description === 'RDP' || container.port_mappings[draft.index].container_port === 3389 container.port_mappings[draft.index].description === 'RDP' || container.port_mappings[draft.index].container_port === 3389