mirror of https://github.com/cirruslabs/tart.git
fix(tart): wire native vmnet port forwarding
This commit is contained in:
parent
1d0ae4566a
commit
cf30410395
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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: ðerAddress) { 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)\""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in New Issue