package network import ( "fmt" "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)) } } sb.WriteString("\n") } return sb.String(), nil } // EnsureSourceInclude ensures that /etc/network/interfaces contains loopback and "source /etc/network/interfaces.d/*" // while removing duplicate interface stanzas that are managed inside interfaces.d/. func (p *PersistenceManager) EnsureSourceInclude() error { cleanContent := `# The loopback network interface auto lo iface lo inet loopback # Include extra interface configurations managed by DebTik / Proxmox Router source /etc/network/interfaces.d/* ` return os.WriteFile(DebianInterfacesPath, []byte(cleanContent), 0644) } // GenerateInterfacesConfig creates /etc/network/interfaces.d/interfaces.cfg for regular interfaces func (p *PersistenceManager) GenerateInterfacesConfig() (string, error) { ifacesWithIPs := p.netMgr.metadata.GetAllWithIPs() 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 len(ips) == 0 || strings.HasPrefix(ifaceName, "vlan") || strings.Contains(ifaceName, ".") { continue // Skip VLANs (handled in vlans.cfg) } sb.WriteString(fmt.Sprintf("# --- Interface %s ---\n", ifaceName)) sb.WriteString(fmt.Sprintf("auto %s\n", ifaceName)) sb.WriteString(fmt.Sprintf("iface %s inet static\n", ifaceName)) sb.WriteString(fmt.Sprintf(" address %s\n", ips[0])) for i := 1; i < len(ips); i++ { sb.WriteString(fmt.Sprintf(" up ip addr add %s dev %s || true\n", ips[i], ifaceName)) sb.WriteString(fmt.Sprintf(" down ip addr del %s dev %s || true\n", ips[i], 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 { _ = os.WriteFile(DebianIfacesCfgPath, []byte(ifaceCfg), 0644) } _ = p.EnsureSourceInclude() return cfg, nil }