package network import ( "fmt" "net" "os" "path/filepath" "strings" "time" ) const ( DebianInterfacesPath = "/etc/network/interfaces" DebianVlansCfgPath = "/etc/network/interfaces.d/vlans.cfg" DebianIfacesCfgPath = "/etc/network/interfaces.d/interfaces.cfg" ) // PersistenceManager handles writing Debian network configuration files. type PersistenceManager struct { netMgr *Manager } func NewPersistenceManager(netMgr *Manager) *PersistenceManager { return &PersistenceManager{netMgr: netMgr} } // GenerateDebianConfig creates the contents of the /etc/network/interfaces.d/vlans.cfg file. func (p *PersistenceManager) GenerateDebianConfig() (string, error) { vlans, err := p.netMgr.ListVLANs() if err != nil { return "", err } var sb strings.Builder sb.WriteString(fmt.Sprintf("# ====================================================\n")) sb.WriteString(fmt.Sprintf("# Proxmox Router Appliance - VLANs Configuration\n")) sb.WriteString(fmt.Sprintf("# Generated: %s\n", time.Now().Format(time.RFC1123))) sb.WriteString(fmt.Sprintf("# DO NOT EDIT MANUALLY - Managed via Router Web UI\n")) sb.WriteString(fmt.Sprintf("# ====================================================\n\n")) if len(vlans) == 0 { sb.WriteString("# No VLAN sub-interfaces configured.\n") return sb.String(), nil } for _, v := range vlans { sb.WriteString(fmt.Sprintf("# --- VLAN ID %d (%s) ---\n", v.VlanID, v.Name)) if v.Description != "" { sb.WriteString(fmt.Sprintf("# Description: %s\n", v.Description)) } if v.Role != "" { sb.WriteString(fmt.Sprintf("# Role: %s\n", v.Role)) } sb.WriteString(fmt.Sprintf("auto %s\n", v.Name)) if len(v.IPv4) > 0 { sb.WriteString(fmt.Sprintf("iface %s inet static\n", v.Name)) sb.WriteString(fmt.Sprintf(" address %s\n", v.IPv4[0])) if v.ParentName != "" { sb.WriteString(fmt.Sprintf(" vlan-raw-device %s\n", v.ParentName)) } if v.MTU > 0 && v.MTU != 1500 { sb.WriteString(fmt.Sprintf(" mtu %d\n", v.MTU)) } // additional secondary IPv4 addresses for i := 1; i < len(v.IPv4); i++ { sb.WriteString(fmt.Sprintf(" up ip addr add %s dev %s\n", v.IPv4[i], v.Name)) sb.WriteString(fmt.Sprintf(" down ip addr del %s dev %s\n", v.IPv4[i], v.Name)) } } else { sb.WriteString(fmt.Sprintf("iface %s inet manual\n", v.Name)) if v.ParentName != "" { sb.WriteString(fmt.Sprintf(" vlan-raw-device %s\n", v.ParentName)) } } if len(v.IPv6) > 0 { sb.WriteString(fmt.Sprintf("iface %s inet6 static\n address %s\n", v.Name, v.IPv6[0])) for _, ip := range v.IPv6[1:] { sb.WriteString(fmt.Sprintf(" up ip -6 addr add %s dev %s\n down ip -6 addr del %s dev %s\n", ip, v.Name, ip, v.Name)) } } sb.WriteString("\n") } return sb.String(), nil } // EnsureSourceInclude preserves all existing host configuration. func (p *PersistenceManager) EnsureSourceInclude() error { data, err := os.ReadFile(DebianInterfacesPath) if err != nil && !os.IsNotExist(err) { return err } content := string(data) if p.netMgr != nil && p.netMgr.metadata != nil { content = withManagedAddresses(content, p.netMgr.metadata.GetAllWithIPs()) } return os.WriteFile(DebianInterfacesPath, []byte(withSourceInclude(content)), 0644) } func withSourceInclude(data string) string { for _, line := range strings.Split(data, "\n") { fields := strings.Fields(strings.SplitN(line, "#", 2)[0]) if len(fields) >= 2 && fields[0] == "source" { for _, pattern := range fields[1:] { if pattern == "/etc/network/interfaces.d/*" { return data } } } } if data != "" && !strings.HasSuffix(data, "\n") { data += "\n" } return data + "\n# DebTik managed snippets\nsource /etc/network/interfaces.d/*\n" } func isDynamicOrVirtualInterface(name string) bool { lower := strings.ToLower(name) return strings.HasPrefix(lower, "eoip-") || strings.HasPrefix(lower, "wgx-") || strings.HasPrefix(lower, "wg") || strings.HasPrefix(lower, "aerotun") || strings.HasPrefix(lower, "ppp") || strings.HasPrefix(lower, "gre") } // GenerateInterfacesConfig creates /etc/network/interfaces.d/interfaces.cfg for regular interfaces func (p *PersistenceManager) GenerateInterfacesConfig() (string, error) { ifacesWithIPs := p.netMgr.metadata.GetAllWithIPs() mainData, _ := os.ReadFile(DebianInterfacesPath) existing := declaredInterfaces(string(mainData)) var sb strings.Builder sb.WriteString("# ====================================================\n") sb.WriteString("# Proxmox Router Appliance - Interface IP Configuration\n") sb.WriteString(fmt.Sprintf("# Generated: %s\n", time.Now().Format(time.RFC1123))) sb.WriteString("# DO NOT EDIT MANUALLY - Managed via Router Web UI\n") sb.WriteString("# ====================================================\n\n") if len(ifacesWithIPs) == 0 { sb.WriteString("# No persistent physical interface IPs configured.\n") return sb.String(), nil } for ifaceName, ips := range ifacesWithIPs { if existing[ifaceName] || len(ips) == 0 || strings.HasPrefix(ifaceName, "vlan") || strings.Contains(ifaceName, ".") || isDynamicOrVirtualInterface(ifaceName) { continue // Skip VLANs (handled in vlans.cfg) and daemon-managed virtual/tunnel interfaces } sb.WriteString(fmt.Sprintf("# --- Interface %s ---\n", ifaceName)) sb.WriteString(fmt.Sprintf("auto %s\n", ifaceName)) for _, family := range []string{"inet", "inet6"} { var addresses []string for _, cidr := range ips { ip, _, err := net.ParseCIDR(cidr) if err != nil { return "", err } if (ip.To4() != nil) == (family == "inet") { addresses = append(addresses, cidr) } } if len(addresses) == 0 { continue } sb.WriteString(fmt.Sprintf("iface %s %s static\n address %s\n", ifaceName, family, addresses[0])) for _, ip := range addresses[1:] { sb.WriteString(fmt.Sprintf(" up ip addr replace %s dev %s\n down ip addr del %s dev %s\n", ip, ifaceName, ip, ifaceName)) } } sb.WriteString("\n") } return sb.String(), nil } // SaveDebianConfig writes the configuration to disk. func (p *PersistenceManager) SaveDebianConfig() (string, error) { cfg, err := p.GenerateDebianConfig() if err != nil { return "", err } dir := filepath.Dir(DebianVlansCfgPath) if err := os.MkdirAll(dir, 0755); err != nil { return "", fmt.Errorf("failed to create directory %s: %w", dir, err) } if err := os.WriteFile(DebianVlansCfgPath, []byte(cfg), 0644); err != nil { return "", fmt.Errorf("failed to write %s: %w", DebianVlansCfgPath, err) } ifaceCfg, err := p.GenerateInterfacesConfig() if err != nil { return "", err } if err = os.WriteFile(DebianIfacesCfgPath, []byte(ifaceCfg), 0644); err != nil { return "", err } if err = p.EnsureSourceInclude(); err != nil { return "", err } return cfg, nil } func declaredInterfaces(data string) map[string]bool { result := map[string]bool{} for _, line := range strings.Split(data, "\n") { f := strings.Fields(line) if len(f) >= 4 && f[0] == "iface" { result[f[1]] = true } } return result } func withManagedAddresses(data string, addresses map[string][]string) string { var clean []string skip := false for _, line := range strings.Split(data, "\n") { if strings.TrimSpace(line) == "# BEGIN DEBTIK ADDRESSES" { skip = true continue } if strings.TrimSpace(line) == "# END DEBTIK ADDRESSES" { skip = false continue } if !skip { clean = append(clean, line) } } var out []string seen := map[string]bool{} for _, line := range clean { out = append(out, line) f := strings.Fields(line) if len(f) < 4 || f[0] != "iface" || seen[f[1]] || isDynamicOrVirtualInterface(f[1]) { continue } name := f[1] seen[name] = true if len(addresses[name]) == 0 { continue } out = append(out, " # BEGIN DEBTIK ADDRESSES") for _, cidr := range addresses[name] { if _, _, err := net.ParseCIDR(cidr); err == nil { out = append(out, fmt.Sprintf(" up ip addr replace %s dev %s", cidr, name), fmt.Sprintf(" down ip addr del %s dev %s", cidr, name)) } } out = append(out, " # END DEBTIK ADDRESSES") } return strings.Join(out, "\n") }