Skip to content
Draft
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
1 change: 1 addition & 0 deletions cmd/limactl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ func newApp() *cobra.Command {
newNetworkCommand(),
newCloneCommand(),
newRenameCommand(),
newvzvmnetCommand(),
)
addPluginCommands(rootCmd)

Expand Down
27 changes: 27 additions & 0 deletions cmd/limactl/vz-vmnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0

package main

import (
"github.com/spf13/cobra"
)

func newvzvmnetCommand() *cobra.Command {
newCommand := &cobra.Command{
Use: "vz-vmnet",
Short: "Run vz-vmnet",
Args: cobra.ExactArgs(0),
RunE: newvzvmnetAction,
ValidArgsFunction: newvzvmnetComplete,
Hidden: true,
}
newCommand.Flags().Bool("enable-mach-service", false, "Enable Mach service")
newCommand.Flags().String("mach-service", "", "Run as Mach service")
_ = newCommand.Flags().MarkHidden("mach-service")
return newCommand
}

func newvzvmnetComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return bashCompleteInstanceNames(cmd)
}
41 changes: 41 additions & 0 deletions cmd/limactl/vz-vmnet_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0

package main

import (
"errors"
"os"
"os/signal"
"syscall"

"github.com/coreos/go-semver/semver"
"github.com/spf13/cobra"

"github.com/lima-vm/lima/v2/pkg/osutil"
"github.com/lima-vm/lima/v2/pkg/vzvmnet"
)

func newvzvmnetAction(cmd *cobra.Command, _ []string) error {
macOSProductVersion, err := osutil.ProductVersion()
if err != nil {
return err
}
if macOSProductVersion.LessThan(*semver.New("26.0.0")) {
return errors.New("vz-vmnet requires macOS 26 or higher to run")
}

if !cmd.HasLocalFlags() {
return cmd.Help()
}

ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
defer cancel()

if machServiceName, _ := cmd.Flags().GetString("mach-service"); machServiceName != "" {
return vzvmnet.RunMachService(ctx, machServiceName)
} else if enableMachService, _ := cmd.Flags().GetBool("enable-mach-service"); enableMachService {
return vzvmnet.RegisterMachService(ctx)
}
return vzvmnet.UnregisterMachService(ctx)
}
16 changes: 16 additions & 0 deletions cmd/limactl/vz-vmnet_nodarwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build !darwin

// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0

package main

import (
"errors"

"github.com/spf13/cobra"
)

func newvzvmnetAction(_ *cobra.Command, _ []string) error {
return errors.New("vz-vmnet command is only supported on macOS")
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,5 @@ require (
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
)

replace github.com/Code-Hex/vz/v3 => github.com/norio-nomura/vz/v3 v3.7.2-0.20251212095603-d3fad75f665e
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Code-Hex/go-infinity-channel v1.0.0 h1:M8BWlfDOxq9or9yvF9+YkceoTkDI1pFAqvnP87Zh0Nw=
github.com/Code-Hex/go-infinity-channel v1.0.0/go.mod h1:5yUVg/Fqao9dAjcpzoQ33WwfdMWmISOrQloDRn3bsvY=
github.com/Code-Hex/vz/v3 v3.7.1 h1:EN1yNiyrbPq+dl388nne2NySo8I94EnPppvqypA65XM=
github.com/Code-Hex/vz/v3 v3.7.1/go.mod h1:1LsW0jqW0r0cQ+IeR4hHbjdqOtSidNCVMWhStMHGho8=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
Expand Down Expand Up @@ -209,6 +207,8 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/norio-nomura/vz/v3 v3.7.2-0.20251212095603-d3fad75f665e h1:6qEBUBrWQ/agtdr8rH9bolasuRpAPsbBcjTGlfzy3fA=
github.com/norio-nomura/vz/v3 v3.7.2-0.20251212095603-d3fad75f665e/go.mod h1:+0IVfZY7N/7Vv5KpZWbEgTRK6jMg4s7DVM+op2hdyrs=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
Expand Down
30 changes: 27 additions & 3 deletions pkg/driver/vz/vm_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/lima-vm/lima/v2/pkg/networks/usernet"
"github.com/lima-vm/lima/v2/pkg/osutil"
"github.com/lima-vm/lima/v2/pkg/store"
"github.com/lima-vm/lima/v2/pkg/vzvmnet"
)

// diskImageCachingMode is set to DiskImageCachingModeCached so as to avoid disk corruption on ARM:
Expand Down Expand Up @@ -363,7 +364,8 @@ func attachNetwork(ctx context.Context, inst *limatype.Instance, vmConfig *vz.Vi
}

for i, nw := range inst.Networks {
if nw.VZNAT != nil && *nw.VZNAT {
switch {
case nw.VZNAT != nil && *nw.VZNAT:
attachment, err := vz.NewNATNetworkDeviceAttachment()
if err != nil {
return err
Expand All @@ -373,7 +375,29 @@ func attachNetwork(ctx context.Context, inst *limatype.Instance, vmConfig *vz.Vi
return err
}
configurations = append(configurations, networkConfig)
} else if nw.Lima != "" {
case nw.Vz != "":
nwCfg, err := networks.LoadConfig()
if err != nil {
return err
}
vzCfg, ok := nwCfg.Vz[nw.Vz]
if !ok {
return fmt.Errorf("networks.yaml: 'vz: %s' is not defined", nw.Vz)
}
network, err := vzvmnet.RequestVmnetNetwork(ctx, nw.Vz, vzCfg)
if err != nil {
return err
}
attachment, err := vz.NewVmnetNetworkDeviceAttachment(network)
if err != nil {
return err
}
networkConfig, err := newVirtioNetworkDeviceConfiguration(attachment, nw.MACAddress)
if err != nil {
return err
}
configurations = append(configurations, networkConfig)
case nw.Lima != "":
nwCfg, err := networks.LoadConfig()
if err != nil {
return err
Expand Down Expand Up @@ -425,7 +449,7 @@ func attachNetwork(ctx context.Context, inst *limatype.Instance, vmConfig *vz.Vi
configurations = append(configurations, networkConfig)
}
}
} else if nw.Socket != "" {
case nw.Socket != "":
clientFile, err := DialQemu(ctx, nw.Socket)
if err != nil {
return err
Expand Down
6 changes: 6 additions & 0 deletions pkg/driver/vz/vz_driver_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ func validateConfig(_ context.Context, cfg *limatype.LimaYAML) error {

for i, nw := range cfg.Networks {
if unknown := reflectutil.UnknownNonEmptyFields(nw, "VZNAT",
"Vz",
"Lima",
"Socket",
"MACAddress",
Expand All @@ -288,6 +289,11 @@ func validateConfig(_ context.Context, cfg *limatype.LimaYAML) error {
); len(unknown) > 0 {
logrus.Warnf("vmType %s: ignoring networks[%d]: %+v", *cfg.VMType, i, unknown)
}
if nw.Vz != "" {
if macOSProductVersion.LessThan(*semver.New("26.0.0")) {
return fmt.Errorf("networks[%d]: 'vz: %s' require macOS 26.0 or later", i, nw.Vz)
}
}
}

switch audioDevice := *cfg.Audio.Device; audioDevice {
Expand Down
4 changes: 4 additions & 0 deletions pkg/limatmpl/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,10 @@ func (tmpl *Template) combineNetworks() {
tmpl.copyListEntryField(networks, dst, src, "vzNAT")
dest.VZNAT = nw.VZNAT
}
if dest.Vz == "" && nw.Vz != "" {
tmpl.copyListEntryField(networks, dst, src, "vz")
dest.Vz = nw.Vz
}
if dest.Metric == nil && nw.Metric != nil {
tmpl.copyListEntryField(networks, dst, src, "metric")
dest.Metric = nw.Metric
Expand Down
3 changes: 3 additions & 0 deletions pkg/limatype/lima_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ type Network struct {
Socket string `yaml:"socket,omitempty" json:"socket,omitempty"`
// VZNAT uses VZNATNetworkDeviceAttachment. Needs VZ. No root privilege is required.
VZNAT *bool `yaml:"vzNAT,omitempty" json:"vzNAT,omitempty"`
// Vz uses VZVmnetNetworkDeviceAttachment. Needs VZ. No root privilege is required.
// Requires macOS 26.0 or later.
Vz string `yaml:"vz,omitempty" json:"vz,omitempty"`

MACAddress string `yaml:"macAddress,omitempty" json:"macAddress,omitempty"`
Interface string `yaml:"interface,omitempty" json:"interface,omitempty"`
Expand Down
19 changes: 19 additions & 0 deletions pkg/limayaml/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,22 +466,41 @@ func validateNetwork(y *limatype.LimaYAML) error {
if nw.VZNAT != nil && *nw.VZNAT {
errs = errors.Join(errs, fmt.Errorf("field `%s.lima` and field `%s.vzNAT` are mutually exclusive", field, field))
}
if nw.Vz != "" {
errs = errors.Join(errs, fmt.Errorf("field `%s.lima` and field `%s.vz` are mutually exclusive", field, field))
}
case nw.Socket != "":
if nw.VZNAT != nil && *nw.VZNAT {
errs = errors.Join(errs, fmt.Errorf("field `%s.socket` and field `%s.vzNAT` are mutually exclusive", field, field))
}
if nw.Vz != "" {
errs = errors.Join(errs, fmt.Errorf("field `%s.socket` and field `%s.vz` are mutually exclusive", field, field))
}
if fi, err := os.Stat(nw.Socket); err != nil && !errors.Is(err, os.ErrNotExist) {
errs = errors.Join(errs, err)
} else if err == nil && fi.Mode()&os.ModeSocket == 0 {
errs = errors.Join(errs, fmt.Errorf("field `%s.socket` %q points to a non-socket file", field, nw.Socket))
}
case nw.VZNAT != nil && *nw.VZNAT:
if nw.Vz != "" {
errs = errors.Join(errs, fmt.Errorf("field `%s.vzNAT` and field `%s.vz` are mutually exclusive", field, field))
}
if nw.Lima != "" {
errs = errors.Join(errs, fmt.Errorf("field `%s.vzNAT` and field `%s.lima` are mutually exclusive", field, field))
}
if nw.Socket != "" {
errs = errors.Join(errs, fmt.Errorf("field `%s.vzNAT` and field `%s.socket` are mutually exclusive", field, field))
}
case nw.Vz != "":
if nw.VZNAT != nil && *nw.VZNAT {
errs = errors.Join(errs, fmt.Errorf("field `%s.vz` and field `%s.vzNAT` are mutually exclusive", field, field))
}
if nw.Lima != "" {
errs = errors.Join(errs, fmt.Errorf("field `%s.vz` and field `%s.lima` are mutually exclusive", field, field))
}
if nw.Socket != "" {
errs = errors.Join(errs, fmt.Errorf("field `%s.vz` and field `%s.socket` are mutually exclusive", field, field))
}
default:
errs = errors.Join(errs, fmt.Errorf("field `%s.lima` or field `%s.socket must be set", field, field))
}
Expand Down
7 changes: 7 additions & 0 deletions pkg/networks/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ func fillDefaults(cfg Config) (Config, error) {
}
cfg.Networks[ModeUserV2] = defaultCfg.Networks[ModeUserV2]
}
if len(cfg.Vz) == 0 {
defaultCfg, err := DefaultConfig()
if err != nil {
return cfg, err
}
cfg.Vz = defaultCfg.Vz
}
return cfg, nil
}

Expand Down
21 changes: 21 additions & 0 deletions pkg/networks/networks.TEMPLATE.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,24 @@ networks:
gateway: 192.168.106.1
dhcpEnd: 192.168.106.254
netmask: 255.255.255.0

vz:
shared:
mode: shared
dhcp: true
dnsProxy: true
mtu: 1500
nat44: true
nat66: true
routerAdvertisement: true
subnet: 192.168.107.0/24
host:
mode: host
dhcp: true
dnsProxy: true
mtu: 1500
nat44: true
nat66: true
# host mode ignores routerAdvertisement setting
routerAdvertisement: false
subnet: 192.168.108.0/24
30 changes: 26 additions & 4 deletions pkg/networks/networks.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@

package networks

import "net"
import (
"net"
"net/netip"
)

type Config struct {
Paths Paths `yaml:"paths" json:"paths"`
Group string `yaml:"group,omitempty" json:"group,omitempty"` // default: "everyone"
Networks map[string]Network `yaml:"networks" json:"networks"`
Paths Paths `yaml:"paths" json:"paths"`
Group string `yaml:"group,omitempty" json:"group,omitempty"` // default: "everyone"
Networks map[string]Network `yaml:"networks" json:"networks"`
Vz map[string]VzVmnetConfig `yaml:"vz" json:"vz"`
}

type Paths struct {
Expand Down Expand Up @@ -38,3 +42,21 @@ type Network struct {
DHCPEnd net.IP `yaml:"dhcpEnd,omitempty" json:"dhcpEnd,omitempty"` // default: same as Gateway, last byte is 254
NetMask net.IP `yaml:"netmask,omitempty" json:"netmask,omitempty"` // default: 255.255.255.0
}

type VzVmnetMode string

const (
VzModeShared VzVmnetMode = "shared"
VzModeHost VzVmnetMode = "host"
)

type VzVmnetConfig struct {
Mode VzVmnetMode `yaml:"mode" json:"mode"` // "shared" or "host"
Dhcp bool `yaml:"dhcp,omitempty" json:"dhcp,omitempty"`
DNSProxy bool `yaml:"dnsProxy,omitempty" json:"dnsProxy,omitempty"`
Mtu uint32 `yaml:"mtu,omitempty" json:"mtu,omitempty"`
Nat44 bool `yaml:"nat44,omitempty" json:"nat44,omitempty"`
Nat66 bool `yaml:"nat66,omitempty" json:"nat66,omitempty"`
RouterAdvertisement bool `yaml:"routerAdvertisement,omitempty" json:"routerAdvertisement,omitempty"`
Subnet netip.Prefix `yaml:"subnet,omitempty" json:"subnet,omitempty"`
}
Loading
Loading