Files

91 lines
3.7 KiB
Go
Raw Permalink Normal View History

2026-06-22 16:06:57 +02:00
package networking
import (
"context"
"fmt"
"os/exec"
"time"
"nadir/internal/oscmd"
)
// backend is the write-side abstraction. Each host network manager (nmcli,
// networkd, ifupdown) implements this. Reads go through `ip -j` and
// /etc/resolv.conf regardless - they are backend-agnostic (see read.go).
//
// When detect() finds no backend, Module.be is nil and all write endpoints
// return 501 Not Implemented. Reads still work.
// Methods that shell out take a context so a request that is cancelled (client
// disconnect, timeout) kills the slow command (e.g. `nmcli con up` waiting on
// DHCP). The timer-driven auto-revert, which must finish even with no client,
// passes context.Background().
type backend interface {
// Name returns the backend identifier ("nmcli", "networkd", "ifupdown").
Name() string
// Snapshot captures the current IPv4 configuration of iface so it can be
// restored on rollback. The returned IfaceConfig is backend-specific:
// nmcli reads from NM's connection, networkd/ifupdown read their config
// files, falling back to live `ip` output when no managed file exists
// (in which case Method is "dhcp" - safest revert assumption).
Snapshot(ctx context.Context, iface string) (IfaceConfig, error)
// Apply replaces the interface's IPv4 configuration with cfg. It is the
// caller's responsibility to have taken a Snapshot first (the rollback
// mechanism does this). Apply must be idempotent: calling it with the
// same cfg twice should leave the system in the same state.
Apply(ctx context.Context, iface string, cfg IfaceConfig) error
// SetLinkUp brings the interface up.
SetLinkUp(ctx context.Context, iface string) error
// SetLinkDown takes the interface down.
SetLinkDown(ctx context.Context, iface string) error
}
// detect probes the host for a supported network manager, in priority order:
//
// 1. nmcli (NetworkManager) - the majority of desktop and modern server installs
// 2. networkctl (systemd-networkd) - common on minimal/container hosts
// 3. ifup/ifdown (ifupdown) - classic Debian/Ubuntu servers
//
// Returns nil when none is found. The order matters: some distros ship both NM
// and networkd; NM wins because it's the active manager in that case.
func detect() backend {
if _, err := exec.LookPath("nmcli"); err == nil {
if _, err := oscmd.Run("nmcli", "general", "status"); err == nil {
return &nmcliBackend{}
}
}
if _, err := exec.LookPath("networkctl"); err == nil {
if _, err := oscmd.Run("systemctl", "is-active", "--quiet", "systemd-networkd"); err == nil {
return &networkdBackend{}
}
}
if _, err := exec.LookPath("ifup"); err == nil {
if _, err := exec.LookPath("ifdown"); err == nil {
return &ifupdownBackend{}
}
}
return nil
}
// pendingChange tracks a single in-flight change that has been applied but not
// yet confirmed. revert undoes it (re-apply the prior config, or bring a
// downed link back up). If the timer fires before confirmation, revert runs -
// protecting against lock-yourself-out mistakes.
//
// ponytail: one slot for the whole module, not per-interface. An admin makes one
// change at a time; a concurrent change to another iface is rejected with a 409
// that says so. Key it by iface (a map) if multi-interface concurrency is ever
// needed.
type pendingChange struct {
Iface string // interface that was changed
revert func() error // undoes the change, for rollback
Timer *time.Timer // fires the auto-revert
Deadline time.Time // when the timer will fire (for the status endpoint)
}
// errNoBackend is the 501 returned when no write backend was detected.
var errNoBackend = fmt.Errorf("no supported network backend detected (tried nmcli, networkctl, ifup/ifdown)")