WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 81d4cc6

Browse files
committed
feat: Add limactl vz-vmnet-shared
It shares `VmnetNetwork` serialization between VMs using SharedMode. - `limactl vz-vmnet-shared --enable-mach-service`: register Mach service and launch - `limactl vz-vmnet-shared --enable-mach-service=false`: unregister Mach service When the `limactl` executable file is updated due to rebuilds, etc., the VM using the serialization data held by the Mach server before the update cannot be booted. It is necessary to add a version check and restart the service as appropriate. Also, it seems that it cannot be used with an external vz driver. Signed-off-by: Norio Nomura <[email protected]>
1 parent 8125323 commit 81d4cc6

File tree

9 files changed

+343
-8
lines changed

9 files changed

+343
-8
lines changed

cmd/limactl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ func newApp() *cobra.Command {
208208
newNetworkCommand(),
209209
newCloneCommand(),
210210
newRenameCommand(),
211+
newVzVmnetSharedCommand(),
211212
)
212213
addPluginCommands(rootCmd)
213214

cmd/limactl/vz-vmnet-shared.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package main
5+
6+
import (
7+
"github.com/spf13/cobra"
8+
)
9+
10+
func newVzVmnetSharedCommand() *cobra.Command {
11+
newCommand := &cobra.Command{
12+
Use: "vz-vmnet-shared",
13+
Short: "Run vz-vmnet-shared",
14+
Args: cobra.ExactArgs(0),
15+
RunE: newVzVmnetSharedAction,
16+
ValidArgsFunction: newVzVmnetSharedComplete,
17+
Hidden: true,
18+
}
19+
newCommand.Flags().Bool("enable-mach-service", false, "Enable Mach service")
20+
newCommand.Flags().String("mach-service", "", "Run as Mach service")
21+
newCommand.Flags().MarkHidden("mach-service")
22+
return newCommand
23+
}
24+
25+
func newVzVmnetSharedComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
26+
return bashCompleteInstanceNames(cmd)
27+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package main
5+
6+
import (
7+
"errors"
8+
"os"
9+
"os/signal"
10+
"syscall"
11+
12+
"github.com/coreos/go-semver/semver"
13+
"github.com/spf13/cobra"
14+
15+
"github.com/lima-vm/lima/v2/pkg/osutil"
16+
"github.com/lima-vm/lima/v2/pkg/vzvmnetshared"
17+
)
18+
19+
func newVzVmnetSharedAction(cmd *cobra.Command, args []string) error {
20+
macOSProductVersion, err := osutil.ProductVersion()
21+
if err != nil {
22+
return err
23+
}
24+
if macOSProductVersion.LessThan(*semver.New("26.0.0")) {
25+
return errors.New("vz-vmnet-shared requires macOS 26 or higher to run")
26+
}
27+
28+
if !cmd.HasLocalFlags() {
29+
return cmd.Help()
30+
}
31+
32+
ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
33+
defer cancel()
34+
35+
if machServiceName, _ := cmd.Flags().GetString("mach-service"); machServiceName != "" {
36+
return vzvmnetshared.RunMachService(ctx, machServiceName)
37+
} else if enableMachService, _ := cmd.Flags().GetBool("enable-mach-service"); enableMachService {
38+
return vzvmnetshared.RegisterMachService(ctx)
39+
}
40+
return vzvmnetshared.UnregisterMachService(ctx)
41+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//go:build !darwin
2+
3+
// SPDX-FileCopyrightText: Copyright The Lima Authors
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
package main
7+
8+
import (
9+
"errors"
10+
11+
"github.com/spf13/cobra"
12+
)
13+
14+
func newVzVmnetSharedAction(_ *cobra.Command, _ []string) error {
15+
return errors.New("vz-vmnet-shared command is only supported on macOS")
16+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,4 @@ require (
148148
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
149149
)
150150

151-
replace github.com/Code-Hex/vz/v3 => github.com/norio-nomura/vz/v3 v3.7.2-0.20251122122159-6617c8faa123
151+
replace github.com/Code-Hex/vz/v3 => github.com/norio-nomura/vz/v3 v3.7.2-0.20251203072611-007c2a5b352c

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,8 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
207207
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
208208
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
209209
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
210-
github.com/norio-nomura/vz/v3 v3.7.2-0.20251122122159-6617c8faa123 h1:3Xzg1W5gel17So2d2NSA+flx6yoyknx5nG9Pb6eZU6s=
211-
github.com/norio-nomura/vz/v3 v3.7.2-0.20251122122159-6617c8faa123/go.mod h1:+0IVfZY7N/7Vv5KpZWbEgTRK6jMg4s7DVM+op2hdyrs=
210+
github.com/norio-nomura/vz/v3 v3.7.2-0.20251203072611-007c2a5b352c h1:0QGVXjk6/KA2G5yLQZGKZf9GB8caGCMS5H3sy0zPoH0=
211+
github.com/norio-nomura/vz/v3 v3.7.2-0.20251203072611-007c2a5b352c/go.mod h1:+0IVfZY7N/7Vv5KpZWbEgTRK6jMg4s7DVM+op2hdyrs=
212212
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
213213
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
214214
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=

pkg/driver/vz/vm_darwin.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"fmt"
1212
"io/fs"
1313
"net"
14+
"net/netip"
1415
"os"
1516
"path/filepath"
1617
"runtime"
@@ -37,6 +38,7 @@ import (
3738
"github.com/lima-vm/lima/v2/pkg/networks/usernet"
3839
"github.com/lima-vm/lima/v2/pkg/osutil"
3940
"github.com/lima-vm/lima/v2/pkg/store"
41+
"github.com/lima-vm/lima/v2/pkg/vzvmnetshared"
4042
)
4143

4244
// diskImageCachingMode is set to DiskImageCachingModeCached so as to avoid disk corruption on ARM:
@@ -374,11 +376,8 @@ func attachNetwork(ctx context.Context, inst *limatype.Instance, vmConfig *vz.Vi
374376
}
375377
configurations = append(configurations, networkConfig)
376378
} else if nw.VZShared != nil && *nw.VZShared {
377-
config, err := vz.NewVmnetNetworkConfiguration(vz.SharedMode)
378-
if err != nil {
379-
return err
380-
}
381-
network, err := vz.NewVmnetNetwork(config)
379+
subnet := netip.MustParsePrefix("192.168.107.0/24")
380+
network, err := vzvmnetshared.RequestSharedVmnetNetwork(ctx, subnet)
382381
if err != nil {
383382
return err
384383
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>Label</key>
6+
<string>{{.Label}}</string>
7+
<key>ProgramArguments</key>
8+
<array>
9+
{{- range $arg := .ProgramArguments}}
10+
<string>{{$arg}}</string>
11+
{{- end}}
12+
</array>
13+
<key>RunAtLoad</key>
14+
<true/>
15+
<key>WorkingDirectory</key>
16+
<string>{{ .WorkingDirectory }}</string>
17+
<key>StandardErrorPath</key>
18+
<string>{{ .WorkingDirectory }}/stderr.log</string>
19+
<key>StandardOutPath</key>
20+
<string>{{ .WorkingDirectory }}/stdout.log</string>
21+
<key>MachServices</key>
22+
<dict>
23+
{{- range $service := .MachServices}}
24+
<key>{{$service}}</key>
25+
<true/>
26+
{{- end}}
27+
</dict>
28+
</dict>
29+
</plist>
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package vzvmnetshared
2+
3+
import (
4+
"bytes"
5+
"context"
6+
_ "embed"
7+
"fmt"
8+
"net/netip"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"text/template"
13+
14+
"github.com/Code-Hex/vz/v3"
15+
"github.com/Code-Hex/vz/v3/pkg/xpc"
16+
17+
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
18+
)
19+
20+
//go:embed io.lima-vm.vz.vmnet.shared.plist
21+
var launchdTemplate string
22+
23+
const (
24+
launchdLabel = "io.lima-vm.vz.vmnet.shared"
25+
MachServiceName = launchdLabel + ".subnet"
26+
)
27+
28+
// RegisterMachService registers the "io.lima-vm.vz.vmnet.shared" launchd service.
29+
//
30+
// - It creates a launchd plist under ~/Library/LaunchAgents and bootstraps it.
31+
// - The mach service "io.lima-vm.vz.vmnet.shared.subnet" is registered.
32+
// - The working directory is $LIMA_HOME/_networks/vz-vmnet-shared.
33+
// - It also creates a shell script named "io.lima-vm.vz.vmnet.shared.sh" that runs
34+
// "limactl vz-vmnet-shared" to avoid launching "limactl" directly from launchd.
35+
// macOS System Settings (General > Login Items & Extensions) shows the first
36+
// element of ProgramArguments as the login item name; using a shell script with
37+
// a fixed filename makes the item easier to identify.
38+
func RegisterMachService(ctx context.Context) error {
39+
executablePath, workDir, scriptPath, launchdPlistPath, err := relatedPaths(launchdLabel)
40+
if err != nil {
41+
return err
42+
}
43+
44+
// Create a shell script that runs "limactl vz-vmnet-shared"
45+
scriptContent := "#!/bin/sh\nexec " + executablePath + " vz-vmnet-shared --mach-service='" + MachServiceName + "' \"$@\""
46+
if err := os.WriteFile(scriptPath, []byte(scriptContent), 0o755); err != nil {
47+
return fmt.Errorf("failed to write %q launch script: %w", scriptPath, err)
48+
}
49+
50+
// Create launchd plist
51+
params := struct {
52+
Label string
53+
ProgramArguments []string
54+
WorkingDirectory string
55+
MachServices []string
56+
}{
57+
Label: launchdLabel,
58+
ProgramArguments: []string{scriptPath},
59+
WorkingDirectory: workDir,
60+
MachServices: []string{MachServiceName},
61+
}
62+
template, err := template.New("plist").Parse(launchdTemplate)
63+
if err != nil {
64+
return fmt.Errorf("failed to parse launchd plist template: %w", err)
65+
}
66+
var b bytes.Buffer
67+
if err := template.Execute(&b, params); err != nil {
68+
return fmt.Errorf("failed to execute launchd plist template: %w", err)
69+
}
70+
if err := os.WriteFile(launchdPlistPath, b.Bytes(), 0o644); err != nil {
71+
return fmt.Errorf("failed to write launchd plist %q: %w", launchdPlistPath, err)
72+
}
73+
74+
// Bootstrap launchd plist
75+
cmd := exec.CommandContext(ctx, "launchctl", "bootstrap", serviceDomain(), launchdPlistPath)
76+
if err := cmd.Run(); err != nil {
77+
return fmt.Errorf("failed to execute bootstrap: %v: %w", cmd.Args, err)
78+
}
79+
return nil
80+
}
81+
82+
// UnregisterMachService unregisters the "io.lima-vm.vz.vmnet.shared" launchd service.
83+
//
84+
// - It unbootstraps the launchd plist.
85+
// - It removes the launchd plist file under ~/Library/LaunchAgents.
86+
// - It removes the shell script used to launch "limactl vz-vmnet-shared".
87+
func UnregisterMachService(ctx context.Context) error {
88+
serviceTarget := serviceTarget(launchdLabel)
89+
cmd := exec.CommandContext(ctx, "launchctl", "bootout", serviceTarget)
90+
if err := cmd.Run(); err != nil {
91+
return fmt.Errorf("failed to execute bootout: %v: %w", cmd.Args, err)
92+
}
93+
_, _, scriptPath, launchdPlistPath, err := relatedPaths(launchdLabel)
94+
if err != nil {
95+
return err
96+
}
97+
if err := os.Remove(launchdPlistPath); err != nil && !os.IsNotExist(err) {
98+
return fmt.Errorf("failed to remove launchd plist %q: %w", launchdPlistPath, err)
99+
}
100+
if err := os.Remove(scriptPath); err != nil && !os.IsNotExist(err) {
101+
return fmt.Errorf("failed to remove launch script file %q: %w", scriptPath, err)
102+
}
103+
return nil
104+
}
105+
106+
func relatedPaths(launchdLabel string) (executablePath, workDir, scriptPath, plistPath string, err error) {
107+
executablePath, err = os.Executable()
108+
if err != nil {
109+
return "", "", "", "", fmt.Errorf("failed to get executable path: %w", err)
110+
}
111+
networksDir, err := dirnames.LimaNetworksDir()
112+
if err != nil {
113+
return "", "", "", "", fmt.Errorf("failed to get Lima networks directory: %w", err)
114+
}
115+
// Working directory
116+
workDir = filepath.Join(networksDir, "vz-vmnet-shared")
117+
if err := os.MkdirAll(workDir, 0o755); err != nil {
118+
return "", "", "", "", fmt.Errorf("failed to create working directory %q: %w", workDir, err)
119+
}
120+
// Shell script path
121+
scriptPath = filepath.Join(workDir, launchdLabel+".sh")
122+
// Launchd plist path
123+
userHomeDir, err := os.UserHomeDir()
124+
if err != nil {
125+
return "", "", "", "", fmt.Errorf("failed to get user home directory: %w", err)
126+
}
127+
plistPath = filepath.Join(userHomeDir, "Library", "LaunchAgents", launchdLabel+".plist")
128+
return executablePath, workDir, scriptPath, plistPath, nil
129+
}
130+
131+
func serviceDomain() string {
132+
return fmt.Sprintf("gui/%d", os.Getuid())
133+
}
134+
135+
func serviceTarget(launchdLabel string) string {
136+
return fmt.Sprintf("%s/%s", serviceDomain(), launchdLabel)
137+
}
138+
139+
// RunMachService runs the mach service at specified service name.
140+
//
141+
// It listens for incoming mach messages requesting a shared VmnetNetwork
142+
// for a given subnet, creates the VmnetNetwork if not already created,
143+
// and returns the serialized network object via mach IPC.
144+
func RunMachService(ctx context.Context, serviceName string) error {
145+
serializationStore := make(map[netip.Prefix]*xpc.Object)
146+
listener, err := xpc.NewListener(serviceName,
147+
xpc.NewSessionHandler(
148+
func(msg *xpc.Object) *xpc.Object {
149+
errorReply := func(errMsg string, args ...any) *xpc.Object {
150+
return msg.DictionaryCreateReply(
151+
xpc.WithString("Error", fmt.Sprintf(errMsg, args...)),
152+
)
153+
}
154+
// Handle the message
155+
subnetStr := msg.DictionaryGetString("Subnet")
156+
if subnetStr == "" {
157+
return errorReply("missing Subnet key")
158+
}
159+
prefix, err := netip.ParsePrefix(subnetStr)
160+
if err != nil {
161+
return errorReply("failed to parse Subnet %q: %v", subnetStr, err)
162+
}
163+
// Modify the prefix to having IP that VmnetNetwork can accept.
164+
prefix = netip.PrefixFrom(prefix.Masked().Addr().Next(), prefix.Bits())
165+
serialization, ok := serializationStore[prefix]
166+
if !ok {
167+
if config, err := vz.NewVmnetNetworkConfiguration(vz.SharedMode); err != nil {
168+
return errorReply("failed to create network configuration: %v", err)
169+
} else if err := config.SetIPv4Subnet(prefix); err != nil {
170+
return errorReply("failed to set IPv4 subnet: %v", err)
171+
} else if newNetwork, err := vz.NewVmnetNetwork(config); err != nil {
172+
return errorReply("failed to create VmnetNetwork: %v", err)
173+
} else if rawSerialization, err := newNetwork.CopySerialization(); err != nil {
174+
return errorReply("failed to copy network serialization: %v", err)
175+
} else {
176+
serialization = xpc.NewObject(rawSerialization)
177+
serializationStore[prefix] = serialization
178+
}
179+
}
180+
return msg.DictionaryCreateReply(
181+
xpc.WithValue("Network", serialization),
182+
)
183+
},
184+
nil,
185+
),
186+
)
187+
if err != nil {
188+
return err
189+
}
190+
listener.Activate()
191+
<-ctx.Done()
192+
return listener.Close()
193+
}
194+
195+
// RequestSharedVmnetNetwork requests a shared VmnetNetwork for the given subnet
196+
// from the mach service "io.lima-vm.vz.vmnet.shared.subnet".
197+
func RequestSharedVmnetNetwork(ctx context.Context, subnet netip.Prefix) (*vz.VmnetNetwork, error) {
198+
session, err := xpc.NewSession(MachServiceName)
199+
if err != nil {
200+
return nil, fmt.Errorf("failed to create xpc session to %q: %w", MachServiceName, err)
201+
}
202+
defer session.Cancel()
203+
reply, err := session.SendDictionaryWithReply(
204+
ctx,
205+
xpc.WithString("Subnet", subnet.String()),
206+
)
207+
if err != nil {
208+
return nil, fmt.Errorf("failed to send xpc message to %q: %w", MachServiceName, err)
209+
}
210+
if errMsg := reply.DictionaryGetString("Error"); errMsg != "" {
211+
return nil, fmt.Errorf("error from mach service %q: %s", MachServiceName, errMsg)
212+
}
213+
serialization := reply.DictionaryGetValue("Network")
214+
if serialization == nil {
215+
return nil, fmt.Errorf("no Network object in reply from %q", MachServiceName)
216+
}
217+
network, err := vz.NewVmnetNetworkWithSerialization(serialization.XpcObject)
218+
if err != nil {
219+
return nil, fmt.Errorf("failed to create VmnetNetwork (%v) from serialization: %w", subnet, err)
220+
}
221+
return network, nil
222+
}

0 commit comments

Comments
 (0)