- Load snapshot with ResumeVM: false so MMDS data can be written while VM is paused - Call ResumeVM explicitly after configureMmds succeeds - Skip PUT /mmds/config on restored VMs (Firecracker rejects it with 400) - Strip JSON quotes from MMDS values with tr -d '"' in net-init script - Add 169.254.169.2/32 link-local addr and flush eth0 before applying new IP Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
130 lines
3.9 KiB
Go
130 lines
3.9 KiB
Go
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()
|
|
}
|