diff --git a/Makefile b/Makefile index d0094fd..3dce3d6 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ fmt: .PHONY: test test: - go test -p 1 -exec "go run $(PWD)/cmd/codesign" ./... -timeout 2m -v + go test -p 1 -exec "go run $(PWD)/cmd/codesign" ./... -timeout 3m -v .PHONY: test/run test/run: diff --git a/example/macOS/go.mod b/example/macOS/go.mod index cf28973..53a08c4 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 ) diff --git a/go.mod b/go.mod index c8156c6..695db02 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 c3ecddc..2b62956 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 ( @@ -12,6 +13,7 @@ import ( "net" "os" "syscall" + "unsafe" "github.com/Code-Hex/vz/v3/internal/objc" ) @@ -260,6 +262,50 @@ 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() unsafe.Pointer { + return C.VZVmnetNetworkDeviceAttachment_network(objc.Ptr(v)) +} + +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 unsafe.Pointer) (*VmnetNetworkDeviceAttachment, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + + attachment := &VmnetNetworkDeviceAttachment{ + pointer: objc.NewPointer( + C.newVZVmnetNetworkDeviceAttachment(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/pkg/vmnet/vmnet.go b/pkg/vmnet/vmnet.go new file mode 100644 index 0000000..cbfdaa4 --- /dev/null +++ b/pkg/vmnet/vmnet.go @@ -0,0 +1,482 @@ +package vmnet + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +#cgo darwin LDFLAGS: -lobjc -framework Foundation -framework vmnet +# include "vmnet.h" +*/ +import "C" +import ( + "errors" + "fmt" + "net" + "net/netip" + "runtime" + "slices" + "strconv" + "strings" + "unsafe" + + "golang.org/x/sys/unix" +) + +// MARK: - Return + +// The status code returning the result of vmnet operations. +// - https://developer.apple.com/documentation/vmnet/vmnet_return_t?language=objc +type Return C.uint32_t + +const ( + ErrSuccess Return = C.VMNET_SUCCESS // VMNET_SUCCESS Successfully completed. + ErrFailure Return = C.VMNET_FAILURE // VMNET_FAILURE General failure. + ErrMemFailure Return = C.VMNET_MEM_FAILURE // VMNET_MEM_FAILURE Memory allocation failure. + ErrInvalidArgument Return = C.VMNET_INVALID_ARGUMENT // VMNET_INVALID_ARGUMENT Invalid argument specified. + ErrSetupIncomplete Return = C.VMNET_SETUP_INCOMPLETE // VMNET_SETUP_INCOMPLETE Interface setup is not complete. + ErrInvalidAccess Return = C.VMNET_INVALID_ACCESS // VMNET_INVALID_ACCESS Permission denied. + ErrPacketTooBig Return = C.VMNET_PACKET_TOO_BIG // VMNET_PACKET_TOO_BIG Packet size larger than MTU. + ErrBufferExhausted Return = C.VMNET_BUFFER_EXHAUSTED // VMNET_BUFFER_EXHAUSTED Buffers exhausted in kernel. + ErrTooManyPackets Return = C.VMNET_TOO_MANY_PACKETS // VMNET_TOO_MANY_PACKETS Packet count exceeds limit. + ErrSharingServiceBusy Return = C.VMNET_SHARING_SERVICE_BUSY // VMNET_SHARING_SERVICE_BUSY Vmnet Interface cannot be started as conflicting sharing service is in use. + ErrNotAuthorized Return = C.VMNET_NOT_AUTHORIZED // VMNET_NOT_AUTHORIZED The operation could not be completed due to missing authorization. +) + +var _ error = Return(0) + +func (e Return) Error() string { + switch e { + case ErrSuccess: + return "Vmnet: Successfully completed" + case ErrFailure: + return "Vmnet: Failure" + case ErrMemFailure: + return "Vmnet: Memory allocation failure" + case ErrInvalidArgument: + return "Vmnet: Invalid argument specified" + case ErrSetupIncomplete: + return "Vmnet: Interface setup is not complete" + case ErrInvalidAccess: + return "Vmnet: Permission denied" + case ErrPacketTooBig: + return "Vmnet: Packet size larger than MTU" + case ErrBufferExhausted: + return "Vmnet: Buffers exhausted in kernel" + case ErrTooManyPackets: + return "Vmnet: Packet count exceeds limit" + case ErrSharingServiceBusy: + return "Vmnet: Vmnet Interface cannot be started as conflicting sharing service is in use" + case ErrNotAuthorized: + return "Vmnet: The operation could not be completed due to missing authorization" + default: + return fmt.Sprintf("Vmnet: Unknown error %d", uint32(e)) + } +} + +// MARK: - Mode + +// Mode defines the mode of a [Network]. (See [operating_modes_t]) +// - [HostMode] and [SharedMode] are supported by [NewNetworkConfiguration]. +// - VMNET_BRIDGED_MODE is not supported by underlying API [vmnet_network_configuration_create]. +// +// [operating_modes_t]: https://developer.apple.com/documentation/vmnet/operating_modes_t?language=objc +// [vmnet_network_configuration_create]: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_create(_:_:)?language=objc +type Mode uint32 + +const ( + // https://developer.apple.com/documentation/vmnet/operating_modes_t/vmnet_host_mode?language=objc + HostMode Mode = C.VMNET_HOST_MODE + // https://developer.apple.com/documentation/vmnet/operating_modes_t/vmnet_shared_mode?language=objc + SharedMode Mode = C.VMNET_SHARED_MODE +) + +// MARK: - object + +// object +type object struct { + p unsafe.Pointer +} + +// Raw returns the raw xpc_object_t as [unsafe.Pointer]. +func (o *object) Raw() unsafe.Pointer { + return o.p +} + +// releaseOnCleanup registers a cleanup function to release the object when cleaned up. +func (o *object) releaseOnCleanup() { + runtime.AddCleanup(o, func(p unsafe.Pointer) { + C.vmnetRelease(p) + }, o.p) +} + +// ReleaseOnCleanup calls releaseOnCleanup method on the given object and returns it. +func ReleaseOnCleanup[O interface{ releaseOnCleanup() }](o O) O { + o.releaseOnCleanup() + return o +} + +// macOSAvailable checks if the current macOS version is equal to or higher than the required version. +// required should be provided as major, minor, patch integers. +// For example, to check for macOS 13.4.1 or higher, call macOSAvailable(13, 4, 1). +// If the current macOS version is lower than the required version, an error is returned. +func macOSAvailable(required ...int) error { + name := "kern.osproductversion" + osproductversion, err := unix.Sysctl(name) + if err != nil { + return fmt.Errorf("failed to get macOS product version: %w", err) + } + splitted := strings.Split(osproductversion, ".") + nMin := min(len(splitted), len(required)) + actual := make([]int, 0, nMin) + for _, s := range splitted[:nMin] { + n, err := strconv.Atoi(s) + if err != nil { + return fmt.Errorf("failed to parse %q: %q: %w", name, osproductversion, err) + } + actual = append(actual, n) + } + if slices.Compare(required, actual) <= 0 { + return nil + } + return fmt.Errorf("required macOS product version %v but current is %s", required, osproductversion) +} + +// MARK: - NetworkConfiguration + +// NetworkConfiguration is configuration for the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_create(_:_:)?language=objc +type NetworkConfiguration struct { + *object +} + +// NewNetworkConfiguration creates a new [NetworkConfiguration] 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 NewNetworkConfiguration(mode Mode) (*NetworkConfiguration, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + var status Return + ptr := C.VmnetNetworkConfigurationCreate( + C.uint32_t(mode), + (*C.uint32_t)(unsafe.Pointer(&status)), + ) + if !errors.Is(status, ErrSuccess) { + return nil, fmt.Errorf("failed to create VmnetNetworkConfiguration: %w", status) + } + config := &NetworkConfiguration{object: &object{p: ptr}} + ReleaseOnCleanup(config) + return config, nil +} + +// AddDhcpReservation configures a new DHCP reservation for the [Network]. +// client is the MAC address for which the DHCP address is reserved. +// reservation is the DHCP IPv4 address to be reserved. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_add_dhcp_reservation(_:_:_:)?language=objc +func (c *NetworkConfiguration) AddDhcpReservation(client net.HardwareAddr, reservation netip.Addr) error { + if !reservation.Is4() { + return fmt.Errorf("reservation is not ipv4") + } + ip := reservation.As4() + var cReservation C.struct_in_addr + + cClient, err := netHardwareAddrToEtherAddr(client) + if err != nil { + return err + } + copy((*[4]byte)(unsafe.Pointer(&cReservation))[:], ip[:]) + + status := C.VmnetNetworkConfiguration_addDhcpReservation( + c.Raw(), + &cClient, + &cReservation, + ) + if !errors.Is(Return(status), ErrSuccess) { + return fmt.Errorf("failed to add dhcp reservation: %w", Return(status)) + } + return nil +} + +// AddPortForwardingRule configures a port forwarding rule for the [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. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_add_port_forwarding_rule(_:_:_:_:_:_:)?language=objc +func (c *NetworkConfiguration) 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.VmnetNetworkConfiguration_addPortForwardingRule( + c.Raw(), + C.uint8_t(protocol), + C.sa_family_t(addressFamily), + C.uint16_t(internalPort), + C.uint16_t(externalPort), + address, + ) + if !errors.Is(Return(status), ErrSuccess) { + return fmt.Errorf("failed to add port forwarding rule: %w", Return(status)) + } + return nil +} + +// DisableDhcp disables DHCP server on the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_dhcp(_:)?language=objc +func (c *NetworkConfiguration) DisableDhcp() { + C.VmnetNetworkConfiguration_disableDhcp(c.Raw()) +} + +// DisableDnsProxy disables DNS proxy on the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_dns_proxy(_:)?language=objc +func (c *NetworkConfiguration) DisableDnsProxy() { + C.VmnetNetworkConfiguration_disableDnsProxy(c.Raw()) +} + +// DisableNat44 disables NAT44 on the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_nat44(_:)?language=objc +func (c *NetworkConfiguration) DisableNat44() { + C.VmnetNetworkConfiguration_disableNat44(c.Raw()) +} + +// DisableNat66 disables NAT66 on the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_nat66(_:)?language=objc +func (c *NetworkConfiguration) DisableNat66() { + C.VmnetNetworkConfiguration_disableNat66(c.Raw()) +} + +// DisableRouterAdvertisement disables router advertisement on the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_router_advertisement(_:)?language=objc +func (c *NetworkConfiguration) DisableRouterAdvertisement() { + C.VmnetNetworkConfiguration_disableRouterAdvertisement(c.Raw()) +} + +// SetExternalInterface sets the external interface of the [Network]. +// This is only available to networks of [SharedMode]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_external_interface(_:_:)?language=objc +func (c *NetworkConfiguration) SetExternalInterface(ifname string) error { + cIfname := C.CString(ifname) + defer C.free(unsafe.Pointer(cIfname)) + + status := C.VmnetNetworkConfiguration_setExternalInterface( + c.Raw(), + cIfname, + ) + if !errors.Is(Return(status), ErrSuccess) { + return fmt.Errorf("failed to set external interface: %w", Return(status)) + } + return nil +} + +// SetIPv4Subnet configures the IPv4 address for the [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. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_ipv4_subnet(_:_:_:)?language=objc +func (c *NetworkConfiguration) SetIPv4Subnet(subnet netip.Prefix) error { + if !subnet.Addr().Is4() { + return fmt.Errorf("subnet is not ipv4") + } + 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 + + copy((*[4]byte)(unsafe.Pointer(&cSubnet))[:], ip[:]) + copy((*[4]byte)(unsafe.Pointer(&cMask))[:], mask[:]) + + status := C.VmnetNetworkConfiguration_setIPv4Subnet( + c.Raw(), + &cSubnet, + &cMask, + ) + if !errors.Is(Return(status), ErrSuccess) { + return fmt.Errorf("failed to set ipv4 subnet: %d", Return(status)) + } + return nil +} + +// SetIPv6Prefix configures the IPv6 prefix for the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_ipv6_prefix(_:_:_:)?language=objc +func (c *NetworkConfiguration) 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.VmnetNetworkConfiguration_setIPv6Prefix( + c.Raw(), + &cPrefix, + C.uint8_t(prefix.Bits()), + ) + if !errors.Is(Return(status), ErrSuccess) { + return fmt.Errorf("failed to set ipv6 prefix: %w", Return(status)) + } + return nil +} + +func netHardwareAddrToEtherAddr(hw net.HardwareAddr) (C.ether_addr_t, error) { + if len(hw) != 6 { + return C.ether_addr_t{}, fmt.Errorf("invalid MAC address length: %d", len(hw)) + } + var addr C.ether_addr_t + copy((*[6]byte)(unsafe.Pointer(&addr))[:], hw[:6]) + return addr, nil +} + +// SetMtu configures the maximum transmission unit (MTU) for the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_mtu(_:_:)?language=objc +func (c *NetworkConfiguration) SetMtu(mtu uint32) error { + status := C.VmnetNetworkConfiguration_setMtu( + c.Raw(), + C.uint32_t(mtu), + ) + if !errors.Is(Return(status), ErrSuccess) { + return fmt.Errorf("failed to set mtu: %w", Return(status)) + } + return nil +} + +// MARK: - Network + +// Network represents a [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_create(_:_:)?language=objc +type Network struct { + *object +} + +// NewNetwork creates a new [Network] with [NetworkConfiguration]. +// This is only supported on macOS 26 and newer, error will be returned on older versions. +func NewNetwork(config *NetworkConfiguration) (*Network, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + + var status Return + ptr := C.VmnetNetworkCreate( + config.Raw(), + (*C.uint32_t)(unsafe.Pointer(&status)), + ) + if !errors.Is(status, ErrSuccess) { + return nil, fmt.Errorf("failed to create VmnetNetwork: %w", status) + } + network := &Network{object: &object{p: ptr}} + ReleaseOnCleanup(network) + return network, nil +} + +// NewNetworkWithSerialization creates a new [Network] from a serialized representation. +// This is only supported on macOS 26 and newer, error will be returned on older versions. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_create_with_serialization(_:_:)?language=objc +func NewNetworkWithSerialization(serialization unsafe.Pointer) (*Network, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + + var status Return + ptr := C.VmnetNetworkCreateWithSerialization( + serialization, + (*C.uint32_t)(unsafe.Pointer(&status)), + ) + if !errors.Is(status, ErrSuccess) { + return nil, fmt.Errorf("failed to create VmnetNetwork with serialization: %w", status) + } + network := &Network{object: &object{p: ptr}} + ReleaseOnCleanup(network) + return network, nil +} + +// CopySerialization returns a serialized copy of [Network] in xpc_object_t as [unsafe.Pointer]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_copy_serialization(_:_:)?language=objc +func (n *Network) CopySerialization() (unsafe.Pointer, error) { + var status Return + ptr := C.VmnetNetwork_copySerialization( + n.Raw(), + (*C.uint32_t)(unsafe.Pointer(&status)), + ) + if !errors.Is(status, ErrSuccess) { + return nil, fmt.Errorf("failed to copy serialization: %w", status) + } + return ptr, nil +} + +// IPv4Subnet returns the IPv4 subnet of the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_get_ipv4_subnet(_:_:_:)?language=objc +func (n *Network) IPv4Subnet() (subnet netip.Prefix, err error) { + var cSubnet C.struct_in_addr + var cMask C.struct_in_addr + + C.VmnetNetwork_getIPv4Subnet(n.Raw(), &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]) +} + +// IPv6Prefix returns the IPv6 prefix of the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_get_ipv6_prefix(_:_:_:)?language=objc +func (n *Network) IPv6Prefix() (netip.Prefix, error) { + var prefix C.struct_in6_addr + var prefixLen C.uint8_t + + C.VmnetNetwork_getIPv6Prefix(n.Raw(), &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) +} diff --git a/pkg/vmnet/vmnet.h b/pkg/vmnet/vmnet.h new file mode 100644 index 0000000..c71b4f8 --- /dev/null +++ b/pkg/vmnet/vmnet.h @@ -0,0 +1,61 @@ +#pragma once + +#import +#import +#import +#import +#import + +// MARK: - Macros + +// To avoid including virtualization_helper.h, copied from there. +NSDictionary *vmnetDumpProcessinfo(); + +#define RAISE_REASON_MESSAGE \ + "This may possibly be a bug due to library handling errors.\n" \ + "I would appreciate it if you could report it to https://github.com/Code-Hex/vz/issues/new/choose\n\n" \ + "Information: %@\n" + +#define RAISE_UNSUPPORTED_MACOS_EXCEPTION() \ + do { \ + [NSException \ + raise:@"UnhandledAvailabilityException" \ + format:@RAISE_REASON_MESSAGE, vmnetDumpProcessinfo()]; \ + __builtin_unreachable(); \ + } while (0) + +// 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 + +// MARK: - CFRelease Wrapper + +void vmnetRelease(void *obj); + +// MARK: - vmnet_network_configuration_t (macOS 26+) + +// VmnetNetworkConfiguration +uint32_t VmnetNetworkConfiguration_addDhcpReservation(void *config, ether_addr_t const *client, struct in_addr const *reservation); +uint32_t VmnetNetworkConfiguration_addPortForwardingRule(void *config, uint8_t protocol, sa_family_t address_family, uint16_t internal_port, uint16_t external_port, void const *internal_address); +void *VmnetNetworkConfigurationCreate(uint32_t mode, uint32_t *status); +void VmnetNetworkConfiguration_disableDhcp(void *config); +void VmnetNetworkConfiguration_disableDnsProxy(void *config); +void VmnetNetworkConfiguration_disableNat44(void *config); +void VmnetNetworkConfiguration_disableNat66(void *config); +void VmnetNetworkConfiguration_disableRouterAdvertisement(void *config); +uint32_t VmnetNetworkConfiguration_setExternalInterface(void *config, const char *ifname); +uint32_t VmnetNetworkConfiguration_setIPv4Subnet(void *config, struct in_addr const *subnet_addr, struct in_addr const *subnet_mask); +uint32_t VmnetNetworkConfiguration_setIPv6Prefix(void *config, struct in6_addr const *prefix, uint8_t prefix_len); +uint32_t VmnetNetworkConfiguration_setMtu(void *config, uint32_t mtu); + +// MARK: - vmnet_network_ref (macOS 26+) + +// vmnet_network +void *VmnetNetwork_copySerialization(void *network, uint32_t *status); +void *VmnetNetworkCreate(void *config, uint32_t *status); +void *VmnetNetworkCreateWithSerialization(void *serialization, uint32_t *status); +void VmnetNetwork_getIPv4Subnet(void *network, struct in_addr *subnet, struct in_addr *mask); +void VmnetNetwork_getIPv6Prefix(void *network, struct in6_addr *prefix, uint8_t *prefix_len); diff --git a/pkg/vmnet/vmnet.m b/pkg/vmnet/vmnet.m new file mode 100644 index 0000000..b49d7ab --- /dev/null +++ b/pkg/vmnet/vmnet.m @@ -0,0 +1,233 @@ +#import "vmnet.h" + +// MARK: - Macros + +// To avoid including virtualization_helper.h, copied from there. +NSDictionary *vmnetDumpProcessinfo() +{ + NSString *osVersionString = [[NSProcessInfo processInfo] operatingSystemVersionString]; + return @{ + @"LLVM (Clang) Version" : @__VERSION__, +#ifdef __arm64__ + @"Target for arm64" : @1, +#else + @"Target for arm64" : @0, +#endif + // The version of the macOS on which the process is executing. + @"Running OS Version" : osVersionString, +#ifdef __MAC_OS_X_VERSION_MAX_ALLOWED + @"Max Allowed OS Version" : @__MAC_OS_X_VERSION_MAX_ALLOWED, +#endif +#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED + @"Min Required OS Version" : @__MAC_OS_X_VERSION_MIN_REQUIRED, +#endif + }; +} + +// MARK: - CFRelease Wrapper + +void vmnetRelease(void *obj) +{ + if (obj != NULL) { + CFRelease((CFTypeRef)obj); + } +} + +// MARK: - vmnet_network_configuration_t (macOS 26+) + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_add_dhcp_reservation(_:_:_:)?language=objc +uint32_t VmnetNetworkConfiguration_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_add_port_forwarding_rule(_:_:_:_:_:_:)?language=objc +uint32_t VmnetNetworkConfiguration_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_create(_:_:)?language=objc +void *VmnetNetworkConfigurationCreate(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_disable_dhcp(_:)?language=objc +void VmnetNetworkConfiguration_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 VmnetNetworkConfiguration_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_nat44(_:)?language=objc +void VmnetNetworkConfiguration_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 VmnetNetworkConfiguration_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_router_advertisement(_:)?language=objc +void VmnetNetworkConfiguration_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_external_interface(_:_:)?language=objc +uint32_t VmnetNetworkConfiguration_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_set_ipv4_subnet(_:_:_:)?language=objc +uint32_t VmnetNetworkConfiguration_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 VmnetNetworkConfiguration_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_set_mtu(_:_:)?language=objc +uint32_t VmnetNetworkConfiguration_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(); +} + +// MARK: - vmnet_network_ref (macOS 26+) + +// https://developer.apple.com/documentation/vmnet/vmnet_network_copy_serialization(_:_:)?language=objc +void *VmnetNetwork_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(); +} + +// vmnet_network +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_create(_:_:)?language=objc +void *VmnetNetworkCreate(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_create_with_serialization(_:_:)?language=objc +void *VmnetNetworkCreateWithSerialization(void *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(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_get_ipv4_subnet(_:_:_:)?language=objc +void VmnetNetwork_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(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_get_ipv6_prefix(_:_:_:)?language=objc +void VmnetNetwork_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(); +} diff --git a/pkg/xpc/array.go b/pkg/xpc/array.go new file mode 100644 index 0000000..50750b1 --- /dev/null +++ b/pkg/xpc/array.go @@ -0,0 +1,80 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +# include "xpc.h" +*/ +import "C" +import ( + "iter" + "runtime/cgo" + "unsafe" +) + +// Array represents an XPC array([XPC_TYPE_ARRAY]) object. [TypeArray] +// +// [XPC_TYPE_ARRAY]: https://developer.apple.com/documentation/xpc/xpc_type_array-c.macro?language=objc +type Array struct { + *XpcObject +} + +var _ Object = &Array{} + +// NewArray creates a new [Array] from the given [Object]s. +// +// - https://developer.apple.com/documentation/xpc/xpc_array_create(_:_:)?language=objc +func NewArray(objects ...Object) *Array { + cObjects := make([]unsafe.Pointer, len(objects)) + for i, obj := range objects { + cObjects[i] = obj.Raw() + } + return ReleaseOnCleanup(&Array{XpcObject: &XpcObject{C.xpcArrayCreate( + (*unsafe.Pointer)(unsafe.Pointer(&cObjects[0])), + C.size_t(len(cObjects)), + )}}) +} + +// Count returns the number of elements in the [Array]. +// +// - https://developer.apple.com/documentation/xpc/xpc_array_get_count(_:)?language=objc +func (a *Array) Count() int { + return int(C.xpcArrayGetCount(a.Raw())) +} + +// ArrayApplier is a function type for applying to each element in the Array. +type ArrayApplier func(uint64, Object) bool + +// callArrayApplier is called from C to apply a function to each element in the Array. +// +//export callArrayApplier +func callArrayApplier(cgoApplier uintptr, index C.size_t, cgoValue uintptr) C.bool { + applier := unwrapHandler[ArrayApplier](cgoApplier) + value := unwrapObject[Object](cgoValue) + result := applier(uint64(index), value) + return C.bool(result) +} + +// All iterates over all elements in the [Array]. +// +// - https://developer.apple.com/documentation/xpc/xpc_array_apply(_:_:)?language=objc +func (a *Array) All() iter.Seq2[uint64, Object] { + return func(yieald func(uint64, Object) bool) { + cgoApplier := cgo.NewHandle(ArrayApplier(yieald)) + defer cgoApplier.Delete() + _ = C.xpcArrayApply( + a.Raw(), + C.uintptr_t(cgoApplier), + ) + } +} + +// Values iterates over all values in the [Array] using [Array.All]. +func (a *Array) Values() iter.Seq[Object] { + return func(yieald func(Object) bool) { + for _, value := range a.All() { + if !yieald(value) { + return + } + } + } +} diff --git a/pkg/xpc/cgo_handle.go b/pkg/xpc/cgo_handle.go new file mode 100644 index 0000000..be2e97e --- /dev/null +++ b/pkg/xpc/cgo_handle.go @@ -0,0 +1,44 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +# include "xpc.h" +*/ +import "C" +import ( + "runtime" + "runtime/cgo" +) + +// cgoHandler holds a cgo.Handle for an Object. +// It provides methods to hold and release the handle. +// handle will released when cgoHandler.release is called. +type cgoHandler struct { + handle cgo.Handle +} + +// releaseOnCleanup registers a cleanup function to delete the cgo.Handle when cleaned up. +func (h *cgoHandler) releaseOnCleanup() { + runtime.AddCleanup(h, func(h cgo.Handle) { + h.Delete() + }, h.handle) +} + +// newCgoHandler creates a new cgoHandler and holds the given value. +func newCgoHandler(v any) (*cgoHandler, C.uintptr_t) { + if v == nil { + return nil, 0 + } + h := &cgoHandler{cgo.NewHandle(v)} + return ReleaseOnCleanup(h), C.uintptr_t(h.handle) +} + +// unwrapHandler unwraps the cgo.Handle from the given uintptr and returns the associated value. +// It does NOT delete the handle; it expects the handle to be managed by cgoHandler or caller. +func unwrapHandler[T any](handle uintptr) T { + if handle == 0 { + var zero T + return zero + } + return cgo.Handle(handle).Value().(T) +} diff --git a/pkg/xpc/dictionary.go b/pkg/xpc/dictionary.go new file mode 100644 index 0000000..fd941ec --- /dev/null +++ b/pkg/xpc/dictionary.go @@ -0,0 +1,170 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +# include "xpc.h" +*/ +import "C" +import ( + "fmt" + "iter" + "runtime/cgo" + "unsafe" +) + +// Dictionary represents an XPC dictionary (XPC_TYPE_DICTIONARY) object. [TypeDictionary] +// - https://developer.apple.com/documentation/xpc/xpc_type_dictionary-c.macro?language=objc +type Dictionary struct { + *XpcObject +} + +var _ Object = &Dictionary{} + +// NewDictionary creates a new empty [Dictionary] object and applies the given entries. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_create_empty()?language=objc +// +// The entries can be created using [DictionaryEntry] functions such as [KeyValue]. +func NewDictionary(entries ...DictionaryEntry) *Dictionary { + d := ReleaseOnCleanup(&Dictionary{XpcObject: &XpcObject{C.xpcDictionaryCreateEmpty()}}) + for _, e := range entries { + e(d) + } + return d +} + +// DictionaryEntry defines a function type for customizing [NewDictionary] or [Dictionary.CreateReply]. +type DictionaryEntry func(*Dictionary) + +// KeyValue sets a [Object] value for the given key in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_set_value(_:_:_:)?language=objc +func KeyValue(key string, val Object) DictionaryEntry { + return func(o *Dictionary) { + o.SetValue(key, val) + } +} + +// DictionaryApplier is a function type for applying to each key-value pair in the XPC dictionary. +type DictionaryApplier func(string, Object) bool + +// callDictionaryApplier is called from C to apply a function to each key-value pair in the XPC dictionary object. +// +//export callDictionaryApplier +func callDictionaryApplier(cgoApplier uintptr, cKey *C.char, cgoValue uintptr) C.bool { + applier := unwrapHandler[DictionaryApplier](cgoApplier) + return C.bool(applier(C.GoString(cKey), unwrapObject[Object](cgoValue))) +} + +// All iterates over all key-value pairs in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_apply(_:_:)?language=objc +func (o *Dictionary) All() iter.Seq2[string, Object] { + return func(yieald func(string, Object) bool) { + cgoApplier := cgo.NewHandle(DictionaryApplier(yieald)) + defer cgoApplier.Delete() + C.xpcDictionaryApply(o.Raw(), C.uintptr_t(cgoApplier)) + } +} + +// Keys iterates over all keys in the [Dictionary] using [Dictionary.All]. +func (o *Dictionary) Keys() iter.Seq[string] { + return func(yieald func(string) bool) { + for key := range o.All() { + if !yieald(key) { + return + } + } + } +} + +// Values iterates over all [Object] values in the [Dictionary] using [Dictionary.All]. +func (o *Dictionary) Values() iter.Seq[Object] { + return func(yieald func(Object) bool) { + for _, value := range o.All() { + if !yieald(value) { + return + } + } + } +} + +// Entries iterates over all [DictionaryEntry] entries in the [Dictionary] using [Dictionary.All]. +func (o *Dictionary) Entries() iter.Seq[DictionaryEntry] { + return func(yieald func(DictionaryEntry) bool) { + for key, value := range o.All() { + if !yieald(KeyValue(key, value)) { + return + } + } + } +} + +// GetData retrieves a byte slice value from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_data(_:_:_:)?language=objc +// +// Returns nil if the key does not exist. +func (o *Dictionary) GetData(key string) []byte { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + var n C.size_t + p := C.xpcDictionaryGetData(o.Raw(), cKey, &n) + if p == nil || n == 0 { + return nil + } + return C.GoBytes(p, C.int(n)) +} + +// GetString retrieves a string value from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_string(_:_:)?language=objc +// +// Returns an empty string if the key does not exist. +func (o *Dictionary) GetString(key string) string { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + val := C.xpcDictionaryGetString(o.Raw(), cKey) + return C.GoString(val) +} + +// SetValue sets an [Object] value for the given key in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_set_value(_:_:_:)?language=objc +func (o *Dictionary) SetValue(key string, val Object) { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + C.xpcDictionarySetValue(o.Raw(), cKey, val.Raw()) +} + +// GetValue retrieves an [Object] value from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_value(_:_:)?language=objc +// +// Returns nil if the key does not exist. +func (o *Dictionary) GetValue(key string) Object { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + val := C.xpcDictionaryGetValue(o.Raw(), cKey) + if val == nil { + return nil + } + return NewObject(val) +} + +// DictionaryCreateReply creates a new reply [Dictionary] based on the current [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_create_reply(_:)?language=objc +// +// The entries can be created using [DictionaryEntry] functions such as [KeyValue]. +func (o *Dictionary) CreateReply(entries ...DictionaryEntry) *Dictionary { + // Do not use ReleaseOnCleanup here because the reply dictionary will be released in C after sending. + d := &Dictionary{XpcObject: &XpcObject{C.xpcDictionaryCreateReply(o.Raw())}} + for _, entry := range entries { + entry(d) + } + return d +} + +// SenderSatisfies checks if the sender of the message [Dictionary] satisfies the given [PeerRequirement]. +// - https://developer.apple.com/documentation/xpc/xpc_peer_requirement_match_received_message?language=objc +func (d *Dictionary) SenderSatisfies(requirement *PeerRequirement) (bool, error) { + var err_out unsafe.Pointer + res := C.xpcPeerRequirementMatchReceivedMessage(requirement.Raw(), d.Raw(), &err_out) + if err_out != nil { + return false, fmt.Errorf("error matching peer requirement: %w", newRichError(err_out)) + } + return bool(res), nil +} diff --git a/pkg/xpc/error.go b/pkg/xpc/error.go new file mode 100644 index 0000000..cc3119c --- /dev/null +++ b/pkg/xpc/error.go @@ -0,0 +1,46 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +# include "xpc.h" +*/ +import "C" +import ( + "unsafe" +) + +// RichError represents an XPC rich error. ([XPC_TYPE_RICH_ERROR]) [TypeRichError] +// +// [XPC_TYPE_RICH_ERROR]: https://developer.apple.com/documentation/xpc/xpc_rich_error_t?language=objc +type RichError struct { + *XpcObject +} + +var _ Object = &RichError{} + +var _ error = RichError{} + +// newRichError creates a new RichError from an existing xpc_rich_error_t. +// internal use only. +func newRichError(richErr unsafe.Pointer) *RichError { + if richErr == nil { + return nil + } + return &RichError{XpcObject: &XpcObject{richErr}} +} + +// CanRetry indicates whether the operation that caused the [RichError] can be retried. +// +// - https://developer.apple.com/documentation/xpc/xpc_rich_error_can_retry(_:)?language=objc +func (e RichError) CanRetry() bool { + return bool(C.xpcRichErrorCanRetry(e.Raw())) +} + +// Error implements the [error] interface. +// +// - https://developer.apple.com/documentation/xpc/xpc_rich_error_copy_description(_:)?language=objc +func (e RichError) Error() string { + desc := C.xpcRichErrorCopyDescription(e.Raw()) + defer C.free(unsafe.Pointer(desc)) + return C.GoString(desc) +} diff --git a/pkg/xpc/listener.go b/pkg/xpc/listener.go new file mode 100644 index 0000000..3bba907 --- /dev/null +++ b/pkg/xpc/listener.go @@ -0,0 +1,93 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +#cgo darwin LDFLAGS: -lobjc -framework Foundation +# include "xpc.h" +*/ +import "C" +import ( + "unsafe" +) + +// Listener represents an XPC listener. (macOS 14.0+) +// - https://developer.apple.com/documentation/xpc/xpc_listener_t?language=objc +type Listener struct { + *XpcObject + sessionHandler *cgoHandler +} + +// SessionHandler is a function that handles incoming sessions. +type SessionHandler func(session *Session) + +// Option represents an option for creating a [Listener]. +type ListenerOption interface { + inactiveListenerSet(*Listener) +} + +var ( + _ ListenerOption = (*PeerRequirement)(nil) +) + +// NewListener creates a new [Listener] for the given service name. (macOS 14.0+) +// - https://developer.apple.com/documentation/xpc/xpc_listener_create +// +// You need to call [Listener.Activate] to start accepting incoming connections. +func NewListener(service string, handler SessionHandler, options ...ListenerOption) (*Listener, error) { + cname := C.CString(service) + defer C.free(unsafe.Pointer(cname)) + // Use a serial dispatch queue for the listener, + // because the vmnet framework API does not seem to work well with concurrent queues. + // For example, vmnet_network_create fails when using a concurrent queue. + q := C.dispatchQueueCreateSerial(cname) + defer C.dispatchRelease(q) + cgoHandler, p := newCgoHandler(handler) + var err_out unsafe.Pointer + ptr := C.xpcListenerCreate(cname, q, C.XPC_LISTENER_CREATE_INACTIVE, p, &err_out) + if err_out != nil { + return nil, newRichError(err_out) + } + listener := ReleaseOnCleanup(&Listener{ + XpcObject: &XpcObject{ptr}, + sessionHandler: cgoHandler, + }) + for _, opt := range options { + opt.inactiveListenerSet(listener) + } + return listener, nil +} + +// callSessionHandler is called from C to handle incoming sessions. +// +//export callSessionHandler +func callSessionHandler(cgoSessionHandler, cgoSession uintptr) { + handler := unwrapHandler[SessionHandler](cgoSessionHandler) + session := unwrapObject[*Session](cgoSession) + handler(session) +} + +// String returns a description of the [Listener]. (macOS 14.0+) +// - https://developer.apple.com/documentation/xpc/xpc_listener_copy_description +func (l *Listener) String() string { + desc := C.xpcListenerCopyDescription(l.Raw()) + defer C.free(unsafe.Pointer(desc)) + return C.GoString(desc) +} + +// Activate starts the [Listener] to accept incoming connections. (macOS 14.0+) +// - https://developer.apple.com/documentation/xpc/xpc_listener_activate +func (l *Listener) Activate() error { + var err_out unsafe.Pointer + C.xpcListenerActivate(l.Raw(), &err_out) + if err_out != nil { + return newRichError(err_out) + } + return nil +} + +// Close stops the [Listener] from accepting incoming connections. (macOS 14.0+) +// - https://developer.apple.com/documentation/xpc/xpc_listener_cancel +func (l *Listener) Close() error { + C.xpcListenerCancel(l.Raw()) + return nil +} diff --git a/pkg/xpc/object.go b/pkg/xpc/object.go new file mode 100644 index 0000000..93308b9 --- /dev/null +++ b/pkg/xpc/object.go @@ -0,0 +1,109 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +# include "xpc.h" +*/ +import "C" +import ( + "runtime/cgo" + "unsafe" +) + +// Object represents a generic XPC object. +// - https://developer.apple.com/documentation/xpc/xpc_object_t?language=objc +type Object interface { + Raw() unsafe.Pointer // Raw returns the raw xpc_object_t pointer. + Type() Type // Type returns the type of the XPC object. + String() string // String returns the description of the XPC object. + retain() // retain retains the XPC object. + releaseOnCleanup() // releaseOnCleanup releases the XPC object on cleanup. +} + +// NewObject creates a new [Object] from an existing xpc_object_t. +// The XPC APIs should be wrapped in C to use void* instead of xpc_object_t. +// This function accepts an [unsafe.Pointer] that represents void* in C. +// It determines the specific type and returns the appropriate wrapper. +func NewObject(o unsafe.Pointer) Object { + if o == nil { + return nil + } + xpcObject := &XpcObject{o} + // Determine the specific type and return the appropriate wrapper. + // It allows users to use type assertions to access type-specific methods. + switch xpcObject.Type() { + case TypeArray: + return &Array{xpcObject} + case TypeData: + return &Data{xpcObject} + case TypeDictionary: + return &Dictionary{xpcObject} + case TypeRichError: + return &RichError{xpcObject} + case TypeSession: + return &Session{XpcObject: xpcObject} + case TypeString: + return &String{xpcObject} + default: + return xpcObject + } +} + +// wrapRawObject wraps an existing xpc_object_t into an Object and returns a handle. +// intended to be called from C. +// +//export wrapRawObject +func wrapRawObject(ptr unsafe.Pointer) uintptr { + o := NewObject(ptr) + if o == nil { + return 0 + } + return uintptr(cgo.NewHandle(o)) +} + +// unwrapObject unwraps the [cgo.Handle] from the given uintptr and returns the associated Object. +// It also deletes the handle to avoid memory leaks. +func unwrapObject[T any](handle uintptr) T { + if handle == 0 { + var zero T + return zero + } + defer cgo.Handle(handle).Delete() + return cgo.Handle(handle).Value().(T) +} + +// XPC_TYPE_DATA represents an XPC data object. + +// NewData returns a new [Data] object from the given byte slice. +// - https://developer.apple.com/documentation/xpc/xpc_data_create(_:_:)?language=objc +func NewData(b []byte) Object { + if len(b) == 0 { + return ReleaseOnCleanup(&Data{&XpcObject{C.xpcDataCreate(nil, 0)}}) + } + return ReleaseOnCleanup(&Data{&XpcObject{C.xpcDataCreate( + unsafe.Pointer(&b[0]), + C.size_t(len(b)), + )}}) +} + +// Data represents an XPC data([XPC_TYPE_DATA]) object. [TypeData] +// +// [XPC_TYPE_DATA]: https://developer.apple.com/documentation/xpc/xpc_type_data-c.macro?language=objc +type Data struct{ *XpcObject } + +var _ Object = &Data{} + +// NewString returns a new [String] object from the given Go string. +// - https://developer.apple.com/documentation/xpc/xpc_string_create(_:)?language=objc +func NewString(s string) Object { + cstr := C.CString(s) + defer C.free(unsafe.Pointer(cstr)) + return ReleaseOnCleanup(&String{&XpcObject{C.xpcStringCreate(cstr)}}) +} + +// String represents an XPC string([XPC_TYPE_STRING]) object. [TypeString] +// +// [XPC_TYPE_STRING]: https://developer.apple.com/documentation/xpc/xpc_type_string-c.macro?language=objc +type String struct{ *XpcObject } + +var _ Object = &String{} diff --git a/pkg/xpc/peer_requirement.go b/pkg/xpc/peer_requirement.go new file mode 100644 index 0000000..4feb993 --- /dev/null +++ b/pkg/xpc/peer_requirement.go @@ -0,0 +1,56 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +#cgo darwin LDFLAGS: -lobjc -framework Foundation +# include "xpc.h" +*/ +import "C" +import ( + "unsafe" +) + +// PeerRequirement represents an [xpc_peer_requirement_t]. (macOS 26.0+) +// +// [xpc_peer_requirement_t]: https://developer.apple.com/documentation/xpc/xpc_peer_requirement_t?language=objc +type PeerRequirement struct { + *XpcObject +} + +var _ Object = &PeerRequirement{} + +// NewPeerRequirementLwcr creates a [PeerRequirement] from a LWCR object *[Dictionary]. (macOS 26.0+) +// - https://developer.apple.com/documentation/xpc/xpc_peer_requirement_create_lwcr +// - https://developer.apple.com/documentation/security/defining-launch-environment-and-library-constraints?language=objc +func NewPeerRequirementLwcr(lwcr *Dictionary) (*PeerRequirement, error) { + var err_out unsafe.Pointer + ptr := C.xpcPeerRequirementCreateLwcr(lwcr.Raw(), &err_out) + if err_out != nil { + return nil, newRichError(err_out) + } + return ReleaseOnCleanup(&PeerRequirement{XpcObject: &XpcObject{ptr}}), nil +} + +// NewPeerRequirementLwcrWithEntries creates a [PeerRequirement] from a LWCR object *[Dictionary] constructed +// with the given [DictionaryEntry]s. (macOS 26.0+) +// - https://developer.apple.com/documentation/xpc/xpc_peer_requirement_create_lwcr +// - https://developer.apple.com/documentation/security/defining-launch-environment-and-library-constraints?language=objc +func NewPeerRequirementLwcrWithEntries(entries ...DictionaryEntry) (*PeerRequirement, error) { + return NewPeerRequirementLwcr(NewDictionary(entries...)) +} + +// inactiveListenerSet configures the given [Listener] with the [PeerRequirement]. (macOS 26.0+) +// - https://developer.apple.com/documentation/xpc/xpc_listener_set_peer_requirement +// +// This method implements the [ListenerOption] interface. +func (pr *PeerRequirement) inactiveListenerSet(listener *Listener) { + C.xpcListenerSetPeerRequirement(listener.Raw(), pr.Raw()) +} + +// inactiveSessionSet configures the given [Session] with the [PeerRequirement]. (macOS 26.0+) +// - https://developer.apple.com/documentation/xpc/xpc_session_set_peer_requirement +// +// This method implements the [SessionOption] interface. +func (pr *PeerRequirement) inactiveSessionSet(session *Session) { + C.xpcSessionSetPeerRequirement(session.Raw(), pr.Raw()) +} diff --git a/pkg/xpc/session.go b/pkg/xpc/session.go new file mode 100644 index 0000000..92c6be7 --- /dev/null +++ b/pkg/xpc/session.go @@ -0,0 +1,221 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +#cgo darwin LDFLAGS: -lobjc -framework Foundation +# include "xpc.h" +*/ +import "C" +import ( + "context" + "runtime/cgo" + "unsafe" +) + +// Session represents an [xpc_session_t]. (macOS 13.0+) +// +// [xpc_session_t]: https://developer.apple.com/documentation/xpc/xpc_session_t?language=objc +type Session struct { + // Exported for use in other packages since unimplemented XPC API may require direct access to xpc_session_t. + *XpcObject + cancellationHandler *cgoHandler + incomingMessageHandler *cgoHandler +} + +var _ Object = &Session{} + +// SessionOption represents an option for configuring a inactive [Session]. +type SessionOption interface { + inactiveSessionSet(*Session) +} + +var ( + _ SessionOption = (MessageHandler)(nil) + _ SessionOption = (CancellationHandler)(nil) + _ SessionOption = (*PeerRequirement)(nil) +) + +// NewSession creates a new [Session] for the given Mach service name. (macOS 13.0+) +// +// [SessionOption](s) can be provided to configure the inactive session before activation. +// Available options include [MessageHandler], [CancellationHandler], and [PeerRequirement]. +// - https://developer.apple.com/documentation/xpc/xpc_session_create_mach_service +func NewSession(macServiceName string, sessionOpts ...SessionOption) (*Session, error) { + + cServiceName := C.CString(macServiceName) + defer C.free(unsafe.Pointer(cServiceName)) + + var err_out unsafe.Pointer + ptr := C.xpcSessionCreateMachService(cServiceName, nil, C.XPC_SESSION_CREATE_INACTIVE, &err_out) + if err_out != nil { + return nil, newRichError(err_out) + } + session := ReleaseOnCleanup(&Session{XpcObject: &XpcObject{ptr}}) + for _, o := range sessionOpts { + o.inactiveSessionSet(session) + } + err := session.activate() + if err != nil { + session.Cancel() + return nil, err + } + return session, nil +} + +// MessageHandler is a function [SessionOption] that handles incoming messages in a [Session]. +// It receives the incoming message *[Dictionary] and returns a reply message *[Dictionary]. +type MessageHandler func(msg *Dictionary) (reply *Dictionary) + +// inactiveSessionSet configures the given [Session] with the [MessageHandler]. (macOS 13.0+) +// - https://developer.apple.com/documentation/xpc/xpc_session_set_incoming_message_handler +func (mh MessageHandler) inactiveSessionSet(s *Session) { + s.setIncomingMessageHandler(mh) +} + +// CancellationHandler is a function [SessionOption] that handles session cancellation in a [Session] +// It receives the [RichError] that caused the cancellation. +type CancellationHandler func(err *RichError) + +// inactiveSessionSet configures the given [Session] with the [CancellationHandler]. (macOS 13.0+) +// - https://developer.apple.com/documentation/xpc/xpc_session_set_cancel_handler +func (ch CancellationHandler) inactiveSessionSet(s *Session) { + s.setCancellationHandler(ch) +} + +// Reject rejects the incoming [Session] with the given reason. (macOS 14.0+) +// - https://developer.apple.com/documentation/xpc/xpc_listener_reject_peer +func (s *Session) Reject(reason string) { + cReason := C.CString(reason) + defer C.free(unsafe.Pointer(cReason)) + C.xpcListenerRejectPeer(s.Raw(), cReason) +} + +// Accept creates a [SessionHandler] that accepts incoming sessions with the given [SessionOption]s. +func Accept(sessionOptions ...SessionOption) SessionHandler { + return func(session *Session) { + for _, opt := range sessionOptions { + opt.inactiveSessionSet(session) + } + } +} + +// String returns a description of the [Session]. (macOS 13.0+) +// - https://developer.apple.com/documentation/xpc/xpc_session_copy_description +func (s *Session) String() string { + desc := C.xpcSessionCopyDescription(s.Raw()) + defer C.free(unsafe.Pointer(desc)) + return C.GoString(desc) +} + +// activate activates the [Session]. (macOS 13.0+) +// It is called internally after applying all [SessionOption]s in [NewSession]. +// - https://developer.apple.com/documentation/xpc/xpc_session_activate +func (s *Session) activate() error { + var err_out unsafe.Pointer + C.xpcSessionActivate(s.Raw(), &err_out) + if err_out != nil { + return newRichError(err_out) + } + return nil +} + +// callMessageHandler is called from C to handle incoming messages. +// +//export callMessageHandler +func callMessageHandler(cgoMessageHandler, cgoMessage uintptr) (reply unsafe.Pointer) { + handler := unwrapHandler[MessageHandler](cgoMessageHandler) + message := unwrapObject[*Dictionary](cgoMessage) + return handler(message).Raw() +} + +// setIncomingMessageHandler sets the [MessageHandler] for the inactive [Session]. (macOS 13.0+) +// - https://developer.apple.com/documentation/xpc/xpc_session_set_incoming_message_handler +func (s *Session) setIncomingMessageHandler(handler MessageHandler) { + cgoHandler, p := newCgoHandler(handler) + C.xpcSessionSetIncomingMessageHandler(s.Raw(), p) + // Store the handler after setting it to avoid premature garbage collection of the previous handler. + s.incomingMessageHandler = cgoHandler +} + +// Cancel cancels the [Session]. (macOS 13.0+) +// - https://developer.apple.com/documentation/xpc/xpc_session_cancel +func (s *Session) Cancel() { + C.xpcSessionCancel(s.Raw()) +} + +// callCancelHandler is called from C to handle session cancellation. +// +//export callCancelHandler +func callCancelHandler(cgoCancelHandler, cgoErr uintptr) { + handler := unwrapHandler[CancellationHandler](cgoCancelHandler) + err := unwrapObject[*RichError](cgoErr) + handler(err) +} + +// setCancellationHandler sets the [CancellationHandler] for the inactive [Session]. (macOS 13.0+) +// The handler will call [Session.handleCancellation] after executing the provided handler. +// - https://developer.apple.com/documentation/xpc/xpc_session_set_cancel_handler +func (s *Session) setCancellationHandler(handler CancellationHandler) { + cgoHandler, p := newCgoHandler((CancellationHandler)(func(err *RichError) { + if handler != nil { + handler(err) + } + s.handleCancellation(err) + })) + C.xpcSessionSetCancelHandler(s.Raw(), p) + // Store the handler after setting it to avoid premature garbage collection of the previous handler. + s.cancellationHandler = cgoHandler +} + +// handleCancellation handles [Session] cancellation by deleting the associated handles. +func (s *Session) handleCancellation(_ *RichError) { +} + +type ReplyHandler func(*Dictionary, *RichError) + +// callReplyHandler is called from C to handle reply messages. +// +//export callReplyHandler +func callReplyHandler(cgoReplyHandler uintptr, cgoReply, cgoError uintptr) { + handler := unwrapHandler[ReplyHandler](cgoReplyHandler) + reply := unwrapObject[*Dictionary](uintptr(cgoReply)) + err := unwrapObject[*RichError](cgoError) + handler(reply, err) +} + +// SendMessageWithReply sends a message *[Dictionary] to the [Session] and waits for a reply *[Dictionary]. (macOS 13.0+) +// +// Use [context.Context] to control cancellation and timeouts. +// - https://developer.apple.com/documentation/xpc/xpc_session_send_message_with_reply_async +func (s *Session) SendMessageWithReply(ctx context.Context, message *Dictionary) (*Dictionary, error) { + replyCh := make(chan *Dictionary, 1) + errCh := make(chan *RichError, 1) + replyHandler := (ReplyHandler)(func(reply *Dictionary, err *RichError) { + defer close(replyCh) + defer close(errCh) + if err != nil { + errCh <- Retain(err) + } else { + replyCh <- Retain(reply) + } + }) + cgoReplyHandler := cgo.NewHandle(replyHandler) + defer cgoReplyHandler.Delete() + C.xpcSessionSendMessageWithReplyAsync(s.Raw(), message.Raw(), C.uintptr_t(cgoReplyHandler)) + select { + case reply := <-replyCh: + return reply, nil + case err := <-errCh: + return nil, err + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +// SendDictionaryWithReply creates a message *[Dictionary] and calls [Session.SendMessageWithReply] with it. A`(macOS 13.0+) +// +// Use [context.Context] to control cancellation and timeouts. +// The message *[Dictionary] can be customized using [DictionaryEntry]. +func (s *Session) SendDictionaryWithReply(ctx context.Context, entries ...DictionaryEntry) (*Dictionary, error) { + return s.SendMessageWithReply(ctx, NewDictionary(entries...)) +} diff --git a/pkg/xpc/type.go b/pkg/xpc/type.go new file mode 100644 index 0000000..c3262f7 --- /dev/null +++ b/pkg/xpc/type.go @@ -0,0 +1,46 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +# include "xpc.h" +*/ +import "C" + +// Type represents an XPC type (xpc_type_t). +// - https://developer.apple.com/documentation/xpc/xpc_type_t?language=objc +type Type struct { + xpcType C.xpc_type_t +} + +var ( + TypeActivity = Type{C.XPC_TYPE_ACTIVITY} // https://developer.apple.com/documentation/xpc/xpc_type_activity-c.macro?language=objc + TypeArray = Type{C.XPC_TYPE_ARRAY} // https://developer.apple.com/documentation/xpc/xpc_type_array-c.macro?language=objc + TypeBool = Type{C.XPC_TYPE_BOOL} // https://developer.apple.com/documentation/xpc/xpc_type_bool-c.macro?language=objc + TypeConnection = Type{C.XPC_TYPE_CONNECTION} // https://developer.apple.com/documentation/xpc/xpc_type_connection-c.macro?language=objc + TypeData = Type{C.XPC_TYPE_DATA} // https://developer.apple.com/documentation/xpc/xpc_type_data-c.macro?language=objc + TypeDate = Type{C.XPC_TYPE_DATE} // https://developer.apple.com/documentation/xpc/xpc_type_date-c.macro?language=objc + TypeDictionary = Type{C.XPC_TYPE_DICTIONARY} // https://developer.apple.com/documentation/xpc/xpc_type_dictionary-c.macro?language=objc + TypeDouble = Type{C.XPC_TYPE_DOUBLE} // https://developer.apple.com/documentation/xpc/xpc_type_double-c.macro?language=objc + TypeEndpoint = Type{C.XPC_TYPE_ENDPOINT} // https://developer.apple.com/documentation/xpc/xpc_type_endpoint-c.macro?language=objc + TypeError = Type{C.XPC_TYPE_ERROR} // https://developer.apple.com/documentation/xpc/xpc_type_error-c.macro?language=objc + TypeFD = Type{C.XPC_TYPE_FD} // https://developer.apple.com/documentation/xpc/xpc_type_fd-c.macro?language=objc + TypeInt64 = Type{C.XPC_TYPE_INT64} // https://developer.apple.com/documentation/xpc/xpc_type_int64-c.macro?language=objc + TypeNull = Type{C.XPC_TYPE_NULL} // https://developer.apple.com/documentation/xpc/xpc_type_null-c.macro?language=objc + TypeRichError = Type{C.XPC_TYPE_RICH_ERROR} // does not have official documentation, but defined in + TypeSession = Type{C.XPC_TYPE_SESSION} // does not have official documentation, but defined in + TypeShmem = Type{C.XPC_TYPE_SHMEM} // https://developer.apple.com/documentation/xpc/xpc_type_shmem-c.macro?language=objc + TypeString = Type{C.XPC_TYPE_STRING} // https://developer.apple.com/documentation/xpc/xpc_type_string-c.macro?language=objc + TypeUInt64 = Type{C.XPC_TYPE_UINT64} // https://developer.apple.com/documentation/xpc/xpc_type_uint64-c.macro?language=objc + TypeUUID = Type{C.XPC_TYPE_UUID} // https://developer.apple.com/documentation/xpc/xpc_type_uuid-c.macro?language=objc +) + +// String returns the name of the [XpcType]. +// see: https://developer.apple.com/documentation/xpc/xpc_type_get_name(_:)?language=objc +func (t Type) String() string { + cs := C.xpc_type_get_name(t.xpcType) + if cs == nil { + return "" + } + // do not free cs since it is managed by XPC runtime. + return C.GoString(cs) +} diff --git a/pkg/xpc/xpc.h b/pkg/xpc/xpc.h new file mode 100644 index 0000000..f389a2c --- /dev/null +++ b/pkg/xpc/xpc.h @@ -0,0 +1,252 @@ +#pragma once + +#import +#import +#import + +// MARK: - Macros + +// To avoid including virtualization_helper.h, copied from there. +NSDictionary *xpcDumpProcessinfo(); + +#define RAISE_REASON_MESSAGE \ + "This may possibly be a bug due to library handling errors.\n" \ + "I would appreciate it if you could report it to https://github.com/Code-Hex/vz/issues/new/choose\n\n" \ + "Information: %@\n" + +#define RAISE_UNSUPPORTED_MACOS_EXCEPTION() \ + do { \ + [NSException \ + raise:@"UnhandledAvailabilityException" \ + format:@RAISE_REASON_MESSAGE, xpcDumpProcessinfo()]; \ + __builtin_unreachable(); \ + } while (0) + +// for macOS 13 API +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 130000 +#define INCLUDE_TARGET_OSX_13 1 +#else +#pragma message("macOS 13 API has been disabled") +#endif + +// for macOS 14 API +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 140000 +#define INCLUDE_TARGET_OSX_14 1 +#else +#pragma message("macOS 14 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 + +// MARK: - dispatch_queue_t + +void *dispatchQueueCreateSerial(const char *label); +void dispatchRelease(void *queue); + +// MARK: - xpc.h types +// +// The following types are listed in the same order as the XPC documentation index page. +// https://developer.apple.com/documentation/xpc?language=objc + +// MARK: - xpc_listener_t (macOS 14+) + +void *xpcListenerCreate(const char *service_name, void *queue, uint64_t flags, uintptr_t cgo_session_handler, void **error_out); +const char *xpcListenerCopyDescription(void *listener); +bool xpcListenerActivate(void *listener, void **error_out); +void xpcListenerCancel(void *listener); +void xpcListenerRejectPeer(void *session, const char *reason); +// int xpcListenerSetPeerCodeSigningRequirement(void *listener, const char *requirement); + +// MARK: - xpc_session_t (XPC_TYPE_SESSION) (macOS 13+) + +// void *xpcSessionCreateXpcService(const char *service_name, void *queue, uint64_t flags, void **error_out); +void *xpcSessionCreateMachService(const char *service_name, void *queue, uint64_t flags, void **error_out); +// void xpcSessionSetTargetQueue(void *session, void *queue); +const char *xpcSessionCopyDescription(void *session); +bool xpcSessionActivate(void *session, void **error_out); +void xpcSessionSetIncomingMessageHandler(void *session, uintptr_t cgo_message_handler); +void xpcSessionCancel(void *session); +void xpcSessionSetCancelHandler(void *session, uintptr_t cgo_cancel_handler); +// void *xpcSessionSendMessage(void *session, void *message); +void xpcSessionSendMessageWithReplyAsync(void *session, void *message, uintptr_t cgo_reply_handler); +// void *xpcSessionSendMessageWithReplySync(void *session, void *message, void **error_out); + +// MARK: - xpc_rich_error_t (XPC_TYPE_RICH_ERROR) +bool xpcRichErrorCanRetry(void *err); +const char *xpcRichErrorCopyDescription(void *err); + +// MARK: - Identity + +// # xpc_type_t +xpc_type_t xpcGetType(void *object); +const char *xpcTypeGetName(xpc_type_t type); +// # xpc_object_t +// size_t xpxHash(void *object); + +// MARK: - Comparison + +// # xpc_object_t +// bool xpcEqual(void *object1, void *object2); + +// MARK: - Copying + +// # xpc_object_t +// void *xpcCopy(void *object); +const char *xpcCopyDescription(void *object); + +// MARK: - Boolean objects + +// # xpc_object_t (XPC_TYPE_BOOL) +// void *xpcBoolCreate(bool value); +// bool xpcBoolGetValue(void *object); +// XPC_BOOL_TRUE +// XPC_BOOL_FALSE + +// MARK: - Data objects + +// # xpc_object_t (XPC_TYPE_DATA) +void *xpcDataCreate(const void *bytes, size_t length); +// size_t xpcDataGetBytes(void *object, void *buffer, size_t offset, size_t length); +// const void *xpcDataGetBytesPtr(void *object); +// size_t xpcDataGetLength(void *object); + +// MARK: - Number objects + +// # xpc_object_t (XPC_TYPE_DOUBLE) +// void *xpcDoubleCreate(double value); +// double xpcDoubleGetValue(void *object); + +// # xpc_object_t (XPC_TYPE_INT64) +// void *xpcInt64Create(int64_t value); +// int64_t xpcInt64GetValue(void *object); + +// # xpc_object_t (XPC_TYPE_UINT64) +// void *xpcUInt64Create(uint64_t value); +// uint64_t xpcUInt64GetValue(void *object); + +// MARK: - Array objects + +// # xpc_object_t (XPC_TYPE_ARRAY) +void *xpcArrayCreate(void *const *object, size_t count); +// void *xpcArrayCreateEmpty(); +// void *xpcArrayCreateConnection +// void *xpcArrayGetValue(void *object, size_t index); +// void xpcArraySetValue(void *object, size_t index, void *value); +// void xpcArrayAppendValue(void *object, void *value); +size_t xpcArrayGetCount(void *object); +bool xpcArrayApply(void *object, uintptr_t cgo_applier); +// int xcpArrayDupFds(void *object, size_t index); +// void *xpcArrayGetArray(void *object, size_t index); +// bool xpcArrayGetBool(void *object, size_t index); +// const void *xpcArrayGetData(void *object, size_t index, size_t *length); +// int64_t xpcArrayGetDate(void *object, size_t index); +// void *xpcArrayGetDictionary(void *object, size_t index); +// double xpcArrayGetDouble(void *object, size_t index); +// int64_t xpcArrayGetInt64(void *object, size_t index); +// const char *xpcArrayGetString(void *object, size_t index); +// uint64_t xpcArrayGetUInt64(void *object, size_t index); +// uint8_t *xpcArrayGetUUID(void *object, size_t index); +// void xpcArraySetBool(void *object, size_t index, bool value); +// void xpcArraySetConnection +// void xpcArraySetData(void *object, size_t index, const void *bytes, size_t length); +// void xpcArraySetDate(void *object, size_t index, int64_t value); +// void xpcArraySetDouble(void *object, size_t index, double value); +// void xpcArraySetFd(void *object, size_t index, int fd); +// void xpcArraySetInt64(void *object, size_t index, int64_t value); +// void xpcArraySetString(void *object, size_t index, const char *string); +// void xpcArraySetUInt64(void *object, size_t index, uint64_t value); +// void xpcArraySetUUID(void *object, size_t index, const uuid_t uuid); +// XPC_ARRAY_APPEND + +// MARK: - Dictionary objects + +// # xpc_object_t (XPC_TYPE_DICTIONARY) +// void *xpcDictionaryCreate(const char *const *keys, void *const *values, size_t count); +void *xpcDictionaryCreateEmpty(void); +// void *xpcDictionaryCreateConnection +void *xpcDictionaryCreateReply(void *object); +void xpcDictionarySetValue(void *object, const char *key, void *value); +// size_t xpcDictionaryGetCount(void *object); +void *xpcDictionaryGetValue(void *object, const char *key); +bool xpcDictionaryApply(void *object, uintptr_t cgo_applier); +// int xpcDictionaryDupFd(void *object, const char *key); +void *xpcDictionaryGetArray(void *object, const char *key); +// bool xpcDictionaryGetBool(void *object, const char *key); +const void *xpcDictionaryGetData(void *object, const char *key, size_t *length); +// int64_t xpcDictionaryGetDate(void *object, const char *key); +// void *xpcDictionaryGetDictionary(void *object, const char *key); +// double xpcDictionaryGetDouble(void *object, const char *key); +// int64_t xpcDictionaryGetInt64(void *object, const char *key); +// void *xpcDictionaryGetRemoteConnection +const char *xpcDictionaryGetString(void *object, const char *key); +// uint64_t xpcDictionaryGetUInt64(void *object, const char *key); +// uint8_t *xpcDictionaryGetUUID(void *object, const char *key); +// void xpcDictionarySetBool(void *object, const char *key, bool value); +// void xpcDictionarySetConnection +// void xpcDictionarySetData(void *object, const char *key, const void *bytes, size_t length); +// void xpcDictionarySetDate(void *object, const char *key, int64_t value); +// void xpcDictionarySetDouble(void *object, const char *key, double value); +// void xpcDictionarySetFd(void *object, const char *key, int fd); +// void xpcDictionarySetInt64(void *object, const char *key, int64_t value); +void xpcDictionarySetString(void *object, const char *key, const char *value); +// void xpcDictionarySetUInt64(void *object, const char *key, uint64_t value); +// void xpcDictionarySetUUID(void *object, const char *key, const uuid_t uuid); +// void *xpcDictionaryCopyMachSend +// void xpcDictionarySetMachSend + +// MARK: - String objects + +// # xpc_object_t (XPC_TYPE_STRING) +void *xpcStringCreate(const char *string); +// void *xpcStringCreateWithFormat(const char *format, ...); +// void *xpcStringCreateWithFormatAndArguments(const char *format, va_list args); +// size_t xpcStringGetLength(void *object); +// const char *xpcStringGetStringPtr(void *object); + +// MARK: - File Descriptor objects + +// # xpc_object_t (XPC_TYPE_FD) +// void *xpcFdCreate(int fd); +// int xpcFdDup(void *object); + +// MARK: - Date objects + +// # xpc_object_t (XPC_TYPE_DATE) +// void *xpcDateCreate(int64_t interval); +// void *xpcDateFromCurrent(); +// int64_t xpcDateGetValue(void *object); + +// MARK: - UUID objects + +// # xpc_object_t (XPC_TYPE_UUID) +// void *xpcUUIDCreate(const uuid_t uuid); +// const uint8_t *xpcUUIDGetBytes(void *object); + +// MARK: - Shared Memory objects + +// # xpc_object_t (XPC_TYPE_SHMEM) +// void *xpcShmemCreate(void *region, size_t length); +// size_t xpcShmemMap(void *object, void **region); + +// MARK: - Null objects +// # xpc_object_t (XPC_TYPE_NULL) +// void *xpcNullCreate(); + +// MARK: - Object life cycle +void *xpcRetain(void *object); +void xpcRelease(void *object); + +// MARK: - xpc_peer_requirement_t (macOS 26+) +void xpcListenerSetPeerRequirement(void *listener, void *peer_requirement); +// void *xpcPeerRequirementCreateEntitlementExists(const char *entitlement, void **error_out); +// void *xpcPeerRequirementCreateEntitlementMatchesValue(const char *entitlement, void *value, void **error_out); +void *xpcPeerRequirementCreateLwcr(void *lwcr, void **error_out); +// void *xpcPeerRequirementCreatePlatformIdentity(const char * signing_identifier, void **error_out); +// void *xpcPeerRequirementCreateTeamIdentity(const char * team_identifier, void **error_out); +bool xpcPeerRequirementMatchReceivedMessage(void *peer_requirement, void *message, void **error_out); +void xpcSessionSetPeerRequirement(void *session, void *peer_requirement); diff --git a/pkg/xpc/xpc.m b/pkg/xpc/xpc.m new file mode 100644 index 0000000..3ed4bb3 --- /dev/null +++ b/pkg/xpc/xpc.m @@ -0,0 +1,417 @@ +#include "xpc.h" + +// MARK: - Macros + +// To avoid including virtualization_helper.h, copied from there. +NSDictionary *xpcDumpProcessinfo() +{ + NSString *osVersionString = [[NSProcessInfo processInfo] operatingSystemVersionString]; + return @{ + @"LLVM (Clang) Version" : @__VERSION__, +#ifdef __arm64__ + @"Target for arm64" : @1, +#else + @"Target for arm64" : @0, +#endif + // The version of the macOS on which the process is executing. + @"Running OS Version" : osVersionString, +#ifdef __MAC_OS_X_VERSION_MAX_ALLOWED + @"Max Allowed OS Version" : @__MAC_OS_X_VERSION_MAX_ALLOWED, +#endif +#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED + @"Min Required OS Version" : @__MAC_OS_X_VERSION_MIN_REQUIRED, +#endif + }; +} + +// MARK: - Helper functions defined in Go + +// # xpc_object_t +extern uintptr_t wrapRawObject(void *obj); +// # xpc_listener_t +extern void callSessionHandler(uintptr_t cgoSessionHandler, uintptr_t cgoSession); + +// # xpc_session_t +extern void callReplyHandler(uintptr_t cgoReplyHandler, uintptr_t cgoReply, uintptr_t cgoError); +extern void callCancelHandler(uintptr_t cgoCancelHandler, uintptr_t cgoError); +extern void *callMessageHandler(uintptr_t cgoMessageHandler, uintptr_t cgoMessage); + +// # xpc_object_t (XPC_TYPE_ARRAY) +extern bool callArrayApplier(uintptr_t cgoApplier, size_t index, uintptr_t cgoValue); +// # xpc_object_t (XPC_TYPE_DICTIONARY) +extern bool callDictionaryApplier(uintptr_t cgoApplier, const char *_Nonnull key, uintptr_t cgoValue); + +// MARK: -dispatch_queue_t + +void *dispatchQueueCreateSerial(const char *label) +{ + return dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL); +} + +void dispatchRelease(void *queue) +{ + dispatch_release((dispatch_queue_t)queue); +} + +// MARK: - xpc.h types +// +// The following types are listed in the same order as the XPC documentation index page. +// https://developer.apple.com/documentation/xpc?language=objc + +// MARK: - xpc_listener_t (macOS 14+) + +void *xpcListenerCreate(const char *service_name, void *queue, uint64_t flags, uintptr_t cgo_session_handler, void **error_out) +{ +#ifdef INCLUDE_TARGET_OSX_14 + if (@available(macOS 14, *)) { + return xpc_listener_create( + service_name, + queue, + flags, + ^(xpc_session_t _Nonnull session) { + callSessionHandler(cgo_session_handler, wrapRawObject(session)); + }, + (xpc_rich_error_t *)error_out); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +const char *xpcListenerCopyDescription(void *listener) +{ +#ifdef INCLUDE_TARGET_OSX_14 + if (@available(macOS 14, *)) { + return xpc_listener_copy_description((xpc_listener_t)listener); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +bool xpcListenerActivate(void *listener, void **error_out) +{ +#ifdef INCLUDE_TARGET_OSX_14 + if (@available(macOS 14, *)) { + return xpc_listener_activate((xpc_listener_t)listener, (xpc_rich_error_t *)error_out); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void xpcListenerCancel(void *listener) +{ +#ifdef INCLUDE_TARGET_OSX_14 + if (@available(macOS 14, *)) { + xpc_listener_cancel((xpc_listener_t)listener); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void xpcListenerRejectPeer(void *session, const char *reason) +{ +#ifdef INCLUDE_TARGET_OSX_14 + if (@available(macOS 14, *)) { + xpc_listener_reject_peer((xpc_session_t)session, reason); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// MARK: - xpc_session_t (XPC_TYPE_SESSION) (macOS 13+) + +void *xpcSessionCreateMachService(const char *service_name, void *queue, uint64_t flags, void **error_out) +{ +#ifdef INCLUDE_TARGET_OSX_13 + if (@available(macOS 13, *)) { + return xpc_session_create_mach_service( + service_name, + (dispatch_queue_t)queue, + flags, + (xpc_rich_error_t *)error_out); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +const char *xpcSessionCopyDescription(void *session) +{ +#ifdef INCLUDE_TARGET_OSX_13 + if (@available(macOS 13, *)) { + return xpc_session_copy_description((xpc_session_t)session); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +bool xpcSessionActivate(void *session, void **error_out) +{ +#ifdef INCLUDE_TARGET_OSX_13 + if (@available(macOS 13, *)) { + return xpc_session_activate((xpc_session_t)session, (xpc_rich_error_t *)error_out); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void xpcSessionSetIncomingMessageHandler(void *session, uintptr_t cgo_message_handler) +{ +#ifdef INCLUDE_TARGET_OSX_13 + if (@available(macOS 13, *)) { + xpc_session_set_incoming_message_handler( + (xpc_session_t)session, + ^(xpc_object_t _Nonnull message) { + // Ensure the message is a dictionary. + if (xpc_get_type(message) != XPC_TYPE_DICTIONARY) { + xpc_session_cancel((xpc_session_t)session); + return; + } + xpc_object_t reply = (xpc_object_t)callMessageHandler(cgo_message_handler, wrapRawObject(message)); + xpc_rich_error_t err; + do { + err = xpc_session_send_message(session, reply); + } while (err != nil && xpc_rich_error_can_retry(err)); + xpc_release(reply); + }); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void xpcSessionCancel(void *session) +{ +#ifdef INCLUDE_TARGET_OSX_13 + if (@available(macOS 13, *)) { + xpc_session_cancel((xpc_session_t)session); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void xpcSessionSetCancelHandler(void *session, uintptr_t cgo_cancel_handler) +{ +#ifdef INCLUDE_TARGET_OSX_13 + if (@available(macOS 13, *)) { + xpc_session_set_cancel_handler( + (xpc_session_t)session, + ^(xpc_rich_error_t _Nonnull err) { + callCancelHandler(cgo_cancel_handler, wrapRawObject(err)); + }); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void xpcSessionSendMessageWithReplyAsync(void *session, void *message, uintptr_t cgo_reply_handler) +{ +#ifdef INCLUDE_TARGET_OSX_13 + if (@available(macOS 13, *)) { + xpc_session_send_message_with_reply_async( + (xpc_session_t)session, + (xpc_object_t)message, + ^(xpc_object_t _Nonnull reply, xpc_rich_error_t _Nullable error) { + callReplyHandler(cgo_reply_handler, wrapRawObject(reply), wrapRawObject(error)); + }); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// MARK: - xpc_rich_error_t (XPC_TYPE_RICH_ERROR) + +bool xpcRichErrorCanRetry(void *err) +{ + return xpc_rich_error_can_retry((xpc_rich_error_t)err); +} + +const char *xpcRichErrorCopyDescription(void *err) +{ + return xpc_rich_error_copy_description((xpc_rich_error_t)err); +} + +// MARK: - Identity + +// # xpc_type_t + +xpc_type_t xpcGetType(void *object) +{ + return xpc_get_type((xpc_object_t)object); +} + +const char *xpcTypeGetName(xpc_type_t type) +{ + return xpc_type_get_name(type); +} + +// MARK: - Comparison +// MARK: - Copying + +// # xpc_object_t +const char *xpcCopyDescription(void *object) +{ + return xpc_copy_description((xpc_object_t)object); +} + +// MARK: - Boolean objects + +// MARK: - Data objects + +// # xpc_object_t (XPC_TYPE_DATA) + +void *xpcDataCreate(const void *bytes, size_t length) +{ + return xpc_data_create(bytes, length); +} + +// MARK: - Number objects + +// MARK: - Array objects + +// # xpc_object_t (XPC_TYPE_ARRAY) + +void *xpcArrayCreate(void *const *object, size_t count) +{ + return xpc_array_create((xpc_object_t const *)object, count); +} + +size_t xpcArrayGetCount(void *object) +{ + return xpc_array_get_count((xpc_object_t)object); +} + +bool xpcArrayApply(void *object, uintptr_t cgo_applier) +{ + return xpc_array_apply((xpc_object_t)object, ^bool(size_t index, xpc_object_t _Nonnull value) { + return callArrayApplier(cgo_applier, index, wrapRawObject(value)); + }); +} + +// MARK: - Dictionary objects + +// xpc_object_t (XPC_TYPE_DICTIONARY) + +void *xpcDictionaryCreateEmpty(void) +{ + return xpc_dictionary_create_empty(); +} + +void *xpcDictionaryCreateReply(void *object) +{ + return xpc_dictionary_create_reply((xpc_object_t)object); +} + +void xpcDictionarySetValue(void *object, const char *key, void *value) +{ + xpc_dictionary_set_value((xpc_object_t)object, key, (xpc_object_t)value); +} + +void *xpcDictionaryGetValue(void *object, const char *key) +{ + return xpc_dictionary_get_value((xpc_object_t)object, key); +} + +bool xpcDictionaryApply(void *object, uintptr_t cgo_applier) +{ + return xpc_dictionary_apply((xpc_object_t)object, ^bool(const char *_Nonnull key, xpc_object_t _Nonnull value) { + return callDictionaryApplier(cgo_applier, key, wrapRawObject(value)); + }); +} + +void *xpcDictionaryGetArray(void *object, const char *key) +{ + return xpc_dictionary_get_array((xpc_object_t)object, key); +} + +const void *xpcDictionaryGetData(void *object, const char *key, size_t *length) +{ + return xpc_dictionary_get_data((xpc_object_t)object, key, length); +} + +const char *xpcDictionaryGetString(void *object, const char *key) +{ + return xpc_dictionary_get_string((xpc_object_t)object, key); +} + +void xpcDictionarySetString(void *object, const char *key, const char *value) +{ + xpc_dictionary_set_string((xpc_object_t)object, key, value); +} + +// MARK: - String objects + +void *xpcStringCreate(const char *string) +{ + return xpc_string_create(string); +} + +// MARK: - File descriptor objects +// MARK: - Date objects +// MARK: - UUID objects +// MARK: - Shared memory objects +// MARK: - Null objects + +// MARK: - Object life cycle + +// xpc_object_t + +void *xpcRetain(void *object) +{ + return xpc_retain((xpc_object_t)object); +} + +void xpcRelease(void *object) +{ + xpc_release((xpc_object_t)object); +} + +// MARK: - xpc_peer_requirement_t (macOS 26+) + +void xpcListenerSetPeerRequirement(void *listener, void *peer_requirement) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + xpc_listener_set_peer_requirement((xpc_listener_t)listener, (xpc_peer_requirement_t)peer_requirement); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void *xpcPeerRequirementCreateLwcr(void *lwcr, void **error_out) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return xpc_peer_requirement_create_lwcr((xpc_object_t)lwcr, (xpc_rich_error_t *)error_out); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +bool xpcPeerRequirementMatchReceivedMessage(void *peer_requirement, void *message, void **error_out) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return xpc_peer_requirement_match_received_message( + (xpc_peer_requirement_t)peer_requirement, + (xpc_object_t)message, + (xpc_rich_error_t *)error_out); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void xpcSessionSetPeerRequirement(void *session, void *peer_requirement) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + xpc_session_set_peer_requirement((xpc_session_t)session, (xpc_peer_requirement_t)peer_requirement); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} \ No newline at end of file diff --git a/pkg/xpc/xpc_object.go b/pkg/xpc/xpc_object.go new file mode 100644 index 0000000..b31b1d9 --- /dev/null +++ b/pkg/xpc/xpc_object.go @@ -0,0 +1,67 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +# include "xpc.h" +*/ +import "C" +import ( + "runtime" + "unsafe" +) + +// XpcObject wraps an XPC object (xpc_object_t). +// It is expected to be embedded in other structs as pointer to provide common functionality. +// - https://developer.apple.com/documentation/xpc/xpc_object_t?language=objc +type XpcObject struct { + p unsafe.Pointer +} + +var _ Object = (*XpcObject)(nil) + +// Raw returns the raw xpc_object_t as [unsafe.Pointer]. +func (x *XpcObject) Raw() unsafe.Pointer { + return x.p +} + +// Type returns the [Type] of the [XpcObject]. +// - https://developer.apple.com/documentation/xpc/xpc_get_type(_:)?language=objc +func (x *XpcObject) Type() Type { + return Type{C.xpcGetType(x.p)} +} + +// String returns the description of the [XpcObject]. +// - https://developer.apple.com/documentation/xpc/xpc_copy_description(_:)?language=objc +func (x *XpcObject) String() string { + cs := C.xpcCopyDescription(x.Raw()) + defer C.free(unsafe.Pointer(cs)) + return C.GoString(cs) +} + +// retain retains the [XpcObject]. +// It also uses [runtime.SetFinalizer] to call [XpcObject.release] when it is garbage collected. +// - https://developer.apple.com/documentation/xpc/xpc_retain?language=objc +func (x *XpcObject) retain() { + C.xpcRetain(x.p) + _ = ReleaseOnCleanup(x) +} + +// releaseOnCleanup registers a cleanup function to release the XpcObject when cleaned up. +// - https://developer.apple.com/documentation/xpc/xpc_release?language=objc +func (x *XpcObject) releaseOnCleanup() { + runtime.AddCleanup(x, func(p unsafe.Pointer) { + C.xpcRelease(p) + }, x.p) +} + +// Retain calls retain method on the given object and returns it. +func Retain[T interface{ retain() }](o T) T { + o.retain() + return o +} + +// ReleaseOnCleanup calls releaseOnCleanup method on the given object and returns it. +func ReleaseOnCleanup[O interface{ releaseOnCleanup() }](o O) O { + o.releaseOnCleanup() + return o +} diff --git a/pkg/xpc/xpc_test.go b/pkg/xpc/xpc_test.go new file mode 100644 index 0000000..cf4e781 --- /dev/null +++ b/pkg/xpc/xpc_test.go @@ -0,0 +1,239 @@ +package xpc_test + +import ( + "bytes" + "context" + "flag" + "fmt" + "log" + "os" + "os/exec" + "os/signal" + "path" + "runtime" + "slices" + "strconv" + "strings" + "syscall" + "testing" + "text/template" + + "github.com/Code-Hex/vz/v3/pkg/xpc" + "golang.org/x/sys/unix" +) + +var server bool + +func init() { + // Determine if running as server or client based on command-line arguments + flag.BoolVar(&server, "server", false, "run as mach service server") +} + +func TestMachService(t *testing.T) { + if err := macOSAvailable(14); err != nil { + t.Skip("xpc listener is supported from macOS 14") + } + + label := "dev.code-hex.vz.xpc.test" + machServiceName := label + ".greeting" + + if server { + t.Log("running as mach service server") + listener, err := xpcGreetingServer(t, machServiceName) + if err != nil { + log.Printf("failed to create mach service server: %v", err) + t.Fatal(err) + } + if err := listener.Activate(); err != nil { + log.Printf("failed to activate mach service server: %v", err) + t.Fatal(err) + } + ctx, stop := signal.NotifyContext(t.Context(), os.Interrupt, syscall.SIGTERM) + defer stop() + <-ctx.Done() + _ = listener.Close() + } else { + t.Log("running as mach service client") + xpcRegisterMachService(t, label, machServiceName) + greeting := "Hello, Mach Service!" + greetingReply, err := xpcClientRequestingGreeting(t, machServiceName, greeting) + if err != nil { + t.Fatal(err) + } + if greetingReply != greeting { + t.Fatalf("expected greeting reply %q to equal greeting %q", greetingReply, greeting) + } + } +} + +// macOSAvailable checks if the current macOS version is equal to or higher than the required version. +// required should be provided as major, minor, patch integers. +// For example, to check for macOS 13.4.1 or higher, call macOSAvailable(13, 4, 1). +// If the current macOS version is lower than the required version, an error is returned. +func macOSAvailable(required ...int) error { + name := "kern.osproductversion" + osproductversion, err := unix.Sysctl(name) + if err != nil { + return fmt.Errorf("failed to get macOS product version: %w", err) + } + splitted := strings.Split(osproductversion, ".") + nMin := min(len(splitted), len(required)) + actual := make([]int, 0, nMin) + for _, s := range splitted[:nMin] { + n, err := strconv.Atoi(s) + if err != nil { + return fmt.Errorf("failed to parse %q: %q: %w", name, osproductversion, err) + } + actual = append(actual, n) + } + if slices.Compare(required, actual) <= 0 { + return nil + } + return fmt.Errorf("required macOS product version %v but current is %s", required, osproductversion) +} + +// xpcGreetingServer creates an Mach service XPC listener that echoes back greetings. +func xpcGreetingServer(t *testing.T, machServiceName string) (*xpc.Listener, error) { + return xpc.NewListener( + machServiceName, + xpc.Accept( + xpc.MessageHandler(func(dic *xpc.Dictionary) *xpc.Dictionary { + createErrorReply := func(errMsg string, args ...any) *xpc.Dictionary { + errorString := fmt.Sprintf(errMsg, args...) + log.Print(errorString) + t.Error(errorString) + return dic.CreateReply( + xpc.KeyValue("Error", xpc.NewString(errorString)), + ) + } + var reply *xpc.Dictionary + if greeting := dic.GetString("Greeting"); greeting == "" { + reply = createErrorReply("missing greeting in request") + } else { + reply = dic.CreateReply( + xpc.KeyValue("Greeting", xpc.NewString(greeting)), + ) + } + return reply + }), + ), + ) +} + +const launchdPlistTemplate = ` + + + + Label + {{.Label}} + ProgramArguments + + {{- range $arg := .ProgramArguments}} + {{$arg}} + {{- end}} + + RunAtLoad + + WorkingDirectory + {{ .WorkingDirectory }} + StandardErrorPath + {{ .WorkingDirectory }}/xpc_test.stderr.log + + MachServices + + {{- range $service := .MachServices}} + {{$service}} + + {{- end}} + + +` + +// xpcRegisterMachService registers the test executable as a Mach service +// using launchctl with the given label and machServiceName. +func xpcRegisterMachService(t *testing.T, label, machServiceName string) { + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + params := struct { + Label string + ProgramArguments []string + WorkingDirectory string + MachServices []string + }{ + Label: label, + ProgramArguments: []string{os.Args[0], "-test.run", "^" + funcName(t, 2) + "$", "-server"}, + WorkingDirectory: cwd, + MachServices: []string{machServiceName}, + } + template, err := template.New("plist").Parse(launchdPlistTemplate) + if err != nil { + t.Fatal(err) + } + var b bytes.Buffer + if err := template.Execute(&b, params); err != nil { + t.Fatal(err) + } + userHomeDir, err := os.UserHomeDir() + if err != nil { + t.Fatal(err) + } + launchAgentDir := path.Join(userHomeDir, "Library", "LaunchAgents", label+".plist") + if err := os.WriteFile(launchAgentDir, b.Bytes(), 0o644); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.Remove(launchAgentDir); err != nil { + t.Logf("failed to remove launch agent plist: %v", err) + } + }) + cmd := exec.CommandContext(t.Context(), "launchctl", "load", launchAgentDir) + if err := cmd.Run(); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + // do not use t.Context() here to ensure unload runs + cmd := exec.CommandContext(context.Background(), "launchctl", "unload", launchAgentDir) + if err := cmd.Run(); err != nil { + t.Logf("failed to unload launch agent: %v", err) + } + }) +} + +// funcName returns the name of the calling function. +// It is used to get the test function name for launchctl registration. +func funcName(t *testing.T, skip int) string { + pc, _, _, ok := runtime.Caller(skip) + if !ok { + t.Fatal("failed to get caller info") + } + funcNameComponents := strings.Split(runtime.FuncForPC(pc).Name(), ".") + return funcNameComponents[len(funcNameComponents)-1] +} + +// xpcClientRequestingGreeting requests a VmnetNetwork serialization for the given subnet +// from the Mach service and returns the deserialized VmnetNetwork instance. +func xpcClientRequestingGreeting(t *testing.T, machServiceName, greeting string) (string, error) { + session, err := xpc.NewSession( + machServiceName, + ) + if err != nil { + return "", err + } + defer session.Cancel() + + resp, err := session.SendDictionaryWithReply( + t.Context(), xpc.KeyValue("Greeting", xpc.NewString(greeting)), + ) + if err != nil { + return "", err + } + errorStr := resp.GetString("Error") + if errorStr != "" { + return "", fmt.Errorf("xpc service error: %s", errorStr) + } + greetingReply := resp.GetString("Greeting") + return greetingReply, nil +} diff --git a/shared_directory_arm64_test.go b/shared_directory_arm64_test.go index 9cc2fcf..dffee60 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_11.h b/virtualization_11.h index f9f9fdc..0dd07f3 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 bab7e29..5350852 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 0000000..1926ae8 --- /dev/null +++ b/virtualization_26.h @@ -0,0 +1,22 @@ +// +// 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 */ +// 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 0000000..f39b55c --- /dev/null +++ b/virtualization_26.m @@ -0,0 +1,30 @@ +// +// virtualization_26.m +// +// Created by codehex. +// + +#import "virtualization_26.h" + +// 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 7914efd..2f25466 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/virtualization_test.go b/virtualization_test.go index 9be7357..1d78b33 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 0000000..ecc1e90 --- /dev/null +++ b/vmnet_test.go @@ -0,0 +1,460 @@ +package vz_test + +import ( + "bytes" + "context" + _ "embed" + "encoding/hex" + "flag" + "fmt" + "log" + "net" + "net/netip" + "os" + "os/exec" + "os/signal" + "path" + "runtime" + "slices" + "strings" + "syscall" + "testing" + "text/template" + + "github.com/Code-Hex/vz/v3" + "github.com/Code-Hex/vz/v3/pkg/vmnet" + "github.com/Code-Hex/vz/v3/pkg/xpc" +) + +// TestVmnetSharedModeAllowsCommunicationBetweenMultipleVMs tests VmnetNetwork in SharedMode +// allows communication between multiple VMs connected to the same VmnetNetwork instance. +// This test creates two VmnetNetwork instances by serializing and deserializing the first instance, +// then boots a VM using each VmnetNetwork instance and tests communication between the two VMs. +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 := vmnet.NewNetworkConfiguration(vmnet.SharedMode) + if err != nil { + t.Fatal(err) + } + network1, err := vmnet.NewNetwork(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 := vmnet.NewNetworkWithSerialization(serialization) + if err != nil { + t.Fatal(err) + } + macaddress2 := randomMACAddress(t) + + container1 := newVirtualizationMachine(t, configureNetworkDevice(network1, macaddress1)) + container2 := newVirtualizationMachine(t, configureNetworkDevice(network2, macaddress2)) + 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 := network1.IPv4Subnet() + if err != nil { + t.Fatal(err) + } + t.Logf("Vmnet network IPv4 subnet: %s", ipv4Subnet.String()) + prefix, err := network1.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) +} + +// TestVmnetSharedModeWithConfiguringIPv4 tests VmnetNetwork in SharedMode +// with custom IPv4 subnet and DHCP reservation. +// This test creates a VmnetNetwork instance with a specified IPv4 subnet and DHCP reservation, +// then boots a VM using the VmnetNetwork and verifies the VM receives the expected IP address. +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 := vmnet.NewNetworkConfiguration(vmnet.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 := netip.MustParseAddr("192.168.5.15") + if err := config.AddDhcpReservation(macaddress.HardwareAddr(), ipv4); err != nil { + t.Fatal(err) + } + + // Create VmnetNetwork instance + network, err := vmnet.NewNetwork(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.String() != containerIPv4 { + t.Fatalf("expected IPv4 %s, but got %s", ipv4, containerIPv4) + } +} + +var server bool + +func init() { + // Determine if running as server or client based on command-line arguments + flag.BoolVar(&server, "server", false, "run as mach service server") +} + +// TestVmnetNetworkShareModeSharingOverXpc tests sharing VmnetNetwork in SharedMode over XPC communication. +// This test registers test executable as an Mach service and launches it using launchctl. +// The launched Mach service provides VmnetNetwork serialization to clients upon request, after booting +// a VM using the provided VmnetNetwork to ensure the network is functional on the server side. +// The client boots VM using the provided VmnetNetwork serialization. +// +// This test uses pkg/xpc package to implement XPC communication. +func TestVmnetNetworkShareModeSharingOverXpc(t *testing.T) { + if vz.Available(26.0) { + t.Skip("VmnetSharedMode is supported from macOS 26") + } + + label := "dev.code-hex.vz.test.vmnetsharedmode" + machServiceName := label + ".subnet" + + if server { + t.Log("running as mach service server") + listener, err := xpcServerProvidingSubnet(t, machServiceName) + if err != nil { + log.Printf("failed to create mach service server: %v", err) + t.Fatal(err) + } + if err := listener.Activate(); err != nil { + log.Printf("failed to activate mach service server: %v", err) + t.Fatal(err) + } + ctx, stop := signal.NotifyContext(t.Context(), os.Interrupt, syscall.SIGTERM) + defer stop() + <-ctx.Done() + if err := listener.Close(); err != nil { + log.Printf("failed to close mach service server: %v", err) + } + } else { + t.Log("running as mach service client") + xpcRegisterMachService(t, label, machServiceName) + ipv4Subnet := detectFreeIPv4Subnet(t, netip.MustParsePrefix("192.168.6.0/24")) + network, err := xpcClientRequestingSubnet(t, machServiceName, ipv4Subnet) + if err != nil { + t.Fatal(err) + } + container := newVirtualizationMachine(t, configureNetworkDevice(network, randomMACAddress(t))) + t.Cleanup(func() { + if err := container.Shutdown(); err != nil { + log.Println(err) + } + }) + containerIPv4 := container.DetectIPv4(t, "eth0") + t.Logf("Container IPv4: %s", containerIPv4) + if !ipv4Subnet.Contains(netip.MustParseAddr(containerIPv4)) { + t.Fatalf("expected container IPv4 %s to be within subnet %s", containerIPv4, ipv4Subnet) + } + } +} + +// configureNetworkDevice returns a function that configures a network device +// with the given VmnetNetwork and MAC address. +func configureNetworkDevice(network *vmnet.Network, macAddress *vz.MACAddress) func(cfg *vz.VirtualMachineConfiguration) error { + return func(cfg *vz.VirtualMachineConfiguration) error { + var configurations []*vz.VirtioNetworkDeviceConfiguration + attachment, err := vz.NewVmnetNetworkDeviceAttachment(network.Raw()) + if err != nil { + return err + } + config, err := vz.NewVirtioNetworkDeviceConfiguration(attachment) + if err != nil { + return err + } + config.SetMACAddress(macAddress) + configurations = append(configurations, config) + cfg.SetNetworkDevicesVirtualMachineConfiguration(configurations) + return nil + } +} + +// detectFreeIPv4Subnet detects a free IPv4 subnet on the host machine. +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{} +} + +// funcName returns the name of the calling function. +// It is used to get the test function name for launchctl registration. +func funcName(t *testing.T, skip int) string { + pc, _, _, ok := runtime.Caller(skip) + if !ok { + t.Fatal("failed to get caller info") + } + funcNameComponents := strings.Split(runtime.FuncForPC(pc).Name(), ".") + return funcNameComponents[len(funcNameComponents)-1] +} + +// randomMACAddress generates a random locally administered MAC address. +func randomMACAddress(t *testing.T) *vz.MACAddress { + mac, err := vz.NewRandomLocallyAdministeredMACAddress() + if err != nil { + t.Fatal(err) + } + return mac +} + +// xpcClientRequestingSubnet requests a VmnetNetwork serialization for the given subnet +// from the Mach service and returns the deserialized VmnetNetwork instance. +func xpcClientRequestingSubnet(t *testing.T, machServiceName string, subnet netip.Prefix) (*vmnet.Network, error) { + session, err := xpc.NewSession(machServiceName) + if err != nil { + return nil, err + } + defer session.Cancel() + + resp, err := session.SendDictionaryWithReply( + t.Context(), + xpc.KeyValue("Subnet", xpc.NewString(subnet.String())), + ) + if err != nil { + return nil, err + } + errorStr := resp.GetString("Error") + if errorStr != "" { + return nil, fmt.Errorf("xpc service error: %s", errorStr) + } + serialization := resp.GetValue("Serialization") + log.Printf("%v", serialization) + if serializationDic, ok := serialization.(*xpc.Dictionary); ok { + serializationData := serializationDic.GetData("networkSerialization") + fmt.Printf("serialization data: %q\n", hex.EncodeToString(serializationData)) + } + return vmnet.NewNetworkWithSerialization(serialization.Raw()) +} + +const launchdPlistTemplate = ` + + + + Label + {{.Label}} + ProgramArguments + + {{- range $arg := .ProgramArguments}} + {{$arg}} + {{- end}} + + RunAtLoad + + WorkingDirectory + {{ .WorkingDirectory }} + StandardErrorPath + {{ .WorkingDirectory }}/vmnet_test.xpc_test.stderr.log + + MachServices + + {{- range $service := .MachServices}} + {{$service}} + + {{- end}} + + +` + +// xpcRegisterMachService registers the test executable as a Mach service +// using launchctl with the given label and machServiceName. +// The launched Mach service stderr output will be redirected to the ./vmnet_test.xpc_test.stderr.log file. +func xpcRegisterMachService(t *testing.T, label, machServiceName string) { + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + params := struct { + Label string + ProgramArguments []string + WorkingDirectory string + MachServices []string + }{ + Label: label, + ProgramArguments: []string{os.Args[0], "-test.run", "^" + funcName(t, 2) + "$", "-server"}, + WorkingDirectory: cwd, + MachServices: []string{machServiceName}, + } + template, err := template.New("plist").Parse(launchdPlistTemplate) + if err != nil { + t.Fatal(err) + } + var b bytes.Buffer + if err := template.Execute(&b, params); err != nil { + t.Fatal(err) + } + userHomeDir, err := os.UserHomeDir() + if err != nil { + t.Fatal(err) + } + launchAgentDir := path.Join(userHomeDir, "Library", "LaunchAgents", label+".plist") + if err := os.WriteFile(launchAgentDir, b.Bytes(), 0o644); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.Remove(launchAgentDir); err != nil { + t.Logf("failed to remove launch agent plist: %v", err) + } + }) + cmd := exec.CommandContext(t.Context(), "launchctl", "load", launchAgentDir) + if err := cmd.Run(); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + // do not use t.Context() here to ensure unload runs + cmd := exec.CommandContext(context.Background(), "launchctl", "unload", launchAgentDir) + if err := cmd.Run(); err != nil { + t.Logf("failed to unload launch agent: %v", err) + } + }) +} + +// xpcServerProvidingSubnet creates an Mach service XPC listener +// that provides VmnetNetwork serialization for requested subnet. +// It also boots a VM using the provided VmnetNetwork to ensure the network is functional on the server side. +func xpcServerProvidingSubnet(t *testing.T, machServiceName string) (*xpc.Listener, error) { + return xpc.NewListener( + machServiceName, + xpc.Accept( + xpc.MessageHandler(func(dic *xpc.Dictionary) *xpc.Dictionary { + createErrorReply := func(errMsg string, args ...any) *xpc.Dictionary { + errorString := fmt.Sprintf(errMsg, args...) + log.Print(errorString) + t.Log(errorString) + return dic.CreateReply( + xpc.KeyValue("Error", xpc.NewString(errorString)), + ) + } + var reply *xpc.Dictionary + if subnet := dic.GetString("Subnet"); subnet == "" { + reply = createErrorReply("missing Subnet in request") + } else if config, err := vmnet.NewNetworkConfiguration(vmnet.SharedMode); err != nil { + reply = createErrorReply("failed to create vmnet network configuration: %v", err) + } else if err := config.SetIPv4Subnet(netip.MustParsePrefix(subnet)); err != nil { + reply = createErrorReply("failed to set ipv4 subnet: %v", err) + } else if network, err := vmnet.NewNetwork(config); err != nil { + reply = createErrorReply("failed to create vmnet network: %v", err) + } else if serialization, err := network.CopySerialization(); err != nil { + reply = createErrorReply("failed to copy serialization: %v", err) + } else { + container := newVirtualizationMachine(t, configureNetworkDevice(network, randomMACAddress(t))) + t.Cleanup(func() { + if err := container.Shutdown(); err != nil { + log.Println(err) + } + }) + containerIPv4 := container.DetectIPv4(t, "eth0") + log.Printf("Container IPv4: %s", containerIPv4) + t.Logf("Container IPv4: %s", containerIPv4) + if netip.MustParsePrefix(subnet).Contains(netip.MustParseAddr(containerIPv4)) { + reply = dic.CreateReply( + xpc.KeyValue("Serialization", xpc.NewObject(serialization)), + ) + } else { + reply = createErrorReply("allocated container IPv4 %s is not within requested subnet %s", containerIPv4, subnet) + } + } + return reply + }), + ), + ) +}