209 lines
4.6 KiB
Go
209 lines
4.6 KiB
Go
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 <name> <family> <method>
|
|
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)
|
|
}
|