fix(tart): wire native vmnet port forwarding

This commit is contained in:
Pedro Piñera Buendía 2026-06-16 16:26:57 +02:00
parent 1d0ae4566a
commit cf30410395
3 changed files with 161 additions and 45 deletions

View File

@ -246,11 +246,11 @@ struct Run: AsyncParsableCommand {
#if compiler(>=6.4)
@Flag(help: ArgumentHelp("Use native vmnet-backed networking instead of Softnet for port forwarding",
discussion: """
Adopts the VZVmnetNetworkDeviceAttachment API introduced in macOS 27 (WWDC26).
Adopts the VZVmnetNetworkDeviceAttachment API introduced in macOS 26.
vmnet networks run in-process with no sidecar, so this can replace --net-softnet
for the common "CI VM with a few forwarded ports" case.
Requires the host to be running macOS 27 (or newer).
Requires the host to be running macOS 26 (or newer).
"""))
var netVmnet: Bool = false
@ -363,8 +363,8 @@ struct Run: AsyncParsableCommand {
#if compiler(>=6.4)
if netVmnet {
if #unavailable(macOS 27) {
throw ValidationError("--net-vmnet requires the host to be running macOS 27 (or newer)")
if #unavailable(macOS 26) {
throw ValidationError("--net-vmnet requires the host to be running macOS 26 (or newer)")
}
}
#endif
@ -733,9 +733,10 @@ struct Run: AsyncParsableCommand {
}
#if compiler(>=6.4)
if netVmnet, #available(macOS 27, *) {
if netVmnet, #available(macOS 26, *) {
let config = try VMConfig.init(fromURL: vmDir.configURL)
let portForwardings = try netVmnetExpose.map { try NetworkVmnet.parsePortForwardings($0) } ?? []
return try NetworkVmnet(portForwardings: portForwardings)
return try NetworkVmnet(vmMACAddress: config.macAddress.string, portForwardings: portForwardings)
}
#endif

View File

@ -1,6 +1,5 @@
// Native vmnet-backed networking, adopting the macOS 27 (WWDC26) Virtualization
// API surface introduced in session 224 ("Expand the capabilities of your
// Virtualization app").
// Native vmnet-backed networking, adopting the vmnet logical network API
// surface introduced alongside VZVmnetNetworkDeviceAttachment.
//
// Goal: provide a sidecar-free alternative to the external Softnet process for
// CI-style port forwarding from host TCP/UDP ports to guest ports. The vmnet
@ -8,18 +7,11 @@
// VZVmnetNetworkDeviceAttachment, so the VM keeps using the standard
// VZVirtioNetworkDeviceConfiguration path.
//
// Compile-verification status: this file targets Swift 6.4 (Xcode 27 beta) and
// the macOS 27 SDK headers. The host Swift available when this branch was
// written was 6.3.2, so it has not been compiled. The C entry points used here
// (vmnet_network_configuration_create, vmnet_network_create) are the names
// shown verbatim in WWDC26 session 224. The port-forwarding configuration
// symbol is not stated in the session and is left as a clearly marked FIXME so
// it can be wired up once the final Xcode 27 headers are available.
//
// The whole file is gated behind `#if compiler(>=6.4)` to match how Tart
// already gates VZMacGuestProvisioningOptions in VM.swift (the same situation:
// macOS 27 SDK symbols referenced by a tree that still builds under Xcode 26).
// new SDK symbols referenced by a tree that still builds under Xcode 26).
import Darwin
import Foundation
import Semaphore
import Virtualization
@ -27,7 +19,7 @@ import Virtualization
#if compiler(>=6.4)
import vmnet
@available(macOS 27, *)
@available(macOS 26, *)
class NetworkVmnet: Network {
enum NetworkProtocol: String, CaseIterable {
case tcp
@ -40,19 +32,22 @@ import Virtualization
let internalPort: UInt16
}
private let network: vmnet_network_t
private let portForwardings: [PortForwarding]
private let network: vmnet_network_ref
init(portForwardings: [PortForwarding] = []) throws {
self.portForwardings = portForwardings
init(vmMACAddress: String, portForwardings: [PortForwarding] = []) throws {
let macAddress = try Self.parseMACAddress(vmMACAddress)
let addressing = try Self.addressing(for: macAddress)
var configStatus: vmnet_return_t = .VMNET_FAILURE
guard let configuration = vmnet_network_configuration_create(.VMNET_SHARED_MODE, &configStatus) else {
throw NetworkVmnetError.ConfigurationCreationFailed(status: configStatus)
}
defer { vmnet_network_configuration_release(configuration) }
defer { Self.release(configuration) }
try Self.applyPortForwarding(rules: portForwardings, to: configuration)
if !portForwardings.isEmpty {
try Self.configureIPv4Addressing(addressing, for: macAddress, to: configuration)
try Self.applyPortForwarding(rules: portForwardings, internalAddress: addressing.guestAddress, to: configuration)
}
var networkStatus: vmnet_return_t = .VMNET_FAILURE
guard let network = vmnet_network_create(configuration, &networkStatus) else {
@ -62,7 +57,7 @@ import Virtualization
}
deinit {
vmnet_network_release(network)
Self.release(network)
}
func attachments() -> [VZNetworkDeviceAttachment] {
@ -78,23 +73,65 @@ import Virtualization
// attachment as part of its normal shutdown.
}
// FIXME(macOS 27 SDK): wire up the actual vmnet port-forwarding setter.
//
// WWDC26 session 224 advertises "forward host TCP/UDP ports to specific
// VMs" as part of the new vmnet_network_configuration_t surface but does
// not show the exact C symbol. Once the final Xcode 27 SDK ships, replace
// the body below with the real calls (likely shaped like
// `vmnet_network_configuration_add_port_forwarding_rule(configuration,
// protocol, externalPort, internalPort, &status)`).
//
// For now, refuse to start a VM with port-forwarding rules so the failure
// mode is loud rather than silently dropped traffic.
private static func applyPortForwarding(
rules: [PortForwarding],
to configuration: vmnet_network_configuration_t
internalAddress: in_addr,
to configuration: vmnet_network_configuration_ref
) throws {
guard !rules.isEmpty else { return }
throw NetworkVmnetError.PortForwardingPendingSDKFinalization
for rule in rules {
var internalAddress = internalAddress
let status = withUnsafePointer(to: &internalAddress) {
vmnet_network_configuration_add_port_forwarding_rule(
configuration,
rule.vmnetProtocol,
sa_family_t(AF_INET),
rule.internalPort,
rule.externalPort,
UnsafeRawPointer($0)
)
}
guard status == .VMNET_SUCCESS else {
throw NetworkVmnetError.PortForwardingConfigurationFailed(rule: rule, status: status)
}
}
}
private static func configureIPv4Addressing(
_ addressing: IPv4Addressing,
for macAddress: MACAddress,
to configuration: vmnet_network_configuration_ref
) throws {
var subnet = addressing.subnet
var mask = addressing.mask
let subnetStatus = withUnsafePointer(to: &subnet) { subnetPointer in
withUnsafePointer(to: &mask) { maskPointer in
vmnet_network_configuration_set_ipv4_subnet(configuration, subnetPointer, maskPointer)
}
}
guard subnetStatus == .VMNET_SUCCESS else {
throw NetworkVmnetError.IPv4SubnetConfigurationFailed(status: subnetStatus)
}
var guestAddress = addressing.guestAddress
var etherAddress = ether_addr_t(
octet: (
macAddress.mac[0],
macAddress.mac[1],
macAddress.mac[2],
macAddress.mac[3],
macAddress.mac[4],
macAddress.mac[5]
)
)
let reservationStatus = withUnsafePointer(to: &etherAddress) { macPointer in
withUnsafePointer(to: &guestAddress) { guestAddressPointer in
vmnet_network_configuration_add_dhcp_reservation(configuration, macPointer, guestAddressPointer)
}
}
guard reservationStatus == .VMNET_SUCCESS else {
throw NetworkVmnetError.DHCPReservationConfigurationFailed(status: reservationStatus)
}
}
static func parsePortForwardings(_ spec: String) throws -> [PortForwarding] {
@ -130,13 +167,84 @@ import Virtualization
return PortForwarding(proto: proto, externalPort: external, internalPort: internalPort)
}
private struct IPv4Addressing {
let subnet: in_addr
let mask: in_addr
let guestAddress: in_addr
}
private static func addressing(for macAddress: MACAddress) throws -> IPv4Addressing {
let thirdOctet = subnetThirdOctet(for: macAddress)
return try IPv4Addressing(
subnet: ipv4Address("192.168.\(thirdOctet).0"),
mask: ipv4Address("255.255.255.0"),
guestAddress: ipv4Address("192.168.\(thirdOctet).2")
)
}
private static func subnetThirdOctet(for macAddress: MACAddress) -> UInt8 {
var hash: UInt32 = 2_166_136_261
for byte in macAddress.mac {
hash ^= UInt32(byte)
hash &*= 16_777_619
}
// Keep forwarded VMs on stable per-MAC subnets and avoid bridge100's common default.
var octet = UInt8((hash % 253) + 2)
if octet == 64 {
octet = 65
}
return octet
}
private static func ipv4Address(_ string: String) throws -> in_addr {
var address = in_addr()
guard inet_pton(AF_INET, string, &address) == 1 else {
throw NetworkVmnetError.InvalidIPv4Address(string)
}
return address
}
private static func parseMACAddress(_ string: String) throws -> MACAddress {
guard let macAddress = MACAddress(fromString: string) else {
throw NetworkVmnetError.InvalidMACAddress(string)
}
return macAddress
}
private static func release(_ pointer: OpaquePointer) {
Unmanaged<AnyObject>.fromOpaque(UnsafeRawPointer(pointer)).release()
}
}
@available(macOS 26, *)
extension NetworkVmnet.PortForwarding: CustomStringConvertible {
fileprivate var vmnetProtocol: UInt8 {
switch proto {
case .tcp:
return UInt8(IPPROTO_TCP)
case .udp:
return UInt8(IPPROTO_UDP)
}
}
var description: String {
"\(externalPort):\(internalPort)/\(proto.rawValue)"
}
}
@available(macOS 26, *)
enum NetworkVmnetError: Error, CustomStringConvertible {
case ConfigurationCreationFailed(status: vmnet_return_t)
case NetworkCreationFailed(status: vmnet_return_t)
case IPv4SubnetConfigurationFailed(status: vmnet_return_t)
case DHCPReservationConfigurationFailed(status: vmnet_return_t)
case PortForwardingConfigurationFailed(rule: NetworkVmnet.PortForwarding, status: vmnet_return_t)
case InvalidPortForwardingSpec(spec: String, why: String)
case PortForwardingPendingSDKFinalization
case InvalidIPv4Address(String)
case InvalidMACAddress(String)
var description: String {
switch self {
@ -146,9 +254,16 @@ import Virtualization
return "vmnet_network_create() failed with status \(status)"
case .InvalidPortForwardingSpec(let spec, let why):
return "invalid port forwarding spec \"\(spec)\": \(why)"
case .PortForwardingPendingSDKFinalization:
return "--net-vmnet-expose is not yet wired through to the macOS 27 vmnet port-forwarding API; "
+ "use --net-softnet-expose for now or remove the rule"
case .IPv4SubnetConfigurationFailed(let status):
return "vmnet_network_configuration_set_ipv4_subnet() failed with status \(status)"
case .DHCPReservationConfigurationFailed(let status):
return "vmnet_network_configuration_add_dhcp_reservation() failed with status \(status)"
case .PortForwardingConfigurationFailed(let rule, let status):
return "vmnet_network_configuration_add_port_forwarding_rule(\(rule)) failed with status \(status)"
case .InvalidIPv4Address(let address):
return "invalid IPv4 address \"\(address)\""
case .InvalidMACAddress(let macAddress):
return "invalid MAC address \"\(macAddress)\""
}
}
}

View File

@ -2,7 +2,7 @@ import XCTest
@testable import tart
#if compiler(>=6.4)
@available(macOS 27, *)
@available(macOS 26, *)
final class NetworkVmnetTests: XCTestCase {
func testParsesSingleTCPRule() throws {
let rules = try NetworkVmnet.parsePortForwardings("2222:22")