From 20274dbda22cb9112b55e114a1f04e3fcba80912 Mon Sep 17 00:00:00 2001 From: Lucas Li Date: Tue, 26 May 2026 19:29:05 +0900 Subject: [PATCH] Add VZVmnetNetworkDeviceAttachment + vmnet subpackage (macOS 26+) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps the new VZVmnetNetworkDeviceAttachment class added in macOS 26 and exposes the underlying vmnet_network configuration surface as a new `vmnet` subpackage so callers can drive vmnet_network_create with mode, IPv4 subnet, and DHCP reservations. package vmnet (new): type Mode int const ( HostMode Mode = 1000 SharedMode Mode = 1001 BridgedMode Mode = 1002 ) type NetworkConfiguration struct { *objc.Pointer } func NewNetworkConfiguration(mode Mode) (*NetworkConfiguration, error) func (*NetworkConfiguration) SetIPv4Subnet(netip.Prefix) error func (*NetworkConfiguration) AddDhcpReservation(net.HardwareAddr, netip.Addr) error type Network struct { *objc.Pointer } func NewNetwork(config *NetworkConfiguration) (*Network, error) func NewNetworkFromPointer(*objc.Pointer) *Network package vz (additions in network.go): type VmnetNetworkDeviceAttachment struct { *pointer *baseNetworkDeviceAttachment } func (*VmnetNetworkDeviceAttachment) String() string func (*VmnetNetworkDeviceAttachment) Network() *vmnet.Network func NewVmnetNetworkDeviceAttachment(*vmnet.Network) (*VmnetNetworkDeviceAttachment, error) NetworkConfiguration.SetIPv4Subnet normalizes its netip.Prefix argument to the gateway IP (first host of the network) before calling vmnet_network_configuration_set_ipv4_subnet. Apple's docs call that parameter "subnet_addr" but the actual semantic — confirmed via vmnet_network_get_ipv4_subnet round-trip — is the gateway IP, matching the older `vmnet_start_address_key` XPC key. VZVmnetNetworkDeviceAttachment.network is a C-typed `@property (readonly) vmnet_network_ref`. ObjC properties for non-NSObject C types default to `assign` semantics, so the attachment does not retain. Lifecycle: - vmnet_network_create returns +1 retained — owned by the Go *vmnet.Network wrapper, which CFReleases on GC finalize. - The VZ attachment stores the same pointer without retaining; callers must keep the *vmnet.Network alive while the attachment is in use (it's stored on the VirtualMachineConfiguration, so the typical orchestration code keeps it reachable). - Network() reads VZ's stored pointer, CFRetain's it, and returns a fresh *vmnet.Network wrapper that owns its own refcount. SDK compile-time guard via the existing INCLUDE_TARGET_OSX_26 macro in virtualization_helper.h; runtime guard via @available(macOS 26, *) in the Obj-C wrappers; macOS-version + build-target guards on the Go side via the existing osversion helpers. Header dependency note: VZVmnetNetworkDeviceAttachment.h references `vmnet_network_ref` via a @property, so every compilation unit that ends up pulling Virtualization.h needs in scope first. Added the include to virtualization_helper.h (imported by every per-version header). --- network.go | 53 ++++++++++++- osversion.go | 2 + virtualization_26.h | 23 ++++++ virtualization_26.m | 38 ++++++++++ virtualization_helper.h | 13 ++++ vmnet/cgo.h | 51 +++++++++++++ vmnet/cgo.m | 100 +++++++++++++++++++++++++ vmnet/vmnet.go | 160 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 439 insertions(+), 1 deletion(-) create mode 100644 virtualization_26.h create mode 100644 virtualization_26.m create mode 100644 vmnet/cgo.h create mode 100644 vmnet/cgo.m create mode 100644 vmnet/vmnet.go diff --git a/network.go b/network.go index fdcdccd4..c16e9caf 100644 --- a/network.go +++ b/network.go @@ -2,9 +2,11 @@ package vz /* #cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc -#cgo darwin LDFLAGS: -lobjc -framework Foundation -framework Virtualization +#cgo darwin LDFLAGS: -lobjc -framework Foundation -framework Virtualization -framework vmnet +# include # include "virtualization_11.h" # include "virtualization_13.h" +# include "virtualization_26.h" */ import "C" import ( @@ -14,6 +16,7 @@ import ( "syscall" "github.com/Code-Hex/vz/v3/internal/objc" + "github.com/Code-Hex/vz/v3/vmnet" ) // BridgedNetwork defines a network interface that bridges a physical interface with a virtual machine. @@ -266,6 +269,54 @@ func (f *FileHandleNetworkDeviceAttachment) MaximumTransmissionUnit() int { return f.mtu } +// VmnetNetworkDeviceAttachment is a network device attachment backed +// by an in-process vmnet logical network. The attachment binds the +// vmnet network to the virtio-net device in-kernel — no userspace +// pump. The caller builds the network via the vmnet subpackage, +// which exposes operating mode (host / shared / bridged), IPv4 +// subnet, and DHCP reservations. +// +// see: https://developer.apple.com/documentation/virtualization/vzvmnetnetworkdeviceattachment +type VmnetNetworkDeviceAttachment struct { + *pointer + + *baseNetworkDeviceAttachment +} + +func (*VmnetNetworkDeviceAttachment) String() string { + return "VmnetNetworkDeviceAttachment" +} + +// Network returns the vmnet network underlying the attachment. The +// returned wrapper owns its own +1 retain count. +func (v *VmnetNetworkDeviceAttachment) Network() *vmnet.Network { + ptr := C.VZVmnetNetworkDeviceAttachment_network(objc.Ptr(v)) + return vmnet.NewNetworkFromPointer(objc.NewPointer(ptr)) +} + +var _ NetworkDeviceAttachment = (*VmnetNetworkDeviceAttachment)(nil) + +// NewVmnetNetworkDeviceAttachment creates a new +// VmnetNetworkDeviceAttachment wrapping the given vmnet.Network. +// +// This is only supported on macOS 26 and newer, error will +// be returned on older versions. +func NewVmnetNetworkDeviceAttachment(network *vmnet.Network) (*VmnetNetworkDeviceAttachment, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + + attachment := &VmnetNetworkDeviceAttachment{ + pointer: objc.NewPointer( + C.newVZVmnetNetworkDeviceAttachment(objc.Ptr(network)), + ), + } + objc.SetFinalizer(attachment, func(self *VmnetNetworkDeviceAttachment) { + objc.Release(self) + }) + return attachment, nil +} + // NetworkDeviceAttachment for a network device attachment. // see: https://developer.apple.com/documentation/virtualization/vznetworkdeviceattachment?language=objc type NetworkDeviceAttachment interface { diff --git a/osversion.go b/osversion.go index 02a8a373..d3d405f3 100644 --- a/osversion.go +++ b/osversion.go @@ -107,6 +107,8 @@ func macOSBuildTargetAvailable(version float64) error { target = 140000 // __MAC_14_0 case 15: target = 150000 // __MAC_15_0 + case 26: + target = 260000 // __MAC_26_0 } if allowedVersion < target { return fmt.Errorf("%w for %.1f (the binary was built with __MAC_OS_X_VERSION_MAX_ALLOWED=%d; needs recompilation)", diff --git a/virtualization_26.h b/virtualization_26.h new file mode 100644 index 00000000..8e5d03a8 --- /dev/null +++ b/virtualization_26.h @@ -0,0 +1,23 @@ +// +// virtualization_26.h +// +// Created by codehex. +// + +#pragma once + +#import "virtualization_helper.h" +#import +#import + +// newVZVmnetNetworkDeviceAttachment wraps a vmnet_network_ref +// (created via the vmnet subpackage, passed in as void *) in a +// VZVmnetNetworkDeviceAttachment. The attachment does not retain +// the network; the caller's Network wrapper owns the +1. +void *newVZVmnetNetworkDeviceAttachment(void *network); + +// VZVmnetNetworkDeviceAttachment_network reads the network back from +// an attachment. CFRetain'd before return so the Go-side Network +// wrapper can own a refcount independent of the attachment's +// storage. +void *VZVmnetNetworkDeviceAttachment_network(void *attachment); diff --git a/virtualization_26.m b/virtualization_26.m new file mode 100644 index 00000000..540cd754 --- /dev/null +++ b/virtualization_26.m @@ -0,0 +1,38 @@ +// +// virtualization_26.m +// +// Created by codehex. +// + +#import "virtualization_26.h" + +#ifdef INCLUDE_TARGET_OSX_26 +#import +#import +#endif // INCLUDE_TARGET_OSX_26 + +void *newVZVmnetNetworkDeviceAttachment(void *network) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return [[VZVmnetNetworkDeviceAttachment alloc] + initWithNetwork:(vmnet_network_ref)network]; + } +#endif + (void)network; + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); + return NULL; +} + +void *VZVmnetNetworkDeviceAttachment_network(void *attachment) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_ref network = ((VZVmnetNetworkDeviceAttachment *)attachment).network; + if (network) CFRetain(network); + return network; + } +#endif + (void)attachment; + return NULL; +} diff --git a/virtualization_helper.h b/virtualization_helper.h index c1b52070..6e5aa443 100644 --- a/virtualization_helper.h +++ b/virtualization_helper.h @@ -2,6 +2,12 @@ #import #import +// vmnet.h declares vmnet_network_ref, which Virtualization.framework's +// VZVmnetNetworkDeviceAttachment.h references via a @property. Every +// compilation unit that ends up pulling in +// needs this in scope first, so include it here in the helper that +// every per-version header imports. +#import NSDictionary *dumpProcessinfo(); NSFileHandle *newFileHandleDupFd(int fileDescriptor, void **error); @@ -47,6 +53,13 @@ NSFileHandle *newFileHandleDupFd(int fileDescriptor, void **error); #pragma message("macOS 15 API has been disabled") #endif +// for macOS 26 API +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 260000 +#define INCLUDE_TARGET_OSX_26 1 +#else +#pragma message("macOS 26 API has been disabled") +#endif + static inline int mac_os_x_version_max_allowed() { #ifdef __MAC_OS_X_VERSION_MAX_ALLOWED diff --git a/vmnet/cgo.h b/vmnet/cgo.h new file mode 100644 index 00000000..5c9bddbe --- /dev/null +++ b/vmnet/cgo.h @@ -0,0 +1,51 @@ +// +// vmnet.h +// +// Created by codehex. +// + +#pragma once + +#import +#include +#include + +// SDK guard, mirroring the parent package's virtualization_helper.h. +// Duplicated because cgo includes are per-package (subpackages don't +// share headers with the root unless explicitly wired). Keep in sync. +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 260000 +#define INCLUDE_TARGET_OSX_26 1 +#endif + +// --- vmnet_network_configuration_* wrappers ------------------------------ + +// newNetworkConfiguration returns a +1-retained +// vmnet_network_configuration_ref for the given operating mode. +// On older macOS / older SDKs returns NULL; status (if non-NULL) +// gets the vmnet_return_t (or -1 when the API isn't compiled in). +void *newNetworkConfiguration(int mode, int *status); + +// releaseNetworkConfiguration CFReleases a config ref. Safe on NULL. +void releaseNetworkConfiguration(void *config); + +// setIPv4Subnet calls vmnet_network_configuration_set_ipv4_subnet. +// gatewayIP is the first usable address of the range (e.g. +// "192.168.200.1" for /24) — Apple's docstring calls the param +// "subnet_addr" but the actual semantic is the gateway IP. mask is +// dotted-quad ("255.255.255.0"). Returns vmnet_return_t (0=success). +int setIPv4Subnet(void *config, const char *gatewayIP, const char *mask); + +// addDhcpReservation calls vmnet_network_configuration_add_dhcp_reservation. +// mac is exactly 6 bytes; reservationIP is a dotted-quad IPv4 string. +// Returns vmnet_return_t (0=success). +int addDhcpReservation(void *config, const uint8_t *mac, const char *reservationIP); + +// --- vmnet_network_* wrappers -------------------------------------------- + +// newNetwork returns a +1-retained vmnet_network_ref from the given +// configuration. The configuration may be released after this call. +// On failure or older macOS / SDK returns NULL. +void *newNetwork(void *config, int *status); + +// releaseNetwork CFReleases a network ref. Safe on NULL. +void releaseNetwork(void *network); diff --git a/vmnet/cgo.m b/vmnet/cgo.m new file mode 100644 index 00000000..8f6101ba --- /dev/null +++ b/vmnet/cgo.m @@ -0,0 +1,100 @@ +// +// vmnet.m +// +// Created by codehex. +// + +#import "cgo.h" + +#ifdef INCLUDE_TARGET_OSX_26 +#import +#import +#import +#import +#import +#endif // INCLUDE_TARGET_OSX_26 + +void *newNetworkConfiguration(int mode, int *status) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_return_t s = VMNET_SUCCESS; + vmnet_network_configuration_ref config = + vmnet_network_configuration_create((vmnet_mode_t)mode, &s); + if (status) *status = (int)s; + return config; + } +#endif + if (status) *status = -1; + return NULL; +} + +void releaseNetworkConfiguration(void *config) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (config) CFRelease((vmnet_network_configuration_ref)config); +#else + (void)config; +#endif +} + +// setIPv4Subnet / addDhcpReservation return 0 on success, the raw +// vmnet_return_t on failure (or -1 if the API isn't compiled in). +// vmnet's VMNET_SUCCESS is 1000, not 0 — callers can't compare the +// raw return against 0, so we normalize here. +int setIPv4Subnet(void *config, const char *gatewayIP, const char *mask) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + struct in_addr s, m; + if (inet_pton(AF_INET, gatewayIP, &s) != 1) return (int)VMNET_INVALID_ARGUMENT; + if (inet_pton(AF_INET, mask, &m) != 1) return (int)VMNET_INVALID_ARGUMENT; + vmnet_return_t rc = vmnet_network_configuration_set_ipv4_subnet( + (vmnet_network_configuration_ref)config, &s, &m); + return rc == VMNET_SUCCESS ? 0 : (int)rc; + } +#endif + (void)config; (void)gatewayIP; (void)mask; + return -1; +} + +int addDhcpReservation(void *config, const uint8_t *mac, const char *reservationIP) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + ether_addr_t client; + memcpy(&client, mac, 6); + struct in_addr res; + if (inet_pton(AF_INET, reservationIP, &res) != 1) return (int)VMNET_INVALID_ARGUMENT; + vmnet_return_t rc = vmnet_network_configuration_add_dhcp_reservation( + (vmnet_network_configuration_ref)config, &client, &res); + return rc == VMNET_SUCCESS ? 0 : (int)rc; + } +#endif + (void)config; (void)mac; (void)reservationIP; + return -1; +} + +void *newNetwork(void *config, int *status) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_return_t s = VMNET_SUCCESS; + vmnet_network_ref network = vmnet_network_create( + (vmnet_network_configuration_ref)config, &s); + if (status) *status = (int)s; + return network; + } +#endif + if (status) *status = -1; + return NULL; +} + +void releaseNetwork(void *network) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (network) CFRelease((vmnet_network_ref)network); +#else + (void)network; +#endif +} diff --git a/vmnet/vmnet.go b/vmnet/vmnet.go new file mode 100644 index 00000000..c727d4d1 --- /dev/null +++ b/vmnet/vmnet.go @@ -0,0 +1,160 @@ +// Package vmnet wraps the macOS 26 vmnet_network API: +// vmnet_network_configuration_create, set_ipv4_subnet, +// add_dhcp_reservation, and vmnet_network_create. It is consumed by +// vz.VmnetNetworkDeviceAttachment to construct VZ networks with +// caller-controlled subnet, DHCP reservations, etc. +// +// All APIs require macOS 26 or later. On older systems the +// constructors return an error. +package vmnet + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +#cgo darwin LDFLAGS: -lobjc -framework Foundation -framework CoreFoundation -framework vmnet +# include "cgo.h" +*/ +import "C" + +import ( + "errors" + "fmt" + "net" + "net/netip" + "unsafe" + + "github.com/Code-Hex/vz/v3/internal/objc" +) + +// Mode is the vmnet operating mode. Values mirror operating_modes_t +// in . +type Mode int + +const ( + // HostMode creates an isolated network shared between VMs on the + // host. No outside connectivity. + HostMode Mode = 1000 + // SharedMode adds Apple-managed NAT to the host's primary uplink. + SharedMode Mode = 1001 + // BridgedMode puts the VM on the same L2 as a physical interface. + // The configuration's external interface name must be set; + // NewNetworkConfiguration does not expose that knob. + BridgedMode Mode = 1002 +) + +// NetworkConfiguration wraps a vmnet_network_configuration_ref. Build +// it with the Set/Add methods, then pass to NewNetwork. The +// configuration is consumed by NewNetwork — do not reuse it. +type NetworkConfiguration struct { + *objc.Pointer +} + +// NewNetworkConfiguration creates a vmnet network configuration in +// the given mode. +// +// Requires macOS 26+. +func NewNetworkConfiguration(mode Mode) (*NetworkConfiguration, error) { + var status C.int + ptr := C.newNetworkConfiguration(C.int(mode), &status) + if ptr == nil { + return nil, fmt.Errorf("vmnet_network_configuration_create failed: status=%d", int(status)) + } + c := &NetworkConfiguration{Pointer: objc.NewPointer(ptr)} + objc.SetFinalizer(c, func(self *NetworkConfiguration) { + if p := objc.Ptr(self); p != nil { + C.releaseNetworkConfiguration(p) + } + }) + return c, nil +} + +// SetIPv4Subnet configures the IPv4 subnet for the network. +// +// The argument is a normal netip.Prefix; the network base is +// normalized internally to the first usable host of the range +// (e.g. 192.168.200.0/24 → gateway 192.168.200.1), which is what +// vmnet's underlying API requires. Apple's docs call the parameter +// "subnet_addr" but the actual semantic — confirmed by +// round-tripping through vmnet_network_get_ipv4_subnet — is the +// gateway IP. +func (c *NetworkConfiguration) SetIPv4Subnet(subnet netip.Prefix) error { + if !subnet.Addr().Is4() { + return fmt.Errorf("vmnet: SetIPv4Subnet requires an IPv4 prefix, got %s", subnet) + } + if !subnet.IsValid() { + return errors.New("vmnet: SetIPv4Subnet got an invalid Prefix") + } + // Normalize: gateway is the first host of the network. + gateway := subnet.Masked().Addr().Next() + mask := net.IP(net.CIDRMask(subnet.Bits(), 32)).String() + + gatewayCStr := C.CString(gateway.String()) + defer C.free(unsafe.Pointer(gatewayCStr)) + maskCStr := C.CString(mask) + defer C.free(unsafe.Pointer(maskCStr)) + + if rc := C.setIPv4Subnet(objc.Ptr(c), gatewayCStr, maskCStr); rc != 0 { + return fmt.Errorf("vmnet_network_configuration_set_ipv4_subnet failed: status=%d", int(rc)) + } + return nil +} + +// AddDhcpReservation pins the given MAC address to the given IPv4 +// reservation. vmnet's DHCP server will then serve the reservation +// IP to clients matching the MAC. +func (c *NetworkConfiguration) AddDhcpReservation(client net.HardwareAddr, reservation netip.Addr) error { + if len(client) != 6 { + return fmt.Errorf("vmnet: AddDhcpReservation requires a 6-byte MAC address, got %d bytes", len(client)) + } + if !reservation.Is4() { + return fmt.Errorf("vmnet: AddDhcpReservation requires an IPv4 reservation, got %s", reservation) + } + var mac [6]C.uint8_t + for i, b := range client { + mac[i] = C.uint8_t(b) + } + ipCStr := C.CString(reservation.String()) + defer C.free(unsafe.Pointer(ipCStr)) + + if rc := C.addDhcpReservation(objc.Ptr(c), &mac[0], ipCStr); rc != 0 { + return fmt.Errorf("vmnet_network_configuration_add_dhcp_reservation failed: status=%d", int(rc)) + } + return nil +} + +// Network wraps a vmnet_network_ref. +type Network struct { + *objc.Pointer +} + +// NewNetwork creates a vmnet network from the given configuration. +// The configuration is consumed: do not reuse it after this call. +// +// Requires macOS 26+. +func NewNetwork(config *NetworkConfiguration) (*Network, error) { + var status C.int + ptr := C.newNetwork(objc.Ptr(config), &status) + if ptr == nil { + return nil, fmt.Errorf("vmnet_network_create failed: status=%d", int(status)) + } + return newNetwork(objc.NewPointer(ptr)), nil +} + +// NewNetworkFromPointer wraps an existing vmnet_network_ref into a +// Network. The caller is responsible for ensuring the supplied +// pointer is +1 retained — the returned Network takes ownership of +// that retain count and will CFRelease it when garbage-collected. +// Used by vz.VmnetNetworkDeviceAttachment.Network() to materialize +// a Go wrapper around the attachment's underlying network. +func NewNetworkFromPointer(ptr *objc.Pointer) *Network { + return newNetwork(ptr) +} + +func newNetwork(ptr *objc.Pointer) *Network { + n := &Network{Pointer: ptr} + objc.SetFinalizer(n, func(self *Network) { + if p := objc.Ptr(self); p != nil { + C.releaseNetwork(p) + } + }) + return n +}