package orchestrator import ( "bytes" "context" "encoding/json" "fmt" "io" "net" "net/http" "os/exec" "strings" ) // setupBridge creates the host bridge + enables NAT if it doesn't exist. func (o *Orchestrator) setupBridge() error { if o.cfg.Bridge == "none" { return nil } // check if bridge already exists if err := run("ip", "link", "show", o.cfg.Bridge); err == nil { return nil // already up } if err := run("ip", "link", "add", o.cfg.Bridge, "type", "bridge"); err != nil { return fmt.Errorf("create bridge: %w", err) } if err := run("ip", "addr", "add", o.cfg.BridgeCIDR, "dev", o.cfg.Bridge); err != nil { return fmt.Errorf("add bridge addr: %w", err) } if err := run("ip", "link", "set", o.cfg.Bridge, "up"); err != nil { return fmt.Errorf("bring bridge up: %w", err) } // find default route interface for NAT out, err := exec.Command("ip", "-4", "route", "show", "default").Output() if err == nil { fields := strings.Fields(string(out)) for i, f := range fields { if f == "dev" && i+1 < len(fields) { iface := fields[i+1] _ = run("sysctl", "-qw", "net.ipv4.ip_forward=1") // idempotent: ignore error if rule exists _ = run("iptables", "-t", "nat", "-A", "POSTROUTING", "-o", iface, "-j", "MASQUERADE") break } } } o.log.Infof("bridge %s up on %s", o.cfg.Bridge, o.cfg.BridgeCIDR) return nil } // createTap creates a tap device and attaches it to the bridge. func (o *Orchestrator) createTap(name string) error { if err := run("ip", "tuntap", "add", "dev", name, "mode", "tap"); err != nil { return fmt.Errorf("create tap %s: %w", name, err) } if err := run("ip", "link", "set", name, "up"); err != nil { return fmt.Errorf("bring tap %s up: %w", name, err) } if o.cfg.Bridge != "none" { if err := run("ip", "link", "set", name, "master", o.cfg.Bridge); err != nil { return fmt.Errorf("attach tap %s to bridge: %w", name, err) } } return nil } func destroyTap(name string) { _ = run("ip", "link", "del", name) } // configureMmds writes per-clone IP config to the Firecracker MMDS so that // the fc-net-init daemon running inside the guest can read and apply it. // It makes two API calls to the Firecracker Unix socket: // // 1. PUT /mmds/config — associates MMDS with the guest's first NIC ("1") // 2. PUT /mmds — stores ip/gw/dns values the guest daemon will read func configureMmds(ctx context.Context, sockPath, ip, gw, dns string) error { httpClient := &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { return net.Dial("unix", sockPath) }, }, } doJSON := func(method, path string, body any) error { data, err := json.Marshal(body) if err != nil { return fmt.Errorf("marshal %s: %w", path, err) } req, err := http.NewRequestWithContext(ctx, method, "http://localhost"+path, bytes.NewReader(data)) if err != nil { return fmt.Errorf("build request %s: %w", path, err) } req.Header.Set("Content-Type", "application/json") resp, err := httpClient.Do(req) if err != nil { return fmt.Errorf("%s %s: %w", method, path, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { b, _ := io.ReadAll(resp.Body) return fmt.Errorf("%s %s failed (%d): %s", method, path, resp.StatusCode, b) } return nil } // 1. MMDS configuration (version, network_interfaces binding, etc.) is // persisted in the golden snapshot, so we don't need to configure it here. // In fact, Firecracker will reject PUT /mmds/config with a 400 error // on a restored VM, which previously caused this function to abort early. // 2. Store the network config the guest daemon will poll for. return doJSON(http.MethodPut, "/mmds", map[string]string{ "ip": ip, "gw": gw, "dns": dns, }) } // run executes a command, returning an error if it fails. func run(name string, args ...string) error { return exec.Command(name, args...).Run() }