Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/vm/datadisk.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func copyDataDisks(dir string, src []string) ([]string, error) {
paths := make([]string, 0, len(src))
for _, srcPath := range src {
dst := filepath.Join(dir, filepath.Base(srcPath))
if err := utils.ReflinkCopy(dst, srcPath); err != nil {
if err := utils.ReflinkCopy(dst, srcPath, utils.Sync); err != nil {
return nil, fmt.Errorf("copy data disk %s: %w", filepath.Base(srcPath), err)
}
paths = append(paths, dst)
Expand Down
2 changes: 2 additions & 0 deletions cmd/vm/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func (h *Handler) Start(cmd *cobra.Command, args []string) error {
if err := h.launch(cmd, dir, r); err != nil {
return err
}
unquiesceNet(cmd, r)
fmt.Printf("%s (pid %d)\n", n, r.PID)
return nil
}); err != nil {
Expand All @@ -85,6 +86,7 @@ func (h *Handler) Stop(cmd *cobra.Command, args []string) error {
return err
}
terminate(ctx, r, grace)
quiesceNet(cmd, r)
stopVNCProxy(ctx, dir)
r.PID, r.VNCDisp, r.VNCPass = 0, -1, "" // VNC is launch-scoped: gone with the qemu it belonged to
return saveRec(dir, r)
Expand Down
65 changes: 63 additions & 2 deletions cmd/vm/net_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/projecteru2/core/log"
"github.com/spf13/cobra"
"github.com/vishvananda/netlink"

"github.com/cocoonstack/cocoon/cmd/cliutil"
"github.com/cocoonstack/cocoon/config"
Expand Down Expand Up @@ -83,8 +84,15 @@ func teardownNet(cmd *cobra.Command, r *record) {
// warn instead of failing: rm must proceed, but a leaked TAP/netns should leave a trail
if provider, err := newProvider(cmd, r); err != nil {
logger.Warnf(ctx, "teardown network for %s: %v", r.VMID, err)
} else if _, err := provider.Delete(ctx, []string{r.VMID}); err != nil {
logger.Warnf(ctx, "teardown network for %s: %v", r.VMID, err)
} else {
// quiesce before Delete closes the same idle-TAP softirq window quiesceNet guards, for the gap
// between the VMM dying and Delete dropping the redirect.
if err := provider.Quiesce(ctx, r.VMID); err != nil {
logger.Warnf(ctx, "quiesce network for %s: %v", r.VMID, err)
}
if _, err := provider.Delete(ctx, []string{r.VMID}); err != nil {
logger.Warnf(ctx, "teardown network for %s: %v", r.VMID, err)
}
}
// CleanupTAPs runs unconditionally: it removes bt<vmid>-* by name and must not be gated on
// newProvider succeeding (rm has no --bridge flag), or an auto-created TAP would leak.
Expand All @@ -93,6 +101,59 @@ func teardownNet(cmd *cobra.Command, r *record) {
}
}

// quiesceNet brings a stopped VM's owned host NICs down so a dead VMM's carrier-less TAP can't storm
// host softirqs (tc mirred redirect firing against the down device per broadcast packet) — CNI via
// the provider's veths, tap/bridge via setTapLink. unquiesceNet reverses it on start.
func quiesceNet(cmd *cobra.Command, r *record) {
if !r.TapOwned {
return
}
ctx := cliutil.CommandContext(cmd)
logger := log.WithFunc("cmd.vm.quiesceNet")
if provider, err := newProvider(cmd, r); err != nil {
logger.Warnf(ctx, "quiesce network for %s: %v", r.VMID, err)
} else if err := provider.Quiesce(ctx, r.VMID); err != nil {
logger.Warnf(ctx, "quiesce network for %s: %v", r.VMID, err)
}
setTapLink(ctx, r, false)
}

func unquiesceNet(cmd *cobra.Command, r *record) {
if !r.TapOwned {
return
}
ctx := cliutil.CommandContext(cmd)
logger := log.WithFunc("cmd.vm.unquiesceNet")
if provider, err := newProvider(cmd, r); err != nil {
logger.Warnf(ctx, "unquiesce network for %s: %v", r.VMID, err)
} else if err := provider.Unquiesce(ctx, r.VMID); err != nil {
logger.Warnf(ctx, "unquiesce network for %s: %v", r.VMID, err)
}
setTapLink(ctx, r, true)
}

// setTapLink flips a host TAP's admin state (down on stop, up on start). QEMU opens it with script=no
// and cocoon's bridge backend no-ops Quiesce, so cocoon-macos owns the toggle; a CNI TAP lives in a
// netns (r.Netns != "") and is the provider's job, so this only reaches host-netns TAPs.
func setTapLink(ctx context.Context, r *record, up bool) {
if r.Tap == "" || r.Netns != "" {
return
}
logger := log.WithFunc("cmd.vm.setTapLink")
link, err := netlink.LinkByName(r.Tap)
if err != nil {
logger.Warnf(ctx, "find tap %s: %v", r.Tap, err)
return
}
set := netlink.LinkSetUp
if !up {
set = netlink.LinkSetDown
}
if err := set(link); err != nil {
logger.Warnf(ctx, "set tap %s up=%v: %v", r.Tap, up, err)
}
}

// ensureNetnsLoopback brings up lo inside the CNI netns. A freshly-created netns has its loopback
// DOWN, so qemu's -vnc 127.0.0.1:N (and any other loopback bind) fails with EADDRNOTAVAIL until lo
// is up. No-op outside CNI (no netns). Shells out to `ip` because the qemu launch already runs via
Expand Down
4 changes: 4 additions & 0 deletions cmd/vm/net_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ func provisionNet(_ *cobra.Command, _ *record) (tap, netns, mac string, err erro

func teardownNet(_ *cobra.Command, _ *record) {}

func quiesceNet(_ *cobra.Command, _ *record) {}

func unquiesceNet(_ *cobra.Command, _ *record) {}

// launchCmd builds the qemu exec; qemu-system-x86_64 is the authoritative VMM with no Go-native
// equivalent (and off Linux there is no netns to enter).
func launchCmd(_ *record, args []string) *exec.Cmd {
Expand Down
2 changes: 1 addition & 1 deletion cmd/vm/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func scaffoldVM(cmd *cobra.Command, name, image, varsSrc, varsName string) (dir,
return "", "", "", "", err
}
ovmfVars = filepath.Join(dir, varsName)
if err = utils.ReflinkCopy(ovmfVars, varsSrc); err != nil {
if err = utils.ReflinkCopy(ovmfVars, varsSrc, utils.Sync); err != nil {
return "", "", "", "", fmt.Errorf("copy OVMF_VARS: %w", err)
}
return dir, overlay, ovmfVars, digest, nil
Expand Down
35 changes: 35 additions & 0 deletions cmd/vm/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package vm

import (
"testing"
"time"

"github.com/spf13/cobra"
)

// TestGraceFromFlags pins the shared stop/rm force mapping: --force is an immediate SIGKILL
// (grace 0), everything else waits the ACPI grace window.
func TestGraceFromFlags(t *testing.T) {
tests := []struct {
name string
force bool
want time.Duration
}{
{name: "force is immediate", force: true, want: 0},
{name: "default waits the grace window", force: false, want: stopGracePeriod},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := &cobra.Command{}
cmd.Flags().Bool("force", false, "")
if tt.force {
if err := cmd.Flags().Set("force", "true"); err != nil {
t.Fatalf("set force: %v", err)
}
}
if got := graceFromFlags(cmd); got != tt.want {
t.Errorf("graceFromFlags(force=%v): got %v, want %v", tt.force, got, tt.want)
}
})
}
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ module github.com/cocoonstack/cocoon-macos
go 1.26.4

require (
github.com/cocoonstack/cocoon v0.4.6-0.20260704025747-d9fa4dfb5105
github.com/cocoonstack/cocoon v0.5.2-0.20260713182614-0f6c21b5b6f6
github.com/docker/go-units v0.5.0
github.com/opencontainers/image-spec v1.1.1
github.com/projecteru2/core v0.0.0-20241016125006-ff909eefe04c
github.com/spf13/cobra v1.10.2
github.com/vishvananda/netlink v1.3.1
golang.org/x/sync v0.21.0
howett.net/plist v1.0.1
oras.land/oras-go/v2 v2.6.1
Expand Down Expand Up @@ -36,7 +37,6 @@ require (
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.50.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZe
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cocoonstack/cocoon v0.4.6-0.20260704025747-d9fa4dfb5105 h1:kY2QEEK+RiJflgTGRfvRZJvN4X87d36dkrneyOZiAMI=
github.com/cocoonstack/cocoon v0.4.6-0.20260704025747-d9fa4dfb5105/go.mod h1:Rl/SAzj1RbyL8XJaIyWiHdT96yK2D1e0xCr8flVqIcA=
github.com/cocoonstack/cocoon v0.5.2-0.20260713182614-0f6c21b5b6f6 h1:OKpbRm7j/WoON1iB26jiIshYwzLibu+5f/p49Um0rzc=
github.com/cocoonstack/cocoon v0.5.2-0.20260713182614-0f6c21b5b6f6/go.mod h1:Rl/SAzj1RbyL8XJaIyWiHdT96yK2D1e0xCr8flVqIcA=
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.0 h1:Mg3SXBdRGkdXyFC4lcwr6u2ZB2SDeL6LC3U+QrEANuQ=
Expand Down
Loading