package network import ( "bufio" "fmt" "os" "strings" "sync" ) const ConfigFile = "/etc/network/interfaces" // InterfaceConfig represents one stanza in /etc/network/interfaces. type InterfaceConfig struct { Name string `json:"name"` Label string `json:"label,omitempty"` // display name, stored in config.yaml only Auto bool `json:"auto"` Mode string `json:"mode"` // dhcp, static, loopback, manual Address string `json:"address,omitempty"` // static only Netmask string `json:"netmask,omitempty"` Gateway string `json:"gateway,omitempty"` DNS []string `json:"dns,omitempty"` Extra map[string]string `json:"extra,omitempty"` // other raw options } // --- Pending config store (in-memory, not yet written to disk) --- var ( pendingMu sync.Mutex pending = map[string]*InterfaceConfig{} ) func GetPendingConfig(name string) *InterfaceConfig { pendingMu.Lock() defer pendingMu.Unlock() return pending[name] } func SetPendingConfig(cfg *InterfaceConfig) { pendingMu.Lock() defer pendingMu.Unlock() pending[cfg.Name] = cfg } func ClearPendingConfig(name string) { pendingMu.Lock() defer pendingMu.Unlock() delete(pending, name) } func ClearAllPending() { pendingMu.Lock() defer pendingMu.Unlock() pending = map[string]*InterfaceConfig{} } func GetAllPending() map[string]*InterfaceConfig { pendingMu.Lock() defer pendingMu.Unlock() out := make(map[string]*InterfaceConfig, len(pending)) for k, v := range pending { cp := *v out[k] = &cp } return out } // --- Parse /etc/network/interfaces --- func ParseConfig() (map[string]*InterfaceConfig, error) { f, err := os.Open(ConfigFile) if err != nil { if os.IsNotExist(err) { return map[string]*InterfaceConfig{}, nil } return nil, fmt.Errorf("open %s: %w", ConfigFile, err) } defer f.Close() configs := map[string]*InterfaceConfig{} autoSet := map[string]bool{} var cur *InterfaceConfig scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() // Strip inline comments if idx := strings.Index(line, "#"); idx >= 0 { line = line[:idx] } line = strings.TrimSpace(line) if line == "" { continue } fields := strings.Fields(line) switch fields[0] { case "auto": for _, n := range fields[1:] { autoSet[n] = true } case "iface": // iface if len(fields) < 4 { continue } cur = &InterfaceConfig{ Name: fields[1], Mode: fields[3], Extra: map[string]string{}, } configs[fields[1]] = cur default: if cur == nil || len(fields) < 2 { continue } val := strings.Join(fields[1:], " ") switch fields[0] { case "address": cur.Address = val case "netmask": cur.Netmask = val case "gateway": cur.Gateway = val case "dns-nameservers": cur.DNS = fields[1:] default: cur.Extra[fields[0]] = val } } } if err := scanner.Err(); err != nil { return nil, err } for name, cfg := range configs { cfg.Auto = autoSet[name] } return configs, nil } // --- Write /etc/network/interfaces --- func WriteConfig(configs map[string]*InterfaceConfig) error { // Backup if data, err := os.ReadFile(ConfigFile); err == nil { _ = os.WriteFile(ConfigFile+".bak", data, 0644) } f, err := os.Create(ConfigFile) if err != nil { return fmt.Errorf("create %s: %w", ConfigFile, err) } defer f.Close() // loopback first, then physical interfaces, then VLANs (sorted within each group) if lo, ok := configs["lo"]; ok { writeStanza(f, lo) } for name, cfg := range configs { if name == "lo" || IsVLAN(name) { continue } writeStanza(f, cfg) } for name, cfg := range configs { if !IsVLAN(name) { continue } writeStanza(f, cfg) } return nil } func writeStanza(f *os.File, c *InterfaceConfig) { if c.Auto { fmt.Fprintf(f, "auto %s\n", c.Name) } family := "inet" fmt.Fprintf(f, "iface %s %s %s\n", c.Name, family, c.Mode) if c.Mode == "static" { if c.Address != "" { fmt.Fprintf(f, "\taddress %s\n", c.Address) } if c.Netmask != "" { fmt.Fprintf(f, "\tnetmask %s\n", c.Netmask) } if c.Gateway != "" { fmt.Fprintf(f, "\tgateway %s\n", c.Gateway) } if len(c.DNS) > 0 { fmt.Fprintf(f, "\tdns-nameservers %s\n", strings.Join(c.DNS, " ")) } } // VLAN interfaces need vlan-raw-device unless already in Extra if IsVLAN(c.Name) { if _, ok := c.Extra["vlan-raw-device"]; !ok { fmt.Fprintf(f, "\tvlan-raw-device %s\n", VLANParent(c.Name)) } } for k, v := range c.Extra { fmt.Fprintf(f, "\t%s %s\n", k, v) } fmt.Fprintln(f) }