From 88f919c4ca16af18ba82969396ec67513550dc30 Mon Sep 17 00:00:00 2001 From: pptx704 Date: Mon, 30 Mar 2026 17:12:05 +0600 Subject: [PATCH] Rename sandbox prefix to cl-, add MMDS metadata, fix proxy port routing - Change sandbox ID prefix from sb- to cl- (capsule) throughout - Fix proxy URL regex character class: base36 uses 0-9a-z, not just hex - Add MMDS V2 config and metadata to VM boot flow so envd can read WRENN_SANDBOX_ID and WRENN_TEMPLATE_ID from inside the guest - Pass TemplateID through VMConfig into both fresh and snapshot boot paths --- internal/api/handler_sandbox_proxy.go | 8 ++++---- internal/id/id.go | 2 +- internal/id/id_test.go | 4 ++-- internal/layout/layout_test.go | 4 ++-- internal/sandbox/manager.go | 2 ++ internal/validate/name_test.go | 2 +- internal/vm/config.go | 6 +++++- internal/vm/fc.go | 25 +++++++++++++++++++++++++ internal/vm/manager.go | 19 +++++++++++++++++++ 9 files changed, 61 insertions(+), 11 deletions(-) diff --git a/internal/api/handler_sandbox_proxy.go b/internal/api/handler_sandbox_proxy.go index 322a559..299aea9 100644 --- a/internal/api/handler_sandbox_proxy.go +++ b/internal/api/handler_sandbox_proxy.go @@ -18,9 +18,9 @@ import ( "git.omukk.dev/wrenn/sandbox/internal/lifecycle" ) -// sandboxHostPattern matches hostnames like "49999-sb-abcd1234.localhost" or -// "49999-sb-abcd1234.example.com". Captures: port, sandbox ID. -var sandboxHostPattern = regexp.MustCompile(`^(\d+)-(sb-[0-9a-f-]+)\.`) +// sandboxHostPattern matches hostnames like "49999-cl-abcd1234.localhost" or +// "49999-cl-abcd1234.example.com". Captures: port, sandbox ID. +var sandboxHostPattern = regexp.MustCompile(`^(\d+)-(cl-[0-9a-z]+)\.`) // SandboxProxyWrapper wraps an existing HTTP handler and intercepts requests // whose Host header matches the {port}-{sandbox_id}.{domain} pattern. Matching @@ -48,7 +48,7 @@ func NewSandboxProxyWrapper(inner http.Handler, queries *db.Queries, pool *lifec func (h *SandboxProxyWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { host := r.Host - // Strip port from Host header (e.g. "49999-sb-abcd1234.localhost:8000" → "49999-sb-abcd1234.localhost") + // Strip port from Host header (e.g. "49999-cl-abcd1234.localhost:8000" → "49999-cl-abcd1234.localhost") if colonIdx := strings.LastIndex(host, ":"); colonIdx != -1 { host = host[:colonIdx] } diff --git a/internal/id/id.go b/internal/id/id.go index 45cba6c..f4b6cdb 100644 --- a/internal/id/id.go +++ b/internal/id/id.go @@ -65,7 +65,7 @@ func NewRefreshToken() string { // --- Formatting (pgtype.UUID → prefixed string for API/RPC output) --- const ( - PrefixSandbox = "sb-" + PrefixSandbox = "cl-" PrefixUser = "usr-" PrefixTeam = "team-" PrefixAPIKey = "key-" diff --git a/internal/id/id_test.go b/internal/id/id_test.go index c16ec7a..6fb2394 100644 --- a/internal/id/id_test.go +++ b/internal/id/id_test.go @@ -46,8 +46,8 @@ func TestFormatParseRoundTrip(t *testing.T) { id := NewSandboxID() formatted := FormatSandboxID(id) - if formatted[:3] != "sb-" { - t.Fatalf("expected sb- prefix, got %s", formatted) + if formatted[:3] != "cl-" { + t.Fatalf("expected cl- prefix, got %s", formatted) } if len(formatted) != 3+base36IDLen { t.Fatalf("expected %d chars total, got %d: %s", 3+base36IDLen, len(formatted), formatted) diff --git a/internal/layout/layout_test.go b/internal/layout/layout_test.go index bffdae2..f7b9afd 100644 --- a/internal/layout/layout_test.go +++ b/internal/layout/layout_test.go @@ -96,8 +96,8 @@ func TestTemplateRootfs(t *testing.T) { } func TestPauseSnapshotDir(t *testing.T) { - got := PauseSnapshotDir("/var/lib/wrenn", "sb-abc123") - want := "/var/lib/wrenn/snapshots/sb-abc123" + got := PauseSnapshotDir("/var/lib/wrenn", "cl-abc123") + want := "/var/lib/wrenn/snapshots/cl-abc123" if got != want { t.Errorf("PauseSnapshotDir() = %q, want %q", got, want) } diff --git a/internal/sandbox/manager.go b/internal/sandbox/manager.go index a8293e6..ac2bc22 100644 --- a/internal/sandbox/manager.go +++ b/internal/sandbox/manager.go @@ -169,6 +169,7 @@ func (m *Manager) Create(ctx context.Context, sandboxID string, teamID, template // Boot VM — Firecracker gets the dm device path. vmCfg := vm.VMConfig{ SandboxID: sandboxID, + TemplateID: id.UUIDString(templateID), KernelPath: layout.KernelPath(m.cfg.WrennDir), RootfsPath: dmDev.DevicePath, VCPUs: vcpus, @@ -1033,6 +1034,7 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, team // Restore VM. vmCfg := vm.VMConfig{ SandboxID: sandboxID, + TemplateID: id.UUIDString(templateID), KernelPath: layout.KernelPath(m.cfg.WrennDir), RootfsPath: dmDev.DevicePath, VCPUs: vcpus, diff --git a/internal/validate/name_test.go b/internal/validate/name_test.go index 4b7769e..f17e210 100644 --- a/internal/validate/name_test.go +++ b/internal/validate/name_test.go @@ -11,7 +11,7 @@ func TestSafeName(t *testing.T) { {"simple", "minimal", false}, {"with-dash", "template-abc123", false}, {"with-dot", "my-snapshot.v2", false}, - {"sandbox-id", "sb-12345678", false}, + {"sandbox-id", "cl-12345678", false}, {"single-char", "a", false}, {"numbers", "123", false}, {"max-length", "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz01", false}, diff --git a/internal/vm/config.go b/internal/vm/config.go index b99480e..0c1f258 100644 --- a/internal/vm/config.go +++ b/internal/vm/config.go @@ -4,9 +4,13 @@ import "fmt" // VMConfig holds the configuration for creating a Firecracker microVM. type VMConfig struct { - // SandboxID is the unique identifier for this sandbox (e.g., "sb-a1b2c3d4"). + // SandboxID is the unique identifier for this sandbox (e.g., "cl-a1b2c3d4"). SandboxID string + // TemplateID is the template UUID string used to populate MMDS metadata + // so that envd can read WRENN_TEMPLATE_ID from inside the guest. + TemplateID string + // KernelPath is the path to the uncompressed Linux kernel (vmlinux). KernelPath string diff --git a/internal/vm/fc.go b/internal/vm/fc.go index b5af5db..3d0f246 100644 --- a/internal/vm/fc.go +++ b/internal/vm/fc.go @@ -101,6 +101,31 @@ func (c *fcClient) setMachineConfig(ctx context.Context, vcpus, memMB int) error }) } +// setMMDSConfig enables MMDS V2 token-based access on the given network interface. +// Must be called before startVM. +func (c *fcClient) setMMDSConfig(ctx context.Context, ifaceID string) error { + return c.do(ctx, http.MethodPut, "/mmds/config", map[string]any{ + "version": "V2", + "network_interfaces": []string{ifaceID}, + }) +} + +// mmdsMetadata is the metadata payload written to the Firecracker MMDS store. +// envd reads this via PollForMMDSOpts to populate WRENN_SANDBOX_ID and WRENN_TEMPLATE_ID. +type mmdsMetadata struct { + SandboxID string `json:"instanceID"` + TemplateID string `json:"envID"` +} + +// setMMDS writes sandbox metadata to the Firecracker MMDS store. +// Can be called after the VM has started. +func (c *fcClient) setMMDS(ctx context.Context, sandboxID, templateID string) error { + return c.do(ctx, http.MethodPut, "/mmds", mmdsMetadata{ + SandboxID: sandboxID, + TemplateID: templateID, + }) +} + // startVM issues the InstanceStart action. func (c *fcClient) startVM(ctx context.Context) error { return c.do(ctx, http.MethodPut, "/actions", map[string]string{ diff --git a/internal/vm/manager.go b/internal/vm/manager.go index c7e3479..9e9466f 100644 --- a/internal/vm/manager.go +++ b/internal/vm/manager.go @@ -71,6 +71,13 @@ func (m *Manager) Create(ctx context.Context, cfg VMConfig) (*VM, error) { return nil, fmt.Errorf("start VM: %w", err) } + // Step 5: Push sandbox metadata into MMDS so envd can read + // WRENN_SANDBOX_ID and WRENN_TEMPLATE_ID from inside the guest. + if err := client.setMMDS(ctx, cfg.SandboxID, cfg.TemplateID); err != nil { + _ = proc.stop() + return nil, fmt.Errorf("set MMDS metadata: %w", err) + } + vm := &VM{ Config: cfg, process: proc, @@ -108,6 +115,12 @@ func configureVM(ctx context.Context, client *fcClient, cfg *VMConfig) error { return fmt.Errorf("set machine config: %w", err) } + // MMDS config — enable V2 token access on eth0 so that envd can read + // WRENN_SANDBOX_ID and WRENN_TEMPLATE_ID from inside the guest. + if err := client.setMMDSConfig(ctx, "eth0"); err != nil { + return fmt.Errorf("set MMDS config: %w", err) + } + return nil } @@ -238,6 +251,12 @@ func (m *Manager) CreateFromSnapshot(ctx context.Context, cfg VMConfig, snapPath return nil, fmt.Errorf("resume VM: %w", err) } + // Step 5: Push sandbox metadata into MMDS. + if err := client.setMMDS(ctx, cfg.SandboxID, cfg.TemplateID); err != nil { + _ = proc.stop() + return nil, fmt.Errorf("set MMDS metadata: %w", err) + } + vm := &VM{ Config: cfg, process: proc,