mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
232 lines
6.6 KiB
Go
232 lines
6.6 KiB
Go
package lxc
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
"strconv"
|
|
|
|
"clicd/internal/config"
|
|
)
|
|
|
|
// ApplyPortMappings applies iptables DNAT rules for a container's port mappings
|
|
func (m *Manager) ApplyPortMappings(id int) error {
|
|
c := config.FindContainer(id)
|
|
if c == nil {
|
|
return fmt.Errorf("container not found: %d", id)
|
|
}
|
|
if c.IP == "" {
|
|
return fmt.Errorf("container has no IP")
|
|
}
|
|
tag := clicdTag(id)
|
|
bridge := "lxcbr0"
|
|
subnet := "10.0.3.0/24"
|
|
if c.IsKVM() {
|
|
bridge = "virbr0"
|
|
subnet = "192.168.122.0/24"
|
|
}
|
|
|
|
EnsureForwardRules(bridge)
|
|
m.CleanPortMappings(id)
|
|
|
|
for _, pm := range c.PortMappings {
|
|
cmd := exec.Command("iptables",
|
|
"-t", "nat",
|
|
"-I", "PREROUTING", "1",
|
|
"-p", pm.Protocol,
|
|
"--dport", fmt.Sprintf("%d", pm.HostPort),
|
|
"-j", "DNAT",
|
|
"--to-destination", fmt.Sprintf("%s:%d", c.IP, pm.ContainerPort),
|
|
"-m", "comment", "--comment", fmt.Sprintf("clicd-%s-%d", tag, pm.HostPort),
|
|
)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
fmt.Printf("Warning: failed to apply port mapping %d->%s:%d: %v, output: %s\n",
|
|
pm.HostPort, c.IP, pm.ContainerPort, err, string(output))
|
|
continue
|
|
}
|
|
fmt.Printf("Port mapping: host:%d -> %s:%d\n", pm.HostPort, c.IP, pm.ContainerPort)
|
|
}
|
|
|
|
if exec.Command("iptables", "-t", "nat", "-C", "POSTROUTING", "-s", subnet, "-o", "eth+", "-j", "MASQUERADE").Run() != nil {
|
|
exec.Command("iptables", "-t", "nat", "-I", "POSTROUTING", "1", "-s", subnet, "-o", "eth+", "-j", "MASQUERADE").Run()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func clicdTag(id int) string { return "c" + strconv.Itoa(id) }
|
|
|
|
// EnsureForwardRules makes sure iptables FORWARD chain allows bridge traffic.
|
|
func EnsureForwardRules(bridge string) {
|
|
if bridge == "" {
|
|
bridge = "lxcbr0"
|
|
}
|
|
rules := [][]string{
|
|
{"-i", bridge, "-j", "ACCEPT"},
|
|
{"-o", bridge, "-j", "ACCEPT"},
|
|
{"-i", bridge, "-o", bridge, "-j", "ACCEPT"},
|
|
}
|
|
for _, args := range rules {
|
|
for {
|
|
deleteArgs := append([]string{"-D", "FORWARD"}, args...)
|
|
if exec.Command("iptables", deleteArgs...).Run() != nil {
|
|
break
|
|
}
|
|
}
|
|
insertArgs := append([]string{"-I", "FORWARD", "1"}, args...)
|
|
exec.Command("iptables", insertArgs...).Run()
|
|
}
|
|
}
|
|
|
|
// CleanPortMappings removes all iptables rules for a container
|
|
func (m *Manager) CleanPortMappings(id int) error {
|
|
tag := clicdTag(id)
|
|
cmd := exec.Command("sh", "-c",
|
|
fmt.Sprintf("iptables -t nat -L PREROUTING -n --line-numbers 2>/dev/null | grep 'clicd-%s' | awk '{print $1}' | sort -rn | while read num; do iptables -t nat -D PREROUTING $num; done", tag))
|
|
cmd.Run()
|
|
return nil
|
|
}
|
|
|
|
// SetupDefaultPortMappings creates default port mappings
|
|
func SetupDefaultPortMappings(sshPort int) []config.PortMapping {
|
|
return []config.PortMapping{
|
|
{ContainerPort: 22, HostPort: sshPort, Protocol: "tcp", Description: "SSH"},
|
|
}
|
|
}
|
|
|
|
// AddPortMapping adds a NAT rule to a container
|
|
func (m *Manager) AddPortMapping(id int, pm config.PortMapping) ([]config.PortMapping, error) {
|
|
c := config.FindContainer(id)
|
|
if c == nil {
|
|
return nil, fmt.Errorf("container not found: %d", id)
|
|
}
|
|
if c.PortMappingLimit > 0 && len(c.PortMappings) >= c.PortMappingLimit {
|
|
return nil, fmt.Errorf("port mapping quota exceeded: %d/%d", len(c.PortMappings), c.PortMappingLimit)
|
|
}
|
|
normalized, err := normalizePortMapping(c, -1, pm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.PortMappings = append(c.PortMappings, normalized)
|
|
if err := persistAndReloadMappings(m, c); err != nil {
|
|
return nil, err
|
|
}
|
|
return c.PortMappings, nil
|
|
}
|
|
|
|
// UpdatePortMapping updates an existing NAT rule
|
|
func (m *Manager) UpdatePortMapping(id int, index int, pm config.PortMapping) ([]config.PortMapping, error) {
|
|
c := config.FindContainer(id)
|
|
if c == nil {
|
|
return nil, fmt.Errorf("container not found: %d", id)
|
|
}
|
|
if index < 0 || index >= len(c.PortMappings) {
|
|
return nil, fmt.Errorf("invalid port mapping index: %d", index)
|
|
}
|
|
normalized, err := normalizePortMapping(c, index, pm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.PortMappings[index] = normalized
|
|
if err := persistAndReloadMappings(m, c); err != nil {
|
|
return nil, err
|
|
}
|
|
return c.PortMappings, nil
|
|
}
|
|
|
|
// DeletePortMapping removes a NAT rule
|
|
func (m *Manager) DeletePortMapping(id int, index int) ([]config.PortMapping, error) {
|
|
c := config.FindContainer(id)
|
|
if c == nil {
|
|
return nil, fmt.Errorf("container not found: %d", id)
|
|
}
|
|
if index < 0 || index >= len(c.PortMappings) {
|
|
return nil, fmt.Errorf("invalid port mapping index: %d", index)
|
|
}
|
|
if c.PortMappings[index].Description == "SSH" {
|
|
return nil, fmt.Errorf("SSH default mapping cannot be deleted")
|
|
}
|
|
c.PortMappings = append(c.PortMappings[:index], c.PortMappings[index+1:]...)
|
|
if err := persistAndReloadMappings(m, c); err != nil {
|
|
return nil, err
|
|
}
|
|
return c.PortMappings, nil
|
|
}
|
|
|
|
func persistAndReloadMappings(m *Manager, c *config.Container) error {
|
|
config.SaveConfig()
|
|
if c.Status == "running" && c.IP != "" {
|
|
return m.ApplyPortMappings(c.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizePortMapping(c *config.Container, skipIndex int, pm config.PortMapping) (config.PortMapping, error) {
|
|
if pm.ContainerPort < 1 || pm.ContainerPort > 65535 {
|
|
return pm, fmt.Errorf("container port must be 1-65535")
|
|
}
|
|
if pm.Protocol == "" {
|
|
pm.Protocol = "tcp"
|
|
}
|
|
if pm.Description == "" {
|
|
pm.Description = fmt.Sprintf("Port-%d", pm.ContainerPort)
|
|
}
|
|
if pm.HostPort <= 0 {
|
|
pm.HostPort = pm.ContainerPort
|
|
}
|
|
// Check current container's own mappings
|
|
for i, existing := range c.PortMappings {
|
|
if i == skipIndex {
|
|
continue
|
|
}
|
|
if existing.HostPort == pm.HostPort && existing.Protocol == pm.Protocol {
|
|
return pm, fmt.Errorf("host port %d/%s already mapped in this container", pm.HostPort, pm.Protocol)
|
|
}
|
|
}
|
|
// Check all other containers (LXC + KVM) for port conflicts
|
|
for _, oc := range config.AppConfig.Containers {
|
|
if oc.ID == c.ID {
|
|
continue
|
|
}
|
|
for _, existing := range oc.PortMappings {
|
|
if existing.HostPort == pm.HostPort && existing.Protocol == pm.Protocol {
|
|
return pm, fmt.Errorf("host port %d/%s already used by container %s (ID: %d)", pm.HostPort, pm.Protocol, oc.Name, oc.ID)
|
|
}
|
|
}
|
|
}
|
|
return pm, nil
|
|
}
|
|
|
|
func allocateDefaultEqualPorts(c *config.Container, count int) []int {
|
|
if count <= 0 {
|
|
return nil
|
|
}
|
|
used := map[int]bool{}
|
|
// Mark current container's ports
|
|
for _, pm := range c.PortMappings {
|
|
used[pm.HostPort] = true
|
|
used[pm.ContainerPort] = true
|
|
}
|
|
// Also mark all other containers' host ports (LXC + KVM)
|
|
for _, oc := range config.AppConfig.Containers {
|
|
if oc.ID == c.ID {
|
|
continue
|
|
}
|
|
for _, pm := range oc.PortMappings {
|
|
used[pm.HostPort] = true
|
|
}
|
|
}
|
|
ports := make([]int, 0, count)
|
|
next := 20000
|
|
for len(ports) < count {
|
|
if !used[next] {
|
|
ports = append(ports, next)
|
|
}
|
|
next++
|
|
if next > 65535 || len(ports) >= count {
|
|
break
|
|
}
|
|
}
|
|
return ports
|
|
}
|