mirror of https://github.com/cirruslabs/tart.git
feat(tart): native vmnet networking via VZVmnetNetworkDeviceAttachment
This commit is contained in:
parent
32d084e9ed
commit
1d0ae4566a
|
|
@ -243,6 +243,29 @@ struct Run: AsyncParsableCommand {
|
|||
@Flag(help: ArgumentHelp("Restrict network access to the host-only network"))
|
||||
var netHost: Bool = false
|
||||
|
||||
#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).
|
||||
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).
|
||||
"""))
|
||||
var netVmnet: Bool = false
|
||||
|
||||
@Option(help: ArgumentHelp("Comma-separated list of ports to forward into the vmnet guest (e.g. --net-vmnet-expose 2222:22,8080:80/tcp,5353:53/udp)",
|
||||
discussion: """
|
||||
Each rule has the form EXTERNAL_PORT:INTERNAL_PORT[/PROTOCOL] where PROTOCOL
|
||||
is tcp (default) or udp. EXTERNAL_PORT is bound on the host's egress interface
|
||||
and forwarded to INTERNAL_PORT on the guest.
|
||||
|
||||
Implies --net-vmnet.
|
||||
""",
|
||||
valueName: "comma-separated port specifications"))
|
||||
var netVmnetExpose: String?
|
||||
#endif
|
||||
|
||||
@Option(help: ArgumentHelp("Set the root disk options (e.g. --root-disk-opts=\"ro\" or --root-disk-opts=\"caching=cached,sync=none\")",
|
||||
discussion: """
|
||||
Options are comma-separated and are as follows:
|
||||
|
|
@ -322,11 +345,30 @@ struct Run: AsyncParsableCommand {
|
|||
if netBridged.count > 0 { netFlags += 1 }
|
||||
if netSoftnet { netFlags += 1 }
|
||||
if netHost { netFlags += 1 }
|
||||
#if compiler(>=6.4)
|
||||
// Automatically enable --net-vmnet when --net-vmnet-expose is specified
|
||||
if netVmnetExpose != nil {
|
||||
netVmnet = true
|
||||
}
|
||||
if netVmnet { netFlags += 1 }
|
||||
#endif
|
||||
|
||||
if netFlags > 1 {
|
||||
throw ValidationError("--net-bridged, --net-softnet and --net-host are mutually exclusive")
|
||||
#if compiler(>=6.4)
|
||||
throw ValidationError("--net-bridged, --net-softnet, --net-host and --net-vmnet are mutually exclusive")
|
||||
#else
|
||||
throw ValidationError("--net-bridged, --net-softnet and --net-host are mutually exclusive")
|
||||
#endif
|
||||
}
|
||||
|
||||
#if compiler(>=6.4)
|
||||
if netVmnet {
|
||||
if #unavailable(macOS 27) {
|
||||
throw ValidationError("--net-vmnet requires the host to be running macOS 27 (or newer)")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if graphics && noGraphics {
|
||||
throw ValidationError("--graphics and --no-graphics are mutually exclusive")
|
||||
}
|
||||
|
|
@ -690,6 +732,13 @@ struct Run: AsyncParsableCommand {
|
|||
return try Softnet(vmMACAddress: config.macAddress.string, extraArguments: ["--vm-net-type", "host"] + softnetExtraArguments)
|
||||
}
|
||||
|
||||
#if compiler(>=6.4)
|
||||
if netVmnet, #available(macOS 27, *) {
|
||||
let portForwardings = try netVmnetExpose.map { try NetworkVmnet.parsePortForwardings($0) } ?? []
|
||||
return try NetworkVmnet(portForwardings: portForwardings)
|
||||
}
|
||||
#endif
|
||||
|
||||
if netBridged.count > 0 {
|
||||
func findBridgedInterface(_ name: String) throws -> VZBridgedNetworkInterface {
|
||||
let interface = VZBridgedNetworkInterface.networkInterfaces.first { interface in
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
// Native vmnet-backed networking, adopting the macOS 27 (WWDC26) Virtualization
|
||||
// API surface introduced in session 224 ("Expand the capabilities of your
|
||||
// Virtualization app").
|
||||
//
|
||||
// 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
|
||||
// configuration is created in-process and handed to
|
||||
// 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).
|
||||
|
||||
import Foundation
|
||||
import Semaphore
|
||||
import Virtualization
|
||||
|
||||
#if compiler(>=6.4)
|
||||
import vmnet
|
||||
|
||||
@available(macOS 27, *)
|
||||
class NetworkVmnet: Network {
|
||||
enum NetworkProtocol: String, CaseIterable {
|
||||
case tcp
|
||||
case udp
|
||||
}
|
||||
|
||||
struct PortForwarding: Equatable {
|
||||
let proto: NetworkProtocol
|
||||
let externalPort: UInt16
|
||||
let internalPort: UInt16
|
||||
}
|
||||
|
||||
private let network: vmnet_network_t
|
||||
private let portForwardings: [PortForwarding]
|
||||
|
||||
init(portForwardings: [PortForwarding] = []) throws {
|
||||
self.portForwardings = portForwardings
|
||||
|
||||
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) }
|
||||
|
||||
try Self.applyPortForwarding(rules: portForwardings, to: configuration)
|
||||
|
||||
var networkStatus: vmnet_return_t = .VMNET_FAILURE
|
||||
guard let network = vmnet_network_create(configuration, &networkStatus) else {
|
||||
throw NetworkVmnetError.NetworkCreationFailed(status: networkStatus)
|
||||
}
|
||||
self.network = network
|
||||
}
|
||||
|
||||
deinit {
|
||||
vmnet_network_release(network)
|
||||
}
|
||||
|
||||
func attachments() -> [VZNetworkDeviceAttachment] {
|
||||
[VZVmnetNetworkDeviceAttachment(network: network)]
|
||||
}
|
||||
|
||||
func run(_ sema: AsyncSemaphore) throws {
|
||||
// vmnet networks run in-process. There is no sidecar to monitor.
|
||||
}
|
||||
|
||||
func stop() async throws {
|
||||
// The network handle is released in deinit; the VM tears down the
|
||||
// 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
|
||||
) throws {
|
||||
guard !rules.isEmpty else { return }
|
||||
throw NetworkVmnetError.PortForwardingPendingSDKFinalization
|
||||
}
|
||||
|
||||
static func parsePortForwardings(_ spec: String) throws -> [PortForwarding] {
|
||||
try spec.split(separator: ",").map { try parseSingle(String($0)) }
|
||||
}
|
||||
|
||||
private static func parseSingle(_ raw: String) throws -> PortForwarding {
|
||||
let (portPart, protoPart): (String, String) = {
|
||||
if let slashIdx = raw.firstIndex(of: "/") {
|
||||
return (String(raw[..<slashIdx]), String(raw[raw.index(after: slashIdx)...]))
|
||||
}
|
||||
return (raw, "tcp")
|
||||
}()
|
||||
|
||||
let ports = portPart.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
|
||||
guard ports.count == 2,
|
||||
let external = UInt16(ports[0]),
|
||||
let internalPort = UInt16(ports[1]),
|
||||
external > 0, internalPort > 0
|
||||
else {
|
||||
throw NetworkVmnetError.InvalidPortForwardingSpec(
|
||||
spec: raw,
|
||||
why: "expected EXTERNAL_PORT:INTERNAL_PORT[/PROTOCOL] with non-zero ports"
|
||||
)
|
||||
}
|
||||
|
||||
guard let proto = NetworkProtocol(rawValue: protoPart.lowercased()) else {
|
||||
throw NetworkVmnetError.InvalidPortForwardingSpec(
|
||||
spec: raw,
|
||||
why: "unknown protocol \"\(protoPart)\", expected tcp or udp"
|
||||
)
|
||||
}
|
||||
|
||||
return PortForwarding(proto: proto, externalPort: external, internalPort: internalPort)
|
||||
}
|
||||
}
|
||||
|
||||
enum NetworkVmnetError: Error, CustomStringConvertible {
|
||||
case ConfigurationCreationFailed(status: vmnet_return_t)
|
||||
case NetworkCreationFailed(status: vmnet_return_t)
|
||||
case InvalidPortForwardingSpec(spec: String, why: String)
|
||||
case PortForwardingPendingSDKFinalization
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .ConfigurationCreationFailed(let status):
|
||||
return "vmnet_network_configuration_create() failed with status \(status)"
|
||||
case .NetworkCreationFailed(let status):
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import XCTest
|
||||
@testable import tart
|
||||
|
||||
#if compiler(>=6.4)
|
||||
@available(macOS 27, *)
|
||||
final class NetworkVmnetTests: XCTestCase {
|
||||
func testParsesSingleTCPRule() throws {
|
||||
let rules = try NetworkVmnet.parsePortForwardings("2222:22")
|
||||
XCTAssertEqual(rules, [
|
||||
NetworkVmnet.PortForwarding(proto: .tcp, externalPort: 2222, internalPort: 22),
|
||||
])
|
||||
}
|
||||
|
||||
func testParsesExplicitProtocols() throws {
|
||||
let rules = try NetworkVmnet.parsePortForwardings("8080:80/tcp,5353:53/udp")
|
||||
XCTAssertEqual(rules, [
|
||||
NetworkVmnet.PortForwarding(proto: .tcp, externalPort: 8080, internalPort: 80),
|
||||
NetworkVmnet.PortForwarding(proto: .udp, externalPort: 5353, internalPort: 53),
|
||||
])
|
||||
}
|
||||
|
||||
func testProtocolIsCaseInsensitive() throws {
|
||||
let rules = try NetworkVmnet.parsePortForwardings("9000:9000/UDP")
|
||||
XCTAssertEqual(rules.first?.proto, .udp)
|
||||
}
|
||||
|
||||
func testRejectsMissingInternalPort() {
|
||||
XCTAssertThrowsError(try NetworkVmnet.parsePortForwardings("2222"))
|
||||
}
|
||||
|
||||
func testRejectsZeroPort() {
|
||||
XCTAssertThrowsError(try NetworkVmnet.parsePortForwardings("0:22"))
|
||||
XCTAssertThrowsError(try NetworkVmnet.parsePortForwardings("2222:0"))
|
||||
}
|
||||
|
||||
func testRejectsUnknownProtocol() {
|
||||
XCTAssertThrowsError(try NetworkVmnet.parsePortForwardings("2222:22/sctp"))
|
||||
}
|
||||
|
||||
func testRejectsOutOfRangePort() {
|
||||
XCTAssertThrowsError(try NetworkVmnet.parsePortForwardings("99999:22"))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Loading…
Reference in New Issue