From 44c2773cc0569026f4bac788fca4e1677d879e38 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:03:59 +0200 Subject: [PATCH] Add support for host networking with Apple Virtualization --- .../UTMAppleConfigurationNetwork.swift | 48 ++++++++++++++++++- .../macOS/VMConfigAppleNetworkingView.swift | 21 ++++++++ Scripting/UTMScripting.swift | 1 + Scripting/UTMScriptingConfigImpl.swift | 1 + 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/Configuration/UTMAppleConfigurationNetwork.swift b/Configuration/UTMAppleConfigurationNetwork.swift index 5c67d4805..75508b948 100644 --- a/Configuration/UTMAppleConfigurationNetwork.swift +++ b/Configuration/UTMAppleConfigurationNetwork.swift @@ -23,11 +23,13 @@ struct UTMAppleConfigurationNetwork: Codable, Identifiable { enum NetworkMode: String, CaseIterable, QEMUConstant { case shared = "Shared" case bridged = "Bridged" + case host = "Host" var prettyValue: String { switch self { case .shared: return NSLocalizedString("Shared Network", comment: "UTMAppleConfigurationNetwork") case .bridged: return NSLocalizedString("Bridged (Advanced)", comment: "UTMAppleConfigurationNetwork") + case .host: return NSLocalizedString("Host (Advanced)", comment: "UTMAppleConfigurationNetwork") } } } @@ -40,12 +42,16 @@ struct UTMAppleConfigurationNetwork: Codable, Identifiable { /// In bridged mode this is the physical interface to bridge. var bridgeInterface: String? + /// Network UUID to attach to in host mode + var hostNetUuid: String? + let id = UUID() enum CodingKeys: String, CodingKey { case mode = "Mode" case macAddress = "MacAddress" case bridgeInterface = "BridgeInterface" + case hostNetUuid = "HostNetUuid" } init() { @@ -56,6 +62,7 @@ struct UTMAppleConfigurationNetwork: Codable, Identifiable { mode = try values.decode(NetworkMode.self, forKey: .mode) macAddress = try values.decode(String.self, forKey: .macAddress) bridgeInterface = try values.decodeIfPresent(String.self, forKey: .bridgeInterface) + hostNetUuid = try values.decodeIfPresent(UUID.self, forKey: .hostNetUuid)?.uuidString } func encode(to encoder: Encoder) throws { @@ -65,6 +72,9 @@ struct UTMAppleConfigurationNetwork: Codable, Identifiable { if mode == .bridged { try container.encodeIfPresent(bridgeInterface, forKey: .bridgeInterface) } + if mode == .host { + try container.encodeIfPresent(hostNetUuid, forKey: .hostNetUuid) + } } init?(from config: VZNetworkDeviceConfiguration) { @@ -77,9 +87,26 @@ struct UTMAppleConfigurationNetwork: Codable, Identifiable { bridgeInterface = attachment.interface.identifier } else if let _ = virtioConfig.attachment as? VZNATNetworkDeviceAttachment { mode = .shared - } else { - return nil + } else if #available(macOS 26.0, *) { + if let attachment = virtioConfig.attachment as? VZVmnetNetworkDeviceAttachment { + mode = .host + var status: vmnet_return_t = .VMNET_SUCCESS + guard let vmnetConfig = vmnet_network_copy_serialization(attachment.network, &status), status == .VMNET_SUCCESS else { + return nil + } + if let uuidPtr = xpc_dictionary_get_uuid(vmnetConfig, vmnet.vmnet_network_identifier_key) { + let uuidBytes = UnsafeRawPointer(uuidPtr).assumingMemoryBound(to: UInt8.self) + let uuid = UUID(uuid: ( + uuidBytes[0], uuidBytes[1], uuidBytes[2], uuidBytes[3], + uuidBytes[4], uuidBytes[5], uuidBytes[6], uuidBytes[7], + uuidBytes[8], uuidBytes[9], uuidBytes[10], uuidBytes[11], + uuidBytes[12], uuidBytes[13], uuidBytes[14], uuidBytes[15] + )) + hostNetUuid = uuid.uuidString + } + } } + return nil } func vzNetworking() -> VZNetworkDeviceConfiguration? { @@ -109,7 +136,24 @@ struct UTMAppleConfigurationNetwork: Codable, Identifiable { let attachment = VZBridgedNetworkDeviceAttachment(interface: found) config.attachment = attachment } + case .host: + if #available(macOS 26.0, *) { + if let netUuid = hostNetUuid { + let vmnetConfig = xpc_dictionary_create_empty() + xpc_dictionary_set_uint64(vmnetConfig, vmnet.vmnet_operation_mode_key, UInt64(vmnet.vmnet_mode_t.VMNET_HOST_MODE.rawValue)) + xpc_dictionary_set_uuid(vmnetConfig, vmnet.vmnet_network_identifier_key, netUuid) + + var status: vmnet_return_t = .VMNET_SUCCESS + guard let vmnetNetwork = vmnet_network_create_with_serialization(vmnetConfig, &status), status == .VMNET_SUCCESS else { + return nil + } + + let attachment = VZVmnetNetworkDeviceAttachment(network: vmnetNetwork) + config.attachment = attachment + } + } } + return config } } diff --git a/Platform/macOS/VMConfigAppleNetworkingView.swift b/Platform/macOS/VMConfigAppleNetworkingView.swift index 03b8b0366..30fb4ea8d 100644 --- a/Platform/macOS/VMConfigAppleNetworkingView.swift +++ b/Platform/macOS/VMConfigAppleNetworkingView.swift @@ -18,9 +18,15 @@ import SwiftUI import Virtualization struct VMConfigAppleNetworkingView: View { + @AppStorage("HostNetworks") var hostNetworksData: Data = Data() @Binding var config: UTMAppleConfigurationNetwork @EnvironmentObject private var data: UTMData @State private var newMacAddress: String? + @State private var hostNetworks: [UTMConfigurationHostNetwork] = [] + + private func loadData() { + hostNetworks = (try? PropertyListDecoder().decode([UTMConfigurationHostNetwork].self, from: hostNetworksData)) ?? [] + } var body: some View { Form { @@ -50,6 +56,21 @@ struct VMConfigAppleNetworkingView: View { } } } + if #available(macOS 26.0, *) { + if config.mode == .host { + Picker("Host Network", selection: $config.hostNetUuid) { + Text("Default (private)") + .tag(nil as String?) + ForEach(hostNetworks) { interface in + Text(interface.name) + .tag(interface.uuid as String?) + } + }.help("You can configure additional host networks in UTM Settings.") + if config.hostNetUuid != nil { + Text("Note: No DHCP will be provided by UTM") + } + } + } } } diff --git a/Scripting/UTMScripting.swift b/Scripting/UTMScripting.swift index 0121c4a6d..5b72fc8ed 100644 --- a/Scripting/UTMScripting.swift +++ b/Scripting/UTMScripting.swift @@ -142,6 +142,7 @@ import ScriptingBridge @objc public enum UTMScriptingAppleNetworkMode : AEKeyword { case shared = 0x53685264 /* 'ShRd' */ case bridged = 0x42724764 /* 'BrGd' */ + case host = 0x486f5374 /* 'HoSt' */ } // MARK: UTMScriptingModifierKey diff --git a/Scripting/UTMScriptingConfigImpl.swift b/Scripting/UTMScriptingConfigImpl.swift index 5f24f1a50..334252598 100644 --- a/Scripting/UTMScriptingConfigImpl.swift +++ b/Scripting/UTMScriptingConfigImpl.swift @@ -255,6 +255,7 @@ extension UTMScriptingConfigImpl { switch mode { case .shared: return .shared case .bridged: return .bridged + case .host: return .host } }