first commit

This commit is contained in:
2026-04-13 09:46:02 +03:00
commit 7eaa9750b0
33 changed files with 7357 additions and 0 deletions

86
network/apply.go Normal file
View File

@@ -0,0 +1,86 @@
package network
import (
"fmt"
"os/exec"
"strings"
)
// IfDown brings an interface down via ifdown (or ip link set down as fallback).
func IfDown(name string) error {
out, err := exec.Command("ifdown", "--force", name).CombinedOutput()
if err != nil {
// fallback: ip link set down
out2, err2 := exec.Command("ip", "link", "set", name, "down").CombinedOutput()
if err2 != nil {
return fmt.Errorf("ifdown %s: %s\nip fallback: %s", name, strings.TrimSpace(string(out)), strings.TrimSpace(string(out2)))
}
}
return nil
}
// IfUp brings an interface up via ifup (or ip link set up as fallback).
func IfUp(name string) error {
out, err := exec.Command("ifup", name).CombinedOutput()
if err != nil {
out2, err2 := exec.Command("ip", "link", "set", name, "up").CombinedOutput()
if err2 != nil {
return fmt.Errorf("ifup %s: %s\nip fallback: %s", name, strings.TrimSpace(string(out)), strings.TrimSpace(string(out2)))
}
}
return nil
}
// IfRestart brings an interface down then up.
func IfRestart(name string) error {
_ = IfDown(name) // ignore "already down" errors
return IfUp(name)
}
// ApplyPending merges pending configs into /etc/network/interfaces,
// writes the file, and restarts affected interfaces.
// Returns a per-interface error map (nil key = write error).
func ApplyPending() map[string]error {
errs := map[string]error{}
pending := GetAllPending()
if len(pending) == 0 {
return errs
}
// Read current file config
configs, err := ParseConfig()
if err != nil {
errs["__parse__"] = err
return errs
}
// Merge
for name, cfg := range pending {
configs[name] = cfg
}
// Write file
if err := WriteConfig(configs); err != nil {
errs["__write__"] = err
return errs
}
// Restart each changed interface
for name := range pending {
if name == "lo" {
ClearPendingConfig(name)
continue
}
_ = IfDown(name)
if cfg := configs[name]; cfg != nil && cfg.Auto {
if err := IfUp(name); err != nil {
errs[name] = err
continue
}
}
ClearPendingConfig(name)
}
return errs
}

195
network/config.go Normal file
View File

@@ -0,0 +1,195 @@
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"`
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
if lo, ok := configs["lo"]; ok {
writeStanza(f, lo)
}
for name, cfg := range configs {
if name == "lo" {
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, " "))
}
}
for k, v := range c.Extra {
fmt.Fprintf(f, "\t%s %s\n", k, v)
}
fmt.Fprintln(f)
}

143
network/interfaces.go Normal file
View File

@@ -0,0 +1,143 @@
package network
import (
"bufio"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
)
type InterfaceStats struct {
Name string `json:"name"`
State string `json:"state"` // up, down, unknown
IPv4 string `json:"ipv4"`
IPv4Mask string `json:"ipv4_mask"`
IPv6 []string `json:"ipv6"`
Gateway string `json:"gateway"`
RxBytes uint64 `json:"rx_bytes"`
TxBytes uint64 `json:"tx_bytes"`
RxPackets uint64 `json:"rx_packets"`
TxPackets uint64 `json:"tx_packets"`
Mode string `json:"mode"` // dhcp, static, loopback, manual, unknown
}
// GetInterfaces returns all network interface names from /sys/class/net.
func GetInterfaces() ([]string, error) {
entries, err := os.ReadDir("/sys/class/net")
if err != nil {
return nil, fmt.Errorf("read /sys/class/net: %w", err)
}
names := make([]string, 0, len(entries))
for _, e := range entries {
names = append(names, e.Name())
}
return names, nil
}
// GetInterfaceStats returns current runtime state of an interface.
func GetInterfaceStats(name string) (*InterfaceStats, error) {
s := &InterfaceStats{Name: name, IPv6: []string{}}
// Operational state
if raw, err := os.ReadFile("/sys/class/net/" + name + "/operstate"); err == nil {
s.State = strings.TrimSpace(string(raw))
} else {
s.State = "unknown"
}
// IP addresses
if out, err := exec.Command("ip", "addr", "show", "dev", name).Output(); err == nil {
parseIPAddr(string(out), s)
}
// Default gateway for this interface
if out, err := exec.Command("ip", "route", "show", "dev", name).Output(); err == nil {
parseRoute(string(out), s)
}
// Traffic stats from /proc/net/dev
_ = parseNetDev(name, s)
return s, nil
}
func parseIPAddr(output string, s *InterfaceStats) {
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
switch {
case strings.HasPrefix(line, "inet "):
parts := strings.Fields(line)
if len(parts) < 2 {
continue
}
cidr := parts[1]
if slash := strings.Index(cidr, "/"); slash >= 0 {
s.IPv4 = cidr[:slash]
if prefix, err := strconv.Atoi(cidr[slash+1:]); err == nil {
s.IPv4Mask = prefixToMask(prefix)
}
} else {
s.IPv4 = cidr
}
case strings.HasPrefix(line, "inet6 "):
parts := strings.Fields(line)
if len(parts) >= 2 {
s.IPv6 = append(s.IPv6, parts[1])
}
}
}
}
func parseRoute(output string, s *InterfaceStats) {
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(strings.TrimSpace(line))
// "default via <gw> dev ..."
if len(fields) >= 3 && fields[0] == "default" && fields[1] == "via" {
s.Gateway = fields[2]
return
}
}
}
func parseNetDev(name string, s *InterfaceStats) error {
f, err := os.Open("/proc/net/dev")
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
colon := strings.Index(line, ":")
if colon < 0 {
continue
}
if strings.TrimSpace(line[:colon]) != name {
continue
}
// Fields after colon:
// rx: bytes packets errs drop fifo frame compressed multicast
// tx: bytes packets errs drop fifo colls carrier compressed
fields := strings.Fields(line[colon+1:])
if len(fields) >= 10 {
s.RxBytes, _ = strconv.ParseUint(fields[0], 10, 64)
s.RxPackets, _ = strconv.ParseUint(fields[1], 10, 64)
s.TxBytes, _ = strconv.ParseUint(fields[8], 10, 64)
s.TxPackets, _ = strconv.ParseUint(fields[9], 10, 64)
}
return nil
}
return scanner.Err()
}
func prefixToMask(prefix int) string {
if prefix < 0 || prefix > 32 {
return ""
}
mask := ^uint32(0) << uint(32-prefix)
return fmt.Sprintf("%d.%d.%d.%d",
(mask>>24)&0xFF, (mask>>16)&0xFF, (mask>>8)&0xFF, mask&0xFF)
}