Files
alpine-router/network/config.go

209 lines
4.6 KiB
Go
Raw Normal View History

2026-04-13 09:46:02 +03:00
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"`
2026-04-13 12:40:49 +03:00
Label string `json:"label,omitempty"` // display name, stored in config.yaml only
2026-04-13 09:46:02 +03:00
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()
2026-04-13 12:40:49 +03:00
// loopback first, then physical interfaces, then VLANs (sorted within each group)
2026-04-13 09:46:02 +03:00
if lo, ok := configs["lo"]; ok {
writeStanza(f, lo)
}
for name, cfg := range configs {
2026-04-13 12:40:49 +03:00
if name == "lo" || IsVLAN(name) {
continue
}
writeStanza(f, cfg)
}
for name, cfg := range configs {
if !IsVLAN(name) {
2026-04-13 09:46:02 +03:00
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, " "))
}
}
2026-04-13 12:40:49 +03:00
// 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))
}
}
2026-04-13 09:46:02 +03:00
for k, v := range c.Extra {
fmt.Fprintf(f, "\t%s %s\n", k, v)
}
fmt.Fprintln(f)
}