From bdb77ee2732a78e776a120caf2caabd58c88b341 Mon Sep 17 00:00:00 2001 From: CMGS Date: Wed, 23 Sep 2026 22:43:49 +0800 Subject: [PATCH 1/3] vm: rebuild owned networking before launch when the host lost it A host reboot wipes every netns and TAP while the VM record and disks survive, so every later start of a VM with cocoon-owned networking (CNI, or bridge/tap auto mode) failed before QEMU ran: launch entered the persisted netns and opened the persisted TAP, and nothing rebuilt them. The only way out was rm plus recreate. launch now verifies the owned NIC through the cocoon provider and, when it is missing, re-runs Prepare and Add with AddRecover, the same sequence cocoon's own start uses. The NIC keeps its MAC, the provider's existing network record is reused, and the rebuilt TAP and netns path are written back to the record before it is saved. A failed rebuild fails the launch, since half-built networking strands the guest. The single-queue VMConfig moves into one helper that provisioning and recovery share, so the recovered TAP keeps the queue layout QEMU's -netdev tap expects. --- cmd/vm/lifecycle.go | 3 +++ cmd/vm/net_linux.go | 36 ++++++++++++++++++++++++++++++++++-- cmd/vm/net_linux_test.go | 24 ++++++++++++++++++++++++ cmd/vm/net_other.go | 2 ++ 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/cmd/vm/lifecycle.go b/cmd/vm/lifecycle.go index 37ba541..5b0316c 100644 --- a/cmd/vm/lifecycle.go +++ b/cmd/vm/lifecycle.go @@ -238,6 +238,9 @@ func launch(cmd *cobra.Command, dir string, r *record) error { if err := requireCNIVNCPassword(r.Netns != "", r.VNCDisp, r.VNCPass); err != nil { return err } + if err := recoverNet(cmd, r); err != nil { + return fmt.Errorf("recover network: %w", err) + } if isHostAMD() { // macOS reads MSRs an AMD host lacks; without kvm.ignore_msrs KVM injects #GP (best-effort, host-global) if err := os.WriteFile("/sys/module/kvm/parameters/ignore_msrs", []byte("1\n"), 0o600); err != nil { diff --git a/cmd/vm/net_linux.go b/cmd/vm/net_linux.go index c6b976d..9bcfd68 100644 --- a/cmd/vm/net_linux.go +++ b/cmd/vm/net_linux.go @@ -59,6 +59,11 @@ func newProvider(cmd *cobra.Command, r *record) (network.Network, error) { return nil, fmt.Errorf("unknown --net mode %q (want user|tap|cni|bridge)", r.NetMode) } +// netVMConfig pins CPU=1 so NetNumQueues yields a single-queue TAP matching QEMU's single-queue -netdev tap,ifname= +func netVMConfig(r *record) *types.VMConfig { + return &types.VMConfig{CPU: 1, Name: r.Name} +} + // provisionNet auto-creates a TAP via cocoon — the SAME forwarding plane as cocoon's CH/FC VMs. func provisionNet(cmd *cobra.Command, r *record) (tap, netns, mac string, err error) { provider, err := newProvider(cmd, r) @@ -66,8 +71,7 @@ func provisionNet(cmd *cobra.Command, r *record) (tap, netns, mac string, err er return "", "", "", err } ctx := cliutil.CommandContext(cmd) - // CPU=1 => NetNumQueues yields a single-queue TAP matching QEMU's single-queue -netdev tap,ifname= - vmCfg := &types.VMConfig{CPU: 1, Name: r.Name} + vmCfg := netVMConfig(r) nsPath, err := provider.Prepare(ctx, r.VMID, vmCfg) if err != nil { return "", "", "", fmt.Errorf("prepare network: %w", err) @@ -92,6 +96,34 @@ func provisionNet(cmd *cobra.Command, r *record) (tap, netns, mac string, err er return cfgs[0].TAP, nsPath, mac, nil } +func recoverNet(cmd *cobra.Command, r *record) error { + if !r.TapOwned { + return nil + } + provider, err := newProvider(cmd, r) + if err != nil { + return err + } + ctx := cliutil.CommandContext(cmd) + expected := []*types.NetworkConfig{{TAP: r.Tap, MAC: r.MAC}} + verifyErr := provider.Verify(ctx, r.VMID, expected) + if verifyErr == nil { + return nil + } + log.WithFunc("cmd.vm.recoverNet").Warnf(ctx, "rebuild network for %s: %v", r.VMID, verifyErr) + vmCfg := netVMConfig(r) + nsPath, err := provider.Prepare(ctx, r.VMID, vmCfg) + if err != nil { + return err + } + cfgs, err := provider.Add(ctx, r.VMID, vmCfg, network.AddRecover(expected)...) + if err != nil { + return err + } + r.Tap, r.Netns = cfgs[0].TAP, nsPath + return nil +} + func teardownNet(ctx context.Context, cmd *cobra.Command, r *record) error { if !r.TapOwned { return nil diff --git a/cmd/vm/net_linux_test.go b/cmd/vm/net_linux_test.go index a04d8c6..4a0afe9 100644 --- a/cmd/vm/net_linux_test.go +++ b/cmd/vm/net_linux_test.go @@ -71,3 +71,27 @@ func TestRMRetainsStateWhenNetworkTeardownFails(t *testing.T) { t.Errorf("VM state was removed after teardown failure: %v", err) } } + +func TestStartChecksOwnedNetworkBeforeQEMU(t *testing.T) { + stateDir := t.TempDir() + cmd := newLifecycleTestCommand(t, stateDir) + dir, err := home.VMDir(cmd, "macos-demo") + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + r := &record{ + Name: "macos-demo", VMID: "TESTVMID", Disk: filepath.Join(dir, "disk.qcow2"), + NetMode: "invalid", TapOwned: true, + } + if err := saveRec(dir, r); err != nil { + t.Fatal(err) + } + + err = Start(cmd, []string{"macos-demo"}) + if err == nil || !strings.Contains(err.Error(), "recover network: unknown --net mode") { + t.Fatalf("Start error = %v, want the network recovery failure before qemu launch", err) + } +} diff --git a/cmd/vm/net_other.go b/cmd/vm/net_other.go index 0300911..0fc9dcc 100644 --- a/cmd/vm/net_other.go +++ b/cmd/vm/net_other.go @@ -15,6 +15,8 @@ func provisionNet(_ *cobra.Command, _ *record) (tap, netns, mac string, err erro return "", "", "", fmt.Errorf("auto-create TAP / CNI / bridge networking requires Linux (running on %s); pass a pre-created --tap or use --net user", runtime.GOOS) } +func recoverNet(_ *cobra.Command, _ *record) error { return nil } + func teardownNet(_ context.Context, _ *cobra.Command, _ *record) error { return nil } func toggleNet(_ *cobra.Command, _ *record, _ bool) {} From 3902fa02231eb173ed0931080129a7a696c08e54 Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 00:08:41 +0800 Subject: [PATCH 2/3] fix: pin CNI recovery MAC propagation --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2ab1871..9eb4d9e 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/cocoonstack/cocoon-macos go 1.27.0 require ( - github.com/cocoonstack/cocoon v0.6.9-0.20260923034234-4a3ed8c35796 + github.com/cocoonstack/cocoon v0.6.9-0.20260923160610-396e0a50de11 github.com/gofrs/flock v0.13.0 github.com/opencontainers/image-spec v1.1.1 github.com/projecteru2/core v0.1.3 diff --git a/go.sum b/go.sum index 939c0f8..e4ffa90 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILM github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= github.com/cockroachdb/redact v1.1.8 h1:8eVLLj6juKxiKrAEw2b8cJvNqWq++U8WOfQFuL7KTaA= github.com/cockroachdb/redact v1.1.8/go.mod h1:GceHHpJ0rMDpYARL5In88Alq/xMBUtVlz7Qxix6ZVkw= -github.com/cocoonstack/cocoon v0.6.9-0.20260923034234-4a3ed8c35796 h1:P3b28cTPRfbwKgCBj5swqJhwXYdnEtIC8xrKS/PAARM= -github.com/cocoonstack/cocoon v0.6.9-0.20260923034234-4a3ed8c35796/go.mod h1:B02b6pewrk1DbrRn+iBtDc8LNQWxwot2qF1j4YX5/jg= +github.com/cocoonstack/cocoon v0.6.9-0.20260923160610-396e0a50de11 h1:XHDim0y6VPb9mYVrk631IwilJvVtSeSD4lLApypykjQ= +github.com/cocoonstack/cocoon v0.6.9-0.20260923160610-396e0a50de11/go.mod h1:B02b6pewrk1DbrRn+iBtDc8LNQWxwot2qF1j4YX5/jg= github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= github.com/containernetworking/plugins v1.9.1 h1:8oU6WsIsU3bpnNZuvHp74a6cE1MJwbj2P7s4/yTUNlA= From 821405e48c0c03593ee95dcc8d89afeb62413025 Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 00:33:46 +0800 Subject: [PATCH 3/3] bump --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9eb4d9e..7d8a932 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/cocoonstack/cocoon-macos go 1.27.0 require ( - github.com/cocoonstack/cocoon v0.6.9-0.20260923160610-396e0a50de11 + github.com/cocoonstack/cocoon v0.6.9-0.20260923161722-22b7392e16b0 github.com/gofrs/flock v0.13.0 github.com/opencontainers/image-spec v1.1.1 github.com/projecteru2/core v0.1.3 diff --git a/go.sum b/go.sum index e4ffa90..2015474 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILM github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= github.com/cockroachdb/redact v1.1.8 h1:8eVLLj6juKxiKrAEw2b8cJvNqWq++U8WOfQFuL7KTaA= github.com/cockroachdb/redact v1.1.8/go.mod h1:GceHHpJ0rMDpYARL5In88Alq/xMBUtVlz7Qxix6ZVkw= -github.com/cocoonstack/cocoon v0.6.9-0.20260923160610-396e0a50de11 h1:XHDim0y6VPb9mYVrk631IwilJvVtSeSD4lLApypykjQ= -github.com/cocoonstack/cocoon v0.6.9-0.20260923160610-396e0a50de11/go.mod h1:B02b6pewrk1DbrRn+iBtDc8LNQWxwot2qF1j4YX5/jg= +github.com/cocoonstack/cocoon v0.6.9-0.20260923161722-22b7392e16b0 h1:RJe3v2i/sP1BymsYlJx8+0DqnTRxExvYyi1rslV7tvg= +github.com/cocoonstack/cocoon v0.6.9-0.20260923161722-22b7392e16b0/go.mod h1:B02b6pewrk1DbrRn+iBtDc8LNQWxwot2qF1j4YX5/jg= github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= github.com/containernetworking/plugins v1.9.1 h1:8oU6WsIsU3bpnNZuvHp74a6cE1MJwbj2P7s4/yTUNlA=