91 lines
3.7 KiB
Go
91 lines
3.7 KiB
Go
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)")
|