package daemon import ( "context" "errors" "fmt" "log/slog" "net" "path/filepath" "strings" "time" "banger/internal/daemon/fcproc" "banger/internal/firecracker" "banger/internal/model" "banger/internal/paths" "banger/internal/system" "banger/internal/vmdns" "banger/internal/vsockagent" ) // HostNetwork owns the daemon's side of host networking: the TAP // interface pool, the bridge, per-VM tap/NAT/DNS wiring, and the // firecracker-process primitives (bridge setup, socket access, // pgrep-based PID resolution, ctrl-alt-del, wait/kill) plus DM // snapshot helpers. The Daemon holds one *HostNetwork and routes // lifecycle calls through it instead of reaching into host-state // directly. // // Fields stay unexported so peer services (VMService, etc.) access // HostNetwork only through consumer-defined interfaces, not by // fishing around in its struct. Construction goes through // newHostNetwork with an explicit dependency bag so the wiring is // auditable. type HostNetwork struct { runner system.CommandRunner logger *slog.Logger config model.DaemonConfig layout paths.Layout closing chan struct{} tapPool tapPool vmDNS *vmdns.Server // Test seams. Default to real implementations at construction; // tests build HostNetwork with stubs instead of mutating package // globals, so parallel tests can't race each other's fake state. lookupExecutable func(name string) (string, error) vmDNSAddr func(server *vmdns.Server) string } // hostNetworkDeps is the explicit wiring bag newHostNetwork expects. // Keeping the deps in a dedicated struct rather than positional args // makes the construction site in Daemon.Open read like a declaration. type hostNetworkDeps struct { runner system.CommandRunner logger *slog.Logger config model.DaemonConfig layout paths.Layout closing chan struct{} } func newHostNetwork(deps hostNetworkDeps) *HostNetwork { return &HostNetwork{ runner: deps.runner, logger: deps.logger, config: deps.config, layout: deps.layout, closing: deps.closing, lookupExecutable: system.LookupExecutable, vmDNSAddr: func(server *vmdns.Server) string { return server.Addr() }, } } // --- DNS server lifecycle ------------------------------------------- func (n *HostNetwork) startVMDNS(addr string) error { server, err := vmdns.New(addr, n.logger) if err != nil { return err } n.vmDNS = server if n.logger != nil { n.logger.Info("vm dns serving", "dns_addr", server.Addr()) } return nil } func (n *HostNetwork) stopVMDNS() error { if n == nil || n.vmDNS == nil { return nil } err := n.vmDNS.Close() n.vmDNS = nil return err } func (n *HostNetwork) setDNS(ctx context.Context, vmName, guestIP string) error { if n.vmDNS == nil { return nil } if err := n.vmDNS.Set(vmdns.RecordName(vmName), guestIP); err != nil { return err } n.ensureVMDNSResolverRouting(ctx) return nil } func (n *HostNetwork) removeDNS(dnsName string) error { if dnsName == "" || n.vmDNS == nil { return nil } return n.vmDNS.Remove(dnsName) } // replaceDNS replaces the DNS server's full record set. Callers // (Daemon.rebuildDNS) filter by vm-alive first; HostNetwork just // takes the pre-filtered map. func (n *HostNetwork) replaceDNS(records map[string]string) error { if n.vmDNS == nil { return nil } return n.vmDNS.Replace(records) } // --- Firecracker process helpers ------------------------------------ // fc builds a fresh fcproc.Manager from the HostNetwork's current // runner, config, and layout. Manager is stateless beyond those // handles, so constructing per call keeps tests that build literals // working without extra wiring. func (n *HostNetwork) fc() *fcproc.Manager { return fcproc.New(n.runner, fcproc.Config{ FirecrackerBin: n.config.FirecrackerBin, BridgeName: n.config.BridgeName, BridgeIP: n.config.BridgeIP, CIDR: n.config.CIDR, RuntimeDir: n.layout.RuntimeDir, }, n.logger) } func (n *HostNetwork) ensureBridge(ctx context.Context) error { return n.fc().EnsureBridge(ctx) } func (n *HostNetwork) ensureSocketDir() error { return n.fc().EnsureSocketDir() } func (n *HostNetwork) createTap(ctx context.Context, tap string) error { return n.fc().CreateTap(ctx, tap) } func (n *HostNetwork) firecrackerBinary() (string, error) { return n.fc().ResolveBinary() } func (n *HostNetwork) ensureSocketAccess(ctx context.Context, socketPath, label string) error { return n.fc().EnsureSocketAccess(ctx, socketPath, label) } func (n *HostNetwork) findFirecrackerPID(ctx context.Context, apiSock string) (int, error) { return n.fc().FindPID(ctx, apiSock) } func (n *HostNetwork) resolveFirecrackerPID(ctx context.Context, machine *firecracker.Machine, apiSock string) int { return n.fc().ResolvePID(ctx, machine, apiSock) } func (n *HostNetwork) sendCtrlAltDel(ctx context.Context, apiSockPath string) error { return n.fc().SendCtrlAltDel(ctx, apiSockPath) } func (n *HostNetwork) waitForExit(ctx context.Context, pid int, apiSock string, timeout time.Duration) error { return n.fc().WaitForExit(ctx, pid, apiSock, timeout) } func (n *HostNetwork) killVMProcess(ctx context.Context, pid int) error { return n.fc().Kill(ctx, pid) } // waitForGuestVSockAgent is a HostNetwork helper because it's // fundamentally about waiting for a vsock socket the firecracker // process is serving on. No daemon state needed. func (n *HostNetwork) waitForGuestVSockAgent(ctx context.Context, socketPath string, timeout time.Duration) error { if strings.TrimSpace(socketPath) == "" { return errors.New("vsock path is required") } waitCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() ticker := time.NewTicker(vsockReadyPoll) defer ticker.Stop() var lastErr error for { pingCtx, pingCancel := context.WithTimeout(waitCtx, 3*time.Second) err := vsockagent.Health(pingCtx, n.logger, socketPath) pingCancel() if err == nil { return nil } lastErr = err select { case <-waitCtx.Done(): if lastErr != nil { return fmt.Errorf("guest vsock agent not ready: %w", lastErr) } return errors.New("guest vsock agent not ready before timeout") case <-ticker.C: } } } // --- Utilities used across networking ------------------------------ func defaultVSockPath(runtimeDir, vmID string) string { return filepath.Join(runtimeDir, "fc-"+system.ShortID(vmID)+".vsock") } func defaultVSockCID(guestIP string) (uint32, error) { ip := net.ParseIP(strings.TrimSpace(guestIP)).To4() if ip == nil { return 0, fmt.Errorf("guest IP is not IPv4: %q", guestIP) } return 10000 + uint32(ip[3]), nil }