From 9fdf26689d70c73dfe78a946a2f45ae4b76e01e2 Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Fri, 21 Nov 2025 16:49:51 +0900 Subject: [PATCH 1/5] feat: add `VZVmnetNetworkDeviceAttachment` support (macOS 26.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `VZVmnetNetworkDeviceAttachment` does not require the `com.apple.vm.networking` entitlement nor root privileges. `HostMode` and `SharedMode` are supported. In order for multiple VMs to communicate with each other in SharedMode, they must be started in the same process and the same `VmnetNetwork` must be passed to `NewVmnetNetworkDeviceAttachment()` to create an attachment. Add: - `VmnetReturn`: - `ErrVmnetSuccess` - ... - `VmnetMode`: - `HostMode` - `SharedMode` - `BridgedMode`(definition only since not supported. marked as deprecated) - `VmnetNetworkConfiguration`: `NewVmnetNetworkConfiguration()`,   The use of the instance method group is still unknown. Setting subnet seems to trigger disabling DHCP, etc. - `VmnetNetwork`: `NewVmnetNetwork()`, some APIs which using `xpc_object_t` are not implemented. - `VmnetNetworkDeviceAttachment`: `NewVmnetNetworkDeviceAttachment()` see: https://developer.apple.com/documentation/virtualization/vzvmnetnetworkdeviceattachment?language=objc change `MACAddress.EthernetAddress()` to `MACAddress.ethernetAddress()` to avoid export C type from Go Signed-off-by: Norio Nomura --- go.mod | 2 +- network.go | 56 +++++- virtualization_11.h | 1 + virtualization_11.m | 12 ++ virtualization_26.h | 39 +++++ virtualization_26.m | 204 ++++++++++++++++++++++ virtualization_helper.h | 7 + vmnet.go | 372 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 691 insertions(+), 2 deletions(-) create mode 100644 virtualization_26.h create mode 100644 virtualization_26.m create mode 100644 vmnet.go diff --git a/go.mod b/go.mod index c8156c63..695db02d 100644 --- a/go.mod +++ b/go.mod @@ -8,4 +8,4 @@ require ( golang.org/x/mod v0.22.0 ) -require golang.org/x/sys v0.36.0 // indirect +require golang.org/x/sys v0.36.0 diff --git a/network.go b/network.go index c3ecddc8..38c9153a 100644 --- a/network.go +++ b/network.go @@ -2,9 +2,10 @@ 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 "virtualization_11.h" # include "virtualization_13.h" +# include "virtualization_26.h" */ import "C" import ( @@ -260,6 +261,55 @@ func (f *FileHandleNetworkDeviceAttachment) MaximumTransmissionUnit() int { return f.mtu } +// VmnetNetworkDeviceAttachment represents a vmnet network device attachment. +// +// This attachment is used to connect a virtual machine to a vmnet network. +// The attachment is created with a VmnetNetwork and can be used with a VirtioNetworkDeviceConfiguration. +// see: https://developer.apple.com/documentation/virtualization/vzvmnetnetworkdeviceattachment?language=objc +// +// This is only supported on macOS 26 and newer, error will +// be returned on older versions. +type VmnetNetworkDeviceAttachment struct { + *pointer + + *baseNetworkDeviceAttachment +} + +func (*VmnetNetworkDeviceAttachment) String() string { + return "VmnetNetworkDeviceAttachment" +} + +func (v *VmnetNetworkDeviceAttachment) Network() *VmnetNetwork { + network := C.VZVmnetNetworkDeviceAttachment_network(objc.Ptr(v)) + return &VmnetNetwork{ + pointer: objc.NewPointer(network), + } +} + +var _ NetworkDeviceAttachment = (*VmnetNetworkDeviceAttachment)(nil) + +// NewVmnetNetworkDeviceAttachment creates a new VmnetNetworkDeviceAttachment with network. +// +// This is only supported on macOS 26 and newer, error will +// be returned on older versions. +func NewVmnetNetworkDeviceAttachment(network *VmnetNetwork) (*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 { @@ -373,6 +423,10 @@ func (m *MACAddress) String() string { return cstring.String() } +func (m *MACAddress) ethernetAddress() C.ether_addr_t { + return C.getVZMACAddressEthernetAddress(objc.Ptr(m)) +} + func (m *MACAddress) HardwareAddr() net.HardwareAddr { hw, _ := net.ParseMAC(m.String()) return hw diff --git a/virtualization_11.h b/virtualization_11.h index f9f9fdc6..0dd07f3e 100644 --- a/virtualization_11.h +++ b/virtualization_11.h @@ -105,6 +105,7 @@ void *newVZVirtioSocketDeviceConfiguration(); void *newVZMACAddress(const char *macAddress); void *newRandomLocallyAdministeredVZMACAddress(); const char *getVZMACAddressString(void *macAddress); +ether_addr_t getVZMACAddressEthernetAddress(void *macAddress); void *newVZVirtioSocketListener(uintptr_t cgoHandle); void *VZVirtualMachine_socketDevices(void *machine); void VZVirtioSocketDevice_setSocketListenerForPort(void *socketDevice, void *vmQueue, void *listener, uint32_t port); diff --git a/virtualization_11.m b/virtualization_11.m index bab7e292..53508527 100644 --- a/virtualization_11.m +++ b/virtualization_11.m @@ -881,6 +881,18 @@ VZVirtioSocketConnectionFlat convertVZVirtioSocketConnection2Flat(void *connecti RAISE_UNSUPPORTED_MACOS_EXCEPTION(); } +/*! + @abstract The address represented as an ether_addr_t. + */ +ether_addr_t getVZMACAddressEthernetAddress(void *macAddress) +{ + if (@available(macOS 11, *)) { + return [(VZMACAddress *)macAddress ethernetAddress]; + } + + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + /*! @abstract Create a valid, random, unicast, locally administered address. @discussion The generated address is not guaranteed to be unique. diff --git a/virtualization_26.h b/virtualization_26.h new file mode 100644 index 00000000..aa4af767 --- /dev/null +++ b/virtualization_26.h @@ -0,0 +1,39 @@ +// +// virtualization_26.h +// +// Created by codehex. +// + +#pragma once + +// FIXME(codehex): this is dirty hack to avoid clang-format error like below +// "Configuration file(s) do(es) not support C++: /github.com/Code-Hex/vz/.clang-format" +#define NSURLComponents NSURLComponents + +#import "virtualization_helper.h" +#import +#import + +/* exported from cgo */ + +/* macOS 26 API */ +// VZVmnetNetworkConfiguration +void *VZVmnetNetworkConfigurationCreate(uint32_t mode, uint32_t *status); +uint32_t VZVmnetNetworkConfiguration_setExternalInterface(void *config, const char *ifname); +void VZVmnetNetworkConfiguration_disableNat44(void *config); +void VZVmnetNetworkConfiguration_disableNat66(void *config); +void VZVmnetNetworkConfiguration_disableDhcp(void *config); +void VZVmnetNetworkConfiguration_disableDnsProxy(void *config); +void VZVmnetNetworkConfiguration_disableRouterAdvertisement(void *config); +uint32_t VZVmnetNetworkConfiguration_setIPv4Subnet(void *config, struct in_addr const *subnet_addr, struct in_addr const *subnet_mask); +uint32_t VZVmnetNetworkConfiguration_setIPv6Prefix(void *config, struct in6_addr const *prefix, uint8_t prefix_len); +uint32_t VZVmnetNetworkConfiguration_addPortForwardingRule(void *config, uint8_t protocol, sa_family_t address_family, uint16_t internal_port, uint16_t external_port, void const *internal_address); +uint32_t VZVmnetNetworkConfiguration_addDhcpReservation(void *config, ether_addr_t const *client, struct in_addr const *reservation); +uint32_t VZVmnetNetworkConfiguration_setMtu(void *config, uint32_t mtu); +// vmnet_network +void *VZVmnetNetworkCreate(void *config, uint32_t *status); +void VZVmnetNetwork_getIPv6Prefix(void *network, struct in6_addr *prefix, uint8_t *prefix_len); +void VZVmnetNetwork_getIPv4Subnet(void *network, struct in_addr *subnet, struct in_addr *mask); +// VZVmnetNetworkDeviceAttachment +void *newVZVmnetNetworkDeviceAttachment(void *network); +void *VZVmnetNetworkDeviceAttachment_network(void *attachment); diff --git a/virtualization_26.m b/virtualization_26.m new file mode 100644 index 00000000..94f95c71 --- /dev/null +++ b/virtualization_26.m @@ -0,0 +1,204 @@ +// +// virtualization_11.m +// +// Created by codehex. +// + +#import "virtualization_26.h" + +// VZVmnetNetworkConfiguration +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_create(_:_:)?language=objc +void *VZVmnetNetworkConfigurationCreate(uint32_t mode, uint32_t *status) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_configuration_create(mode, status); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_external_interface(_:_:)?language=objc +uint32_t VZVmnetNetworkConfiguration_setExternalInterface(void *config, const char *ifname) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_configuration_set_external_interface((vmnet_network_configuration_ref)config, ifname); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_nat44(_:)?language=objc +void VZVmnetNetworkConfiguration_disableNat44(void *config) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_configuration_disable_nat44((vmnet_network_configuration_ref)config); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_nat66(_:)?language=objc +void VZVmnetNetworkConfiguration_disableNat66(void *config) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_configuration_disable_nat66((vmnet_network_configuration_ref)config); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_dhcp(_:)?language=objc +void VZVmnetNetworkConfiguration_disableDhcp(void *config) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_configuration_disable_dhcp((vmnet_network_configuration_ref)config); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_dns_proxy(_:)?language=objc +void VZVmnetNetworkConfiguration_disableDnsProxy(void *config) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_configuration_disable_dns_proxy((vmnet_network_configuration_ref)config); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_router_advertisement(_:)?language=objc +void VZVmnetNetworkConfiguration_disableRouterAdvertisement(void *config) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_configuration_disable_router_advertisement((vmnet_network_configuration_ref)config); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_ipv4_subnet(_:_:_:)?language=objc +uint32_t VZVmnetNetworkConfiguration_setIPv4Subnet(void *config, struct in_addr const *subnet_addr, struct in_addr const *subnet_mask) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_configuration_set_ipv4_subnet((vmnet_network_configuration_ref)config, subnet_addr, subnet_mask); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_ipv6_prefix(_:_:_:)?language=objc +uint32_t VZVmnetNetworkConfiguration_setIPv6Prefix(void *config, struct in6_addr const *prefix, uint8_t prefix_len) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_configuration_set_ipv6_prefix((vmnet_network_configuration_ref)config, prefix, prefix_len); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_add_port_forwarding_rule(_:_:_:_:_:_:)?language=objc +uint32_t VZVmnetNetworkConfiguration_addPortForwardingRule(void *config, uint8_t protocol, sa_family_t address_family, uint16_t internal_port, uint16_t external_port, void const *internal_address) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_configuration_add_port_forwarding_rule((vmnet_network_configuration_ref)config, protocol, address_family, internal_port, external_port, internal_address); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_add_dhcp_reservation(_:_:_:)?language=objc +uint32_t VZVmnetNetworkConfiguration_addDhcpReservation(void *config, ether_addr_t const *client, struct in_addr const *reservation) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_configuration_add_dhcp_reservation((vmnet_network_configuration_ref)config, client, reservation); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_mtu(_:_:)?language=objc +uint32_t VZVmnetNetworkConfiguration_setMtu(void *config, uint32_t mtu) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_configuration_set_mtu((vmnet_network_configuration_ref)config, mtu); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// vmnet_network +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_create(_:_:)?language=objc +void *VZVmnetNetworkCreate(void *config, uint32_t *status) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_create((vmnet_network_configuration_ref)config, status); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_get_ipv6_prefix(_:_:_:)?language=objc +void VZVmnetNetwork_getIPv6Prefix(void *network, struct in6_addr *prefix, uint8_t *prefix_len) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_get_ipv6_prefix((vmnet_network_ref)network, prefix, prefix_len); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_get_ipv4_subnet(_:_:_:)?language=objc +void VZVmnetNetwork_getIPv4Subnet(void *network, struct in_addr *subnet, struct in_addr *mask) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_get_ipv4_subnet((vmnet_network_ref)network, subnet, mask); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// VZVmnetNetworkDeviceAttachment +// see: https://developer.apple.com/documentation/virtualization/vzvmnetnetworkdeviceattachment/init(network:)?language=objc +void *newVZVmnetNetworkDeviceAttachment(void *network) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return [[VZVmnetNetworkDeviceAttachment alloc] initWithNetwork:network]; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/virtualization/vzvmnetnetworkdeviceattachment/network?language=objc +void *VZVmnetNetworkDeviceAttachment_network(void *attachment) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return [(VZVmnetNetworkDeviceAttachment *)attachment network]; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} diff --git a/virtualization_helper.h b/virtualization_helper.h index 7914efdd..2f254668 100644 --- a/virtualization_helper.h +++ b/virtualization_helper.h @@ -46,6 +46,13 @@ NSDictionary *dumpProcessinfo(); #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.go b/vmnet.go new file mode 100644 index 00000000..15fe143e --- /dev/null +++ b/vmnet.go @@ -0,0 +1,372 @@ +package vz + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +#cgo darwin LDFLAGS: -lobjc -framework Foundation -framework vmnet +# include "virtualization_26.h" +*/ +import "C" +import ( + "errors" + "fmt" + "net" + "net/netip" + "unsafe" + + "github.com/Code-Hex/vz/v3/internal/objc" + "golang.org/x/sys/unix" +) + +// The status code returning the result of vmnet operations. +// see: https://developer.apple.com/documentation/vmnet/vmnet_return_t?language=objc +type VmnetReturn C.uint32_t + +const ( + ErrVmnetSuccess VmnetReturn = C.VMNET_SUCCESS // VMNET_SUCCESS Successfully completed. + ErrVmnetFailure VmnetReturn = C.VMNET_FAILURE // VMNET_FAILURE General failure. + ErrVmnetMemFailure VmnetReturn = C.VMNET_MEM_FAILURE // VMNET_MEM_FAILURE Memory allocation failure. + ErrVmnetInvalidArgument VmnetReturn = C.VMNET_INVALID_ARGUMENT // VMNET_INVALID_ARGUMENT Invalid argument specified. + ErrVmnetSetupIncomplete VmnetReturn = C.VMNET_SETUP_INCOMPLETE // VMNET_SETUP_INCOMPLETE Interface setup is not complete. + ErrVmnetInvalidAccess VmnetReturn = C.VMNET_INVALID_ACCESS // VMNET_INVALID_ACCESS Permission denied. + ErrVmnetPacketTooBig VmnetReturn = C.VMNET_PACKET_TOO_BIG // VMNET_PACKET_TOO_BIG Packet size larger than MTU. + ErrVmnetBufferExhausted VmnetReturn = C.VMNET_BUFFER_EXHAUSTED // VMNET_BUFFER_EXHAUSTED Buffers exhausted in kernel. + ErrVmnetTooManyPackets VmnetReturn = C.VMNET_TOO_MANY_PACKETS // VMNET_TOO_MANY_PACKETS Packet count exceeds limit. + ErrVmnetSharingServiceBusy VmnetReturn = C.VMNET_SHARING_SERVICE_BUSY // VMNET_SHARING_SERVICE_BUSY Vmnet Interface cannot be started as conflicting sharing service is in use. + ErrVmnetNotAuthorized VmnetReturn = C.VMNET_NOT_AUTHORIZED // VMNET_NOT_AUTHORIZED The operation could not be completed due to missing authorization. +) + +var _ error = VmnetReturn(0) + +func (e VmnetReturn) Error() string { + switch e { + case ErrVmnetSuccess: + return "Vmnet: Successfully completed" + case ErrVmnetFailure: + return "Vmnet: Failure" + case ErrVmnetMemFailure: + return "Vmnet: Memory allocation failure" + case ErrVmnetInvalidArgument: + return "Vmnet: Invalid argument specified" + case ErrVmnetSetupIncomplete: + return "Vmnet: Interface setup is not complete" + case ErrVmnetInvalidAccess: + return "Vmnet: Permission denied" + case ErrVmnetPacketTooBig: + return "Vmnet: Packet size larger than MTU" + case ErrVmnetBufferExhausted: + return "Vmnet: Buffers exhausted in kernel" + case ErrVmnetTooManyPackets: + return "Vmnet: Packet count exceeds limit" + case ErrVmnetSharingServiceBusy: + return "Vmnet: Vmnet Interface cannot be started as conflicting sharing service is in use" + case ErrVmnetNotAuthorized: + return "Vmnet: The operation could not be completed due to missing authorization" + default: + return fmt.Sprintf("Vmnet: Unknown error %d", uint32(e)) + } +} + +// VmnetMode defines the mode of a vmnet network. +// see: https://developer.apple.com/documentation/vmnet/operating_modes_t?language=objc +type VmnetMode uint32 + +const ( + HostMode VmnetMode = C.VMNET_HOST_MODE + SharedMode VmnetMode = C.VMNET_SHARED_MODE + // Deprecated: BridgedMode is not supported by NewVmnetNetworkConfiguration + BridgedMode VmnetMode = C.VMNET_BRIDGED_MODE +) + +// VmnetNetworkConfiguration is configuration for a vmnet network. +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_create(_:_:)?language=objc +type VmnetNetworkConfiguration struct { + *pointer +} + +// NewVmnetNetworkConfiguration creates a new VmnetNetworkConfiguration with mode. +// This is only supported on macOS 26 and newer, error will +// be returned on older versions. +// BridgedMode is not supported by this function. +func NewVmnetNetworkConfiguration(mode VmnetMode) (*VmnetNetworkConfiguration, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + var status VmnetReturn + ptr := C.VZVmnetNetworkConfigurationCreate( + C.uint32_t(mode), + (*C.uint32_t)(unsafe.Pointer(&status)), + ) + if !errors.Is(status, ErrVmnetSuccess) { + return nil, fmt.Errorf("failed to create VmnetNetworkConfiguration: %w", status) + } + config := &VmnetNetworkConfiguration{ + pointer: objc.NewPointer(ptr), + } + objc.SetFinalizer(config, func(self *VmnetNetworkConfiguration) { + objc.Release(self) + }) + return config, nil +} + +// SetExternalInterface sets the external interface of a vmnet network. +// This is only available to networks of SharedMode. +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_external_interface(_:_:)?language=objc +func (c *VmnetNetworkConfiguration) SetExternalInterface(ifname string) error { + cIfname := C.CString(ifname) + defer C.free(unsafe.Pointer(cIfname)) + + status := C.VZVmnetNetworkConfiguration_setExternalInterface( + objc.Ptr(c), + cIfname, + ) + if !errors.Is(VmnetReturn(status), ErrVmnetSuccess) { + return fmt.Errorf("failed to set external interface: %w", VmnetReturn(status)) + } + return nil +} + +// DisableNat44 disables NAT44 on a network. +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_nat44(_:)?language=objc +func (c *VmnetNetworkConfiguration) DisableNat44() { + C.VZVmnetNetworkConfiguration_disableNat44(objc.Ptr(c)) +} + +// DisableNat66 disables NAT66 on a network. +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_nat66(_:)?language=objc +func (c *VmnetNetworkConfiguration) DisableNat66() { + C.VZVmnetNetworkConfiguration_disableNat66(objc.Ptr(c)) +} + +// DisableDhcp disables DHCP server on a network. +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_dhcp(_:)?language=objc +func (c *VmnetNetworkConfiguration) DisableDhcp() { + C.VZVmnetNetworkConfiguration_disableDhcp(objc.Ptr(c)) +} + +// DisableDnsProxy disables DNS proxy on a network. +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_dns_proxy(_:)?language=objc +func (c *VmnetNetworkConfiguration) DisableDnsProxy() { + C.VZVmnetNetworkConfiguration_disableDnsProxy(objc.Ptr(c)) +} + +// DisableRouterAdvertisement disables router advertisement on a network. +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_router_advertisement(_:)?language=objc +func (c *VmnetNetworkConfiguration) DisableRouterAdvertisement() { + C.VZVmnetNetworkConfiguration_disableRouterAdvertisement(objc.Ptr(c)) +} + +// SetIPv4Subnet configures the IPv4 address for a vmnet network. +// Note that the first, second, and last addresses of the range are reserved. +// The second address is reserved for the host, the first and last are not assignable to any node. +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_ipv4_subnet(_:_:_:)?language=objc +func (c *VmnetNetworkConfiguration) SetIPv4Subnet(subnet netip.Prefix) error { + if !subnet.Addr().Is4() { + return fmt.Errorf("subnet is not ipv4") + } + ip := subnet.Addr().As4() + mask := net.CIDRMask(subnet.Bits(), 32) + var cSubnet C.struct_in_addr + var cMask C.struct_in_addr + + copy((*[4]byte)(unsafe.Pointer(&cSubnet))[:], ip[:]) + copy((*[4]byte)(unsafe.Pointer(&cMask))[:], mask[:]) + + status := C.VZVmnetNetworkConfiguration_setIPv4Subnet( + objc.Ptr(c), + &cSubnet, + &cMask, + ) + if !errors.Is(VmnetReturn(status), ErrVmnetSuccess) { + return fmt.Errorf("failed to set ipv4 subnet: %d", VmnetReturn(status)) + } + return nil +} + +// SetIPv6Prefix configures the IPv6 prefix for a vmnet network. +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_ipv6_prefix(_:_:_:)?language=objc +func (c *VmnetNetworkConfiguration) SetIPv6Prefix(prefix netip.Prefix) error { + if !prefix.Addr().Is6() { + return fmt.Errorf("prefix is not ipv6") + } + ip := prefix.Addr().As16() + var cPrefix C.struct_in6_addr + + copy((*[16]byte)(unsafe.Pointer(&cPrefix))[:], ip[:]) + + status := C.VZVmnetNetworkConfiguration_setIPv6Prefix( + objc.Ptr(c), + &cPrefix, + C.uint8_t(prefix.Bits()), + ) + if !errors.Is(VmnetReturn(status), ErrVmnetSuccess) { + return fmt.Errorf("failed to set ipv6 prefix: %w", VmnetReturn(status)) + } + return nil +} + +// AddPortForwardingRule configures a port forwarding rule for the vmnet network. +// These rules will not be able to be removed or queried until network has been started. +// To do that, use `vmnet_interface_remove_ip_forwarding_rule` or +// `vmnet_interface_get_ip_port_forwarding_rules` C API directly. +// (`vmnet_interface` related functionality not implemented in this package yet) +// +// protocol must be either IPPROTO_TCP or IPPROTO_UDP +// addressFamily must be either AF_INET or AF_INET6 +// internalPort is the TCP or UDP port that forwarded traffic should be redirected to. +// externalPort is the TCP or UDP port on the outside network that should be redirected from. +// internalAddress is the IPv4 or IPv6 address of the machine on the internal network that should receive the forwarded traffic. +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_add_port_forwarding_rule(_:_:_:_:_:_:)?language=objc +func (c *VmnetNetworkConfiguration) AddPortForwardingRule(protocol uint8, addressFamily uint8, internalPort uint16, externalPort uint16, internalAddress netip.Addr) error { + var address unsafe.Pointer + switch addressFamily { + case unix.AF_INET: + if !internalAddress.Is4() { + return fmt.Errorf("internal address is not ipv4") + } + var inAddr C.struct_in_addr + ip := internalAddress.As4() + copy((*[4]byte)(unsafe.Pointer(&inAddr))[:], ip[:]) + address = unsafe.Pointer(&inAddr) + case unix.AF_INET6: + if !internalAddress.Is6() { + return fmt.Errorf("internal address is not ipv6") + } + var in6Addr C.struct_in6_addr + ip := internalAddress.As16() + copy((*[16]byte)(unsafe.Pointer(&in6Addr))[:], ip[:]) + address = unsafe.Pointer(&in6Addr) + default: + return fmt.Errorf("unsupported address family: %d", addressFamily) + } + status := C.VZVmnetNetworkConfiguration_addPortForwardingRule( + objc.Ptr(c), + C.uint8_t(protocol), + C.sa_family_t(addressFamily), + C.uint16_t(internalPort), + C.uint16_t(externalPort), + address, + ) + if !errors.Is(VmnetReturn(status), ErrVmnetSuccess) { + return fmt.Errorf("failed to add port forwarding rule: %w", VmnetReturn(status)) + } + return nil +} + +// AddDhcpReservation configures a new DHCP reservation for the vmnet network. +// client is the MAC address for which the DHCP address is reserved. +// reservation is the DHCP IPv4 address to be reserved. +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_add_dhcp_reservation(_:_:_:)?language=objc +func (c *VmnetNetworkConfiguration) AddDhcpReservation(client *MACAddress, reservation netip.Addr) error { + if !reservation.Is4() { + return fmt.Errorf("reservation is not ipv4") + } + ip := reservation.As4() + var cReservation C.struct_in_addr + + cClient := client.ethernetAddress() + copy((*[4]byte)(unsafe.Pointer(&cReservation))[:], ip[:]) + + status := C.VZVmnetNetworkConfiguration_addDhcpReservation( + objc.Ptr(c), + &cClient, + &cReservation, + ) + if !errors.Is(VmnetReturn(status), ErrVmnetSuccess) { + return fmt.Errorf("failed to add dhcp reservation: %w", VmnetReturn(status)) + } + return nil +} + +// SetMtu configures the maximum transmission unit (MTU) for the vmnet network. +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_mtu(_:_:)?language=objc +func (c *VmnetNetworkConfiguration) SetMtu(mtu uint32) error { + status := C.VZVmnetNetworkConfiguration_setMtu( + objc.Ptr(c), + C.uint32_t(mtu), + ) + if !errors.Is(VmnetReturn(status), ErrVmnetSuccess) { + return fmt.Errorf("failed to set mtu: %w", VmnetReturn(status)) + } + return nil +} + +// VmnetNetwork represents a vmnet network. +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_create(_:_:)?language=objc +type VmnetNetwork struct { + *pointer +} + +// IPv6Prefix returns the IPv6 prefix of the network. +func (n *VmnetNetwork) IPv6Prefix() (netip.Prefix, error) { + var prefix C.struct_in6_addr + var prefixLen C.uint8_t + + C.VZVmnetNetwork_getIPv6Prefix(objc.Ptr(n), &prefix, &prefixLen) + + addr := in6AddrToNetipAddr(prefix) + pfx := netip.PrefixFrom(addr, int(prefixLen)) + + if !pfx.IsValid() { + return netip.Prefix{}, fmt.Errorf("invalid ipv6 prefix") + } + return pfx, nil +} + +func in6AddrToNetipAddr(a C.struct_in6_addr) netip.Addr { + p := (*[16]byte)(unsafe.Pointer(&a)) + return netip.AddrFrom16(*p) +} + +// IPv4Subnet returns the IPv4 subnet of the network. +func (n *VmnetNetwork) IPv4Subnet() (subnet netip.Prefix, err error) { + var cSubnet C.struct_in_addr + var cMask C.struct_in_addr + + C.VZVmnetNetwork_getIPv4Subnet(objc.Ptr(n), &cSubnet, &cMask) + + sIP := inAddrToNetipAddr(cSubnet) + mIP := inAddrToIP(cMask) + + // netmask → prefix length + ones, bits := net.IPMask(mIP.To4()).Size() + if bits != 32 { + return netip.Prefix{}, fmt.Errorf("unexpected mask size") + } + + return netip.PrefixFrom(sIP, ones), nil +} + +func inAddrToNetipAddr(a C.struct_in_addr) netip.Addr { + p := (*[4]byte)(unsafe.Pointer(&a)) + return netip.AddrFrom4(*p) +} + +func inAddrToIP(a C.struct_in_addr) net.IP { + p := (*[4]byte)(unsafe.Pointer(&a)) + return net.IPv4(p[0], p[1], p[2], p[3]) +} + +// NewVmnetNetwork creates a new VmnetNetwork with config. +// This is only supported on macOS 26 and newer, error will +// be returned on older versions. +func NewVmnetNetwork(config *VmnetNetworkConfiguration) (*VmnetNetwork, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + + var status VmnetReturn + ptr := C.VZVmnetNetworkCreate( + objc.Ptr(config), + (*C.uint32_t)(unsafe.Pointer(&status)), + ) + if !errors.Is(status, ErrVmnetSuccess) { + return nil, fmt.Errorf("failed to create VmnetNetwork: %w", status) + } + network := &VmnetNetwork{ + pointer: objc.NewPointer(ptr), + } + objc.SetFinalizer(network, func(self *VmnetNetwork) { + objc.Release(self) + }) + return network, nil +} From 2529cae1a35bdb160de7ea44bce6103053eee4a0 Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Sat, 22 Nov 2025 20:51:32 +0900 Subject: [PATCH 2/5] feat(vmnet_test.go): add unit tests Add: - `TestVmnetSharedModeAllowsCommunicationBetweenMultipleVMs()` - `Container.DetectIPv4()` Move `Container.exec()` from `shared_directory_arm64_test.go` to `virtualization_test.go` Signed-off-by: Norio Nomura --- shared_directory_arm64_test.go | 19 --------- virtualization_test.go | 41 ++++++++++++++++++++ vmnet_test.go | 70 ++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 19 deletions(-) create mode 100644 vmnet_test.go diff --git a/shared_directory_arm64_test.go b/shared_directory_arm64_test.go index 9cc2fcf6..dffee603 100644 --- a/shared_directory_arm64_test.go +++ b/shared_directory_arm64_test.go @@ -156,25 +156,6 @@ func rosettaConfiguration(t *testing.T, o vz.LinuxRosettaCachingOptions) func(*v } } -func (c *Container) exec(t *testing.T, cmds ...string) { - t.Helper() - for _, cmd := range cmds { - session := c.NewSession(t) - defer session.Close() - output, err := session.CombinedOutput(cmd) - if err != nil { - if len(output) > 0 { - t.Fatalf("failed to run command %q: %v, outputs:\n%s", cmd, err, string(output)) - } else { - t.Fatalf("failed to run command %q: %v", cmd, err) - } - } - if len(output) > 0 { - t.Logf("command %q outputs:\n%s", cmd, string(output)) - } - } -} - // rosettad's default unix socket const rosettadDefaultUnixSocket = "~/.cache/rosettad/uds/rosetta.sock" diff --git a/virtualization_test.go b/virtualization_test.go index 9be7357d..1d78b33d 100644 --- a/virtualization_test.go +++ b/virtualization_test.go @@ -7,6 +7,7 @@ import ( "math" "net" "os" + "regexp" "runtime" "syscall" "testing" @@ -110,6 +111,46 @@ func (c *Container) NewSession(t *testing.T) *ssh.Session { return sshSession } +func (c *Container) DetectIPv4(t *testing.T, ifname string) string { + sshSession, err := c.Client.NewSession() + if err != nil { + t.Fatal(err) + } + defer sshSession.Close() + + output, err := sshSession.Output(fmt.Sprintf("ip address show dev %s scope global", ifname)) + if err != nil { + t.Fatal(err) + } + re := regexp.MustCompile(`(?ms)^\s+inet\s+([0-9.]+)/`) + matches := re.FindStringSubmatch(string(output)) + if len(matches) == 2 { + return matches[1] + } + t.Fatalf("failed to parse IP address from output: %s", output) + + return "" +} + +func (c *Container) exec(t *testing.T, cmds ...string) { + t.Helper() + for _, cmd := range cmds { + session := c.NewSession(t) + defer session.Close() + output, err := session.CombinedOutput(cmd) + if err != nil { + if len(output) > 0 { + t.Fatalf("failed to run command %q: %v, outputs:\n%s", cmd, err, string(output)) + } else { + t.Fatalf("failed to run command %q: %v", cmd, err) + } + } + if len(output) > 0 { + t.Logf("command %q outputs:\n%s", cmd, string(output)) + } + } +} + func (c *Container) Shutdown() error { defer func() { log.Println("shutdown done") diff --git a/vmnet_test.go b/vmnet_test.go new file mode 100644 index 00000000..423da88f --- /dev/null +++ b/vmnet_test.go @@ -0,0 +1,70 @@ +package vz_test + +import ( + "log" + "testing" + + "github.com/Code-Hex/vz/v3" +) + +func TestVmnetSharedModeAllowsCommunicationBetweenMultipleVMs(t *testing.T) { + if vz.Available(26.0) { + t.Skip("VmnetSharedMode is supported from macOS 26") + } + config, err := vz.NewVmnetNetworkConfiguration(vz.SharedMode) + if err != nil { + t.Fatal(err) + } + network, err := vz.NewVmnetNetwork(config) + if err != nil { + t.Fatal(err) + } + + container1 := newVirtualizationMachine(t, ConfigureNetworkDeviceWithNetwork(network)) + container2 := newVirtualizationMachine(t, ConfigureNetworkDeviceWithNetwork(network)) + t.Cleanup(func() { + if err := container1.Shutdown(); err != nil { + log.Println(err) + } + if err := container2.Shutdown(); err != nil { + log.Println(err) + } + }) + + // Log network information + ipv4Subnet, err := network.IPv4Subnet() + if err != nil { + t.Fatal(err) + } + t.Logf("Vmnet network IPv4 subnet: %s", ipv4Subnet.String()) + prefix, err := network.IPv6Prefix() + if err != nil { + t.Fatal(err) + } + t.Logf("Vmnet network IPv6 prefix: %s", prefix.String()) + + // Detect IP addresses and test communication between VMs + container1IPv4 := container1.DetectIPv4(t, "eth0") + t.Logf("Container 1 IPv4: %s", container1IPv4) + container2IPv4 := container2.DetectIPv4(t, "eth0") + t.Logf("Container 2 IPv4: %s", container2IPv4) + container1.exec(t, "ping "+container2IPv4) + container2.exec(t, "ping "+container1IPv4) +} + +func ConfigureNetworkDeviceWithNetwork(network *vz.VmnetNetwork) func(cfg *vz.VirtualMachineConfiguration) error { + return func(cfg *vz.VirtualMachineConfiguration) error { + var configurations []*vz.VirtioNetworkDeviceConfiguration + attachment, err := vz.NewVmnetNetworkDeviceAttachment(network) + if err != nil { + return err + } + config, err := vz.NewVirtioNetworkDeviceConfiguration(attachment) + if err != nil { + return err + } + configurations = append(configurations, config) + cfg.SetNetworkDevicesVirtualMachineConfiguration(configurations) + return nil + } +} From d9ee92f2ff2df2e332c76d26ddabb7175c48a2b4 Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Sat, 22 Nov 2025 21:40:02 +0900 Subject: [PATCH 3/5] example/macOS: go mod tidy Signed-off-by: Norio Nomura --- example/macOS/go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/example/macOS/go.mod b/example/macOS/go.mod index cf289732..53a08c4e 100644 --- a/example/macOS/go.mod +++ b/example/macOS/go.mod @@ -9,4 +9,5 @@ require github.com/Code-Hex/vz/v3 v3.0.0-00010101000000-000000000000 require ( github.com/Code-Hex/go-infinity-channel v1.0.0 // indirect golang.org/x/mod v0.22.0 // indirect + golang.org/x/sys v0.36.0 // indirect ) From 74eff93da883a1d84bd6b36ad0746781fe1a7bf9 Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Wed, 26 Nov 2025 09:10:54 +0900 Subject: [PATCH 4/5] Add VmnetNetwork serialization Signed-off-by: Norio Nomura --- virtualization_26.h | 2 + virtualization_26.m | 22 +++++++++++ vmnet.go | 89 ++++++++++++++++++++++++++++++++------------- 3 files changed, 88 insertions(+), 25 deletions(-) diff --git a/virtualization_26.h b/virtualization_26.h index aa4af767..5900d1c7 100644 --- a/virtualization_26.h +++ b/virtualization_26.h @@ -32,6 +32,8 @@ uint32_t VZVmnetNetworkConfiguration_addDhcpReservation(void *config, ether_addr uint32_t VZVmnetNetworkConfiguration_setMtu(void *config, uint32_t mtu); // vmnet_network void *VZVmnetNetworkCreate(void *config, uint32_t *status); +void *VZVmnetNetworkCreateWithSerialization(CFTypeRef serialization, uint32_t *status); +CFTypeRef VZVmnetNetwork_copySerialization(void *network, uint32_t *status); void VZVmnetNetwork_getIPv6Prefix(void *network, struct in6_addr *prefix, uint8_t *prefix_len); void VZVmnetNetwork_getIPv4Subnet(void *network, struct in_addr *subnet, struct in_addr *mask); // VZVmnetNetworkDeviceAttachment diff --git a/virtualization_26.m b/virtualization_26.m index 94f95c71..f8a53bd3 100644 --- a/virtualization_26.m +++ b/virtualization_26.m @@ -156,6 +156,28 @@ uint32_t VZVmnetNetworkConfiguration_setMtu(void *config, uint32_t mtu) RAISE_UNSUPPORTED_MACOS_EXCEPTION(); } +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_create_with_serialization(_:_:)?language=objc +void *VZVmnetNetworkCreateWithSerialization(CFTypeRef serialization, uint32_t *status) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_create_with_serialization((xpc_object_t)serialization, status); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// https://developer.apple.com/documentation/vmnet/vmnet_network_copy_serialization(_:_:)?language=objc +CFTypeRef VZVmnetNetwork_copySerialization(void *network, uint32_t *status) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_copy_serialization((vmnet_network_ref)network, status); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + // see: https://developer.apple.com/documentation/vmnet/vmnet_network_get_ipv6_prefix(_:_:_:)?language=objc void VZVmnetNetwork_getIPv6Prefix(void *network, struct in6_addr *prefix, uint8_t *prefix_len) { diff --git a/vmnet.go b/vmnet.go index 15fe143e..a4a9d672 100644 --- a/vmnet.go +++ b/vmnet.go @@ -296,6 +296,70 @@ type VmnetNetwork struct { *pointer } +// NewVmnetNetwork creates a new VmnetNetwork with config. +// This is only supported on macOS 26 and newer, error will +// be returned on older versions. +func NewVmnetNetwork(config *VmnetNetworkConfiguration) (*VmnetNetwork, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + + var status VmnetReturn + ptr := C.VZVmnetNetworkCreate( + objc.Ptr(config), + (*C.uint32_t)(unsafe.Pointer(&status)), + ) + if !errors.Is(status, ErrVmnetSuccess) { + return nil, fmt.Errorf("failed to create VmnetNetwork: %w", status) + } + network := &VmnetNetwork{ + pointer: objc.NewPointer(ptr), + } + objc.SetFinalizer(network, func(self *VmnetNetwork) { + objc.Release(self) + }) + return network, nil +} + +// NewVmnetNetworkWithSerialization creates a new VmnetNetwork from a serialized representation. +// This is only supported on macOS 26 and newer, error will +// be returned on older versions. +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_create_with_serialization(_:_:)?language=objc +func NewVmnetNetworkWithSerialization(serialization uintptr) (*VmnetNetwork, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + + var status VmnetReturn + ptr := C.VZVmnetNetworkCreateWithSerialization( + C.CFTypeRef(serialization), + (*C.uint32_t)(unsafe.Pointer(&status)), + ) + if !errors.Is(status, ErrVmnetSuccess) { + return nil, fmt.Errorf("failed to create VmnetNetwork with serialization: %w", status) + } + network := &VmnetNetwork{ + pointer: objc.NewPointer(ptr), + } + objc.SetFinalizer(network, func(self *VmnetNetwork) { + objc.Release(self) + }) + return network, nil +} + +// CopySerialization returns a pointer to a serialized representation of the vmnet network. +func (n *VmnetNetwork) CopySerialization() (uintptr, error) { + var status VmnetReturn + ptr := C.VZVmnetNetwork_copySerialization( + objc.Ptr(n), + (*C.uint32_t)(unsafe.Pointer(&status)), + ) + if !errors.Is(status, ErrVmnetSuccess) { + return 0, fmt.Errorf("failed to copy serialization: %w", status) + } + return uintptr(ptr), nil +} + // IPv6Prefix returns the IPv6 prefix of the network. func (n *VmnetNetwork) IPv6Prefix() (netip.Prefix, error) { var prefix C.struct_in6_addr @@ -345,28 +409,3 @@ func inAddrToIP(a C.struct_in_addr) net.IP { p := (*[4]byte)(unsafe.Pointer(&a)) return net.IPv4(p[0], p[1], p[2], p[3]) } - -// NewVmnetNetwork creates a new VmnetNetwork with config. -// This is only supported on macOS 26 and newer, error will -// be returned on older versions. -func NewVmnetNetwork(config *VmnetNetworkConfiguration) (*VmnetNetwork, error) { - if err := macOSAvailable(26); err != nil { - return nil, err - } - - var status VmnetReturn - ptr := C.VZVmnetNetworkCreate( - objc.Ptr(config), - (*C.uint32_t)(unsafe.Pointer(&status)), - ) - if !errors.Is(status, ErrVmnetSuccess) { - return nil, fmt.Errorf("failed to create VmnetNetwork: %w", status) - } - network := &VmnetNetwork{ - pointer: objc.NewPointer(ptr), - } - objc.SetFinalizer(network, func(self *VmnetNetwork) { - objc.Release(self) - }) - return network, nil -} From 72cc1d4958b6fc27bac7209f770e79f7050b4f95 Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Wed, 26 Nov 2025 09:18:35 +0900 Subject: [PATCH 5/5] Use "192.168.x.1" to IPv4 subnet address instead of "192.168.x.0" change `VmnetNetworkConfiguration.SetIPv4Subnet()` Signed-off-by: Norio Nomura --- vmnet.go | 7 ++- vmnet_test.go | 137 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 137 insertions(+), 7 deletions(-) diff --git a/vmnet.go b/vmnet.go index a4a9d672..70f4bb6e 100644 --- a/vmnet.go +++ b/vmnet.go @@ -163,7 +163,12 @@ func (c *VmnetNetworkConfiguration) SetIPv4Subnet(subnet netip.Prefix) error { if !subnet.Addr().Is4() { return fmt.Errorf("subnet is not ipv4") } - ip := subnet.Addr().As4() + if !netip.MustParsePrefix("192.168.0.0/16").Overlaps(subnet) { + return fmt.Errorf("subnet %s is out of range", subnet.String()) + } + // Use the first assignable address as the subnet address to avoid + // Virtualization fails with error "Internal Virtualization error. Internal Network Error.". + ip := subnet.Masked().Addr().Next().As4() mask := net.CIDRMask(subnet.Bits(), 32) var cSubnet C.struct_in_addr var cMask C.struct_in_addr diff --git a/vmnet_test.go b/vmnet_test.go index 423da88f..9098adfc 100644 --- a/vmnet_test.go +++ b/vmnet_test.go @@ -2,6 +2,9 @@ package vz_test import ( "log" + "net" + "net/netip" + "slices" "testing" "github.com/Code-Hex/vz/v3" @@ -11,17 +14,31 @@ func TestVmnetSharedModeAllowsCommunicationBetweenMultipleVMs(t *testing.T) { if vz.Available(26.0) { t.Skip("VmnetSharedMode is supported from macOS 26") } + + // Create VmnetNetwork instance from configuration config, err := vz.NewVmnetNetworkConfiguration(vz.SharedMode) if err != nil { t.Fatal(err) } - network, err := vz.NewVmnetNetwork(config) + network1, err := vz.NewVmnetNetwork(config) + if err != nil { + t.Fatal(err) + } + macaddress1 := randomMACAddress(t) + + // Create another VmnetNetwork instance from serialization of the first one + serialization, err := network1.CopySerialization() + if err != nil { + t.Fatal(err) + } + network2, err := vz.NewVmnetNetworkWithSerialization(serialization) if err != nil { t.Fatal(err) } + macaddress2 := randomMACAddress(t) - container1 := newVirtualizationMachine(t, ConfigureNetworkDeviceWithNetwork(network)) - container2 := newVirtualizationMachine(t, ConfigureNetworkDeviceWithNetwork(network)) + container1 := newVirtualizationMachine(t, configureNetworkDevice(network1, macaddress1)) + container2 := newVirtualizationMachine(t, configureNetworkDevice(network2, macaddress2)) t.Cleanup(func() { if err := container1.Shutdown(); err != nil { log.Println(err) @@ -32,12 +49,12 @@ func TestVmnetSharedModeAllowsCommunicationBetweenMultipleVMs(t *testing.T) { }) // Log network information - ipv4Subnet, err := network.IPv4Subnet() + ipv4Subnet, err := network1.IPv4Subnet() if err != nil { t.Fatal(err) } t.Logf("Vmnet network IPv4 subnet: %s", ipv4Subnet.String()) - prefix, err := network.IPv6Prefix() + prefix, err := network1.IPv6Prefix() if err != nil { t.Fatal(err) } @@ -52,7 +69,68 @@ func TestVmnetSharedModeAllowsCommunicationBetweenMultipleVMs(t *testing.T) { container2.exec(t, "ping "+container1IPv4) } -func ConfigureNetworkDeviceWithNetwork(network *vz.VmnetNetwork) func(cfg *vz.VirtualMachineConfiguration) error { +func TestVmnetSharedModeWithConfiguringIPv4(t *testing.T) { + if vz.Available(26.0) { + t.Skip("VmnetSharedMode is supported from macOS 26") + } + // Create VmnetNetwork instance from configuration + config, err := vz.NewVmnetNetworkConfiguration(vz.SharedMode) + if err != nil { + t.Fatal(err) + } + // Configure IPv4 subnet + ipv4Subnet := detectFreeIPv4Subnet(t, netip.MustParsePrefix("192.168.5.0/24")) + if err := config.SetIPv4Subnet(ipv4Subnet); err != nil { + t.Fatal(err) + } + // Configure DHCP reservation + macaddress := randomMACAddress(t) + ipv4 := "192.168.5.15" + config.AddDhcpReservation(macaddress, netip.MustParseAddr(ipv4)) + + // Create VmnetNetwork instance + network, err := vz.NewVmnetNetwork(config) + if err != nil { + t.Fatal(err) + } + + // Create VirtualizationMachine instance + container := newVirtualizationMachine(t, configureNetworkDevice(network, macaddress)) + t.Cleanup(func() { + if err := container.Shutdown(); err != nil { + log.Println(err) + } + }) + + // Log network information + ipv4SubnetConfigured, err := network.IPv4Subnet() + if err != nil { + t.Fatal(err) + } + t.Logf("Vmnet network IPv4 subnet: %s", ipv4SubnetConfigured.String()) + + // Verify the configured subnet + // Compare with masked value to ignore host bits since Vmnet prefers to use first address as network address. + if ipv4Subnet != ipv4SubnetConfigured.Masked() { + t.Fatalf("expected IPv4 subnet %s, but got %s", ipv4Subnet.String(), ipv4SubnetConfigured.Masked().String()) + } + + // Log IPv6 prefix + prefix, err := network.IPv6Prefix() + if err != nil { + t.Fatal(err) + } + t.Logf("Vmnet network IPv6 prefix: %s", prefix.String()) + + // Detect IP address and verify DHCP reservation + containerIPv4 := container.DetectIPv4(t, "eth0") + t.Logf("Container IPv4: %s", containerIPv4) + if ipv4 != containerIPv4 { + t.Fatalf("expected IPv4 %s, but got %s", ipv4, containerIPv4) + } +} + +func configureNetworkDevice(network *vz.VmnetNetwork, macAddress *vz.MACAddress) func(cfg *vz.VirtualMachineConfiguration) error { return func(cfg *vz.VirtualMachineConfiguration) error { var configurations []*vz.VirtioNetworkDeviceConfiguration attachment, err := vz.NewVmnetNetworkDeviceAttachment(network) @@ -63,8 +141,55 @@ func ConfigureNetworkDeviceWithNetwork(network *vz.VmnetNetwork) func(cfg *vz.Vi if err != nil { return err } + config.SetMACAddress(macAddress) configurations = append(configurations, config) cfg.SetNetworkDevicesVirtualMachineConfiguration(configurations) return nil } } + +func detectFreeIPv4Subnet(t *testing.T, prefer netip.Prefix) netip.Prefix { + targetPrefix := netip.MustParsePrefix("192.168.0.0/16") + hostNetIfs, err := net.Interfaces() + if err != nil { + t.Fatal(err) + } + candidates := make([]netip.Prefix, len(hostNetIfs)) + for _, hostNetIf := range hostNetIfs { + hostNetAddrs, err := hostNetIf.Addrs() + if err != nil { + continue + } + for _, hostNetAddr := range hostNetAddrs { + netIPNet, ok := hostNetAddr.(*net.IPNet) + if !ok { + continue + } + hostPrefix := netip.MustParsePrefix(netIPNet.String()) + if targetPrefix.Overlaps(hostPrefix) { + candidates = append(candidates, hostPrefix) + } + } + } + slices.SortFunc(candidates, func(l, r netip.Prefix) int { + if l.Addr().Less(r.Addr()) { + return -1 + } + return 1 + }) + for _, candidate := range candidates { + if prefer.Addr() != candidate.Addr() { + return prefer + } + } + t.Fatal("no free IPv4 subnet found") + return netip.Prefix{} +} + +func randomMACAddress(t *testing.T) *vz.MACAddress { + mac, err := vz.NewRandomLocallyAdministeredMACAddress() + if err != nil { + t.Fatal(err) + } + return mac +}