diff --git a/Configuration/UTMConfigurationHostNetwork.swift b/Configuration/UTMConfigurationHostNetwork.swift new file mode 100644 index 000000000..ec70f0d0c --- /dev/null +++ b/Configuration/UTMConfigurationHostNetwork.swift @@ -0,0 +1,124 @@ +// +// Copyright © 2024 osy. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Host network settings. +struct UTMConfigurationHostNetwork: Codable, Identifiable { + /// Network name + var name: String + + /// Network UUID + var uuid: String = UUID().uuidString + + let id = UUID() + + enum CodingKeys: String, CodingKey { + case name = "Name" + case uuid = "Uuid" + } + + init() { + self.name = uuid + } + + init(name: String) { + self.name = name + } + + init(name: String, uuid: String) { + self.name = name + self.uuid = uuid + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + uuid = try values.decodeIfPresent(UUID.self, forKey: .uuid)?.uuidString ?? UUID().uuidString + name = try values.decodeIfPresent(String.self, forKey: .name) ?? uuid + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encode(uuid, forKey: .uuid) + } + + static func parseVMware(from url: URL) -> [UTMConfigurationHostNetwork] { + let accessing = url.startAccessingSecurityScopedResource() + if !accessing { return [] } + defer { + if accessing { + url.stopAccessingSecurityScopedResource() + } + } + + var currentId: String?; + var currentName: String?; + var currentUuid: String?; + var result: [UTMConfigurationHostNetwork] = [] + + if let content = try? String(contentsOf: url) { + for line in content.split(whereSeparator: \.isNewline) { + let parts = line.split(separator: " ") + if parts.count != 3 || (parts[0] != "answer" && !parts[1].starts(with: "VNET_")) { + continue + } + + let name_parts = parts[1].split(separator: "_", maxSplits: 2) + if name_parts.count != 3 { + continue + } + + if currentId == nil { + currentId = String(name_parts[1]) + } + + if let id = currentId { + if id != name_parts[1] { + if let uuid = currentUuid { + result.append(UTMConfigurationHostNetwork(name: currentName ?? "VMware vmnet\(id)", uuid: uuid)) + } + + currentId = String(name_parts[1]) + currentName = nil + currentUuid = nil + } + + if name_parts[2] == "DISPLAY_NAME" { + currentName = String(parts[2]) + } + + if name_parts[2] == "HOSTONLY_UUID" { + currentUuid = String(parts[2]) + } + } + } + + if let id = currentId, let uuid = currentUuid { + var newNetwork = UTMConfigurationHostNetwork() + newNetwork.name = if let name = currentName { + name + } else { + "VMware vmnet\(id)" + } + newNetwork.uuid = uuid + result.append(newNetwork) + } + } + + return result + } +} diff --git a/Configuration/UTMQemuConfiguration+Arguments.swift b/Configuration/UTMQemuConfiguration+Arguments.swift index 201b9cd6f..35d87853f 100644 --- a/Configuration/UTMQemuConfiguration+Arguments.swift +++ b/Configuration/UTMQemuConfiguration+Arguments.swift @@ -861,6 +861,9 @@ import Virtualization // for getting network interfaces useVMnet = true "vmnet-host" "id=net\(i)" + if let netUuid = networks[i].hostNetUuid { + "net-uuid=\(netUuid)" + } } else { "user" "id=net\(i)" diff --git a/Configuration/UTMQemuConfigurationNetwork.swift b/Configuration/UTMQemuConfigurationNetwork.swift index 8e11ac56d..c2913ac5b 100644 --- a/Configuration/UTMQemuConfigurationNetwork.swift +++ b/Configuration/UTMQemuConfigurationNetwork.swift @@ -66,6 +66,9 @@ struct UTMQemuConfigurationNetwork: Codable, Identifiable { /// DNS search domain for emulated VLAN. var vlanDnsSearchDomain: String? + /// Network UUID to attach to in host mode + var hostNetUuid: String? + let id = UUID() /// Generate a random MAC address @@ -99,6 +102,7 @@ struct UTMQemuConfigurationNetwork: Codable, Identifiable { case vlanDnsServerAddress = "VlanDnsServerAddress" case vlanDnsServerAddressIPv6 = "VlanDnsServerAddressIPv6" case vlanDnsSearchDomain = "VlanDnsSearchDomain" + case hostNetUuid = "HostNetUuid" } init() { @@ -122,6 +126,7 @@ struct UTMQemuConfigurationNetwork: Codable, Identifiable { vlanDnsServerAddress = try values.decodeIfPresent(String.self, forKey: .vlanDnsServerAddress) vlanDnsServerAddressIPv6 = try values.decodeIfPresent(String.self, forKey: .vlanDnsServerAddressIPv6) vlanDnsSearchDomain = try values.decodeIfPresent(String.self, forKey: .vlanDnsSearchDomain) + hostNetUuid = try values.decodeIfPresent(UUID.self, forKey: .hostNetUuid)?.uuidString } func encode(to encoder: Encoder) throws { @@ -144,6 +149,9 @@ struct UTMQemuConfigurationNetwork: Codable, Identifiable { try container.encodeIfPresent(vlanDnsServerAddress, forKey: .vlanDnsServerAddress) try container.encodeIfPresent(vlanDnsServerAddressIPv6, forKey: .vlanDnsServerAddressIPv6) try container.encodeIfPresent(vlanDnsSearchDomain, forKey: .vlanDnsSearchDomain) + if mode == .host { + try container.encodeIfPresent(hostNetUuid, forKey: .hostNetUuid) + } } } diff --git a/Platform/Shared/VMConfigNetworkView.swift b/Platform/Shared/VMConfigNetworkView.swift index 516931fd4..0e95e7716 100644 --- a/Platform/Shared/VMConfigNetworkView.swift +++ b/Platform/Shared/VMConfigNetworkView.swift @@ -20,10 +20,16 @@ import Virtualization #endif struct VMConfigNetworkView: View { + @AppStorage("HostNetworks") var hostNetworksData: Data = Data() @Binding var config: UTMQemuConfigurationNetwork @Binding var system: UTMQemuConfigurationSystem + @State private var hostNetworks: [UTMConfigurationHostNetwork] = [] @State private var showAdvanced: Bool = false + private func loadData() { + hostNetworks = (try? PropertyListDecoder().decode([UTMConfigurationHostNetwork].self, from: hostNetworksData)) ?? [] + } + var body: some View { VStack { Form { @@ -40,9 +46,22 @@ struct VMConfigNetworkView: View { } } } + 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?) + } + } + if config.hostNetUuid != nil { + Text("Note: No DHCP will be provided by UTM") + } + } #endif VMConfigConstantPicker("Emulated Network Card", selection: $config.hardware, type: system.architecture.networkDeviceType) - } + }.onAppear(perform: loadData) HStack { DefaultTextField("MAC Address", text: $config.macAddress, prompt: "00:00:00:00:00:00") diff --git a/Platform/macOS/SettingsView.swift b/Platform/macOS/SettingsView.swift index 4485a773f..55745a855 100644 --- a/Platform/macOS/SettingsView.swift +++ b/Platform/macOS/SettingsView.swift @@ -37,6 +37,12 @@ struct SettingsView: View { .tabItem { Label("Input", systemImage: "keyboard") } + if #available(macOS 12, *) { + NetworkSettingsView().padding() + .tabItem { + Label("Network", systemImage: "network") + } + } ServerSettingsView().padding() .tabItem { Label("Server", systemImage: "server.rack") @@ -185,6 +191,83 @@ struct InputSettingsView: View { } } +@available(macOS 12, *) +struct NetworkSettingsView: View { + @AppStorage("HostNetworks") var hostNetworksData: Data = Data() + @State private var hostNetworks: [UTMConfigurationHostNetwork] = [] + @State private var selectedID: UUID? + @State private var isImporterPresented: Bool = false + + private func loadData() { + hostNetworks = (try? PropertyListDecoder().decode([UTMConfigurationHostNetwork].self, from: hostNetworksData)) ?? [] + } + + private func saveData() { + hostNetworksData = (try? PropertyListEncoder().encode(hostNetworks)) ?? Data() + } + + var body: some View { + Form { + Section(header: Text("Host networks")) { + Table($hostNetworks, selection: $selectedID) { + TableColumn("Name") { $network in + TextField( + "Name", + text: $network.name + ) + .labelsHidden() + } + TableColumn("UUID") { $network in + TextField( + "UUID", + text: $network.uuid, + onEditingChanged: { (editingChanged) in + if !editingChanged && UUID(uuidString: network.uuid) != nil { + saveData() + } + } + ) + .labelsHidden() + .autocorrectionDisabled() + .foregroundStyle(UUID(uuidString: network.uuid) == nil ? .red : .primary) + } + .width(min: 160) + } + HStack { + Button("Import from VMware Fusion") { + isImporterPresented.toggle() + }.fileImporter(isPresented: $isImporterPresented, allowedContentTypes: [.data]) { result in + + if let url = try? result.get() { + for network in UTMConfigurationHostNetwork.parseVMware(from: url) { + if !hostNetworks.contains(where: {$0.uuid == network.uuid}) { + hostNetworks.append(network) + } + } + + saveData() + } + }.help("Navigate to `/Library/Preferences/VMware Fusion` (⌘+Shift+G) and select the `networking` file") + Spacer() + Button("Delete") { + hostNetworks.removeAll { network in + network.id == selectedID + } + selectedID = nil + saveData() + + }.disabled(selectedID == nil) + Button("Add") { + let network = UTMConfigurationHostNetwork(name: "Network \(hostNetworks.count)") + hostNetworks.append(network) + saveData() + } + } + } + }.onAppear(perform: loadData) + } +} + struct ServerSettingsView: View { private let defaultPort = 21589 diff --git a/UTM.xcodeproj/project.pbxproj b/UTM.xcodeproj/project.pbxproj index b28e0edf5..189ace857 100644 --- a/UTM.xcodeproj/project.pbxproj +++ b/UTM.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 03FA9C722B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA9C712B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift */; }; + 03FA9C732B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA9C712B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift */; }; + 03FA9C742B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA9C712B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift */; }; + 03FA9C752B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA9C712B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift */; }; 2C33B3A92566C9B100A954A6 /* VMContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C33B3A82566C9B100A954A6 /* VMContextMenuModifier.swift */; }; 2C33B3AA2566C9B100A954A6 /* VMContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C33B3A82566C9B100A954A6 /* VMContextMenuModifier.swift */; }; 2C6D9E03256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6D9E02256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift */; }; @@ -1579,6 +1583,7 @@ 037DAA202B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = ""; }; 037DAA212B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; 037DAA222B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; + 03FA9C712B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMConfigurationHostNetwork.swift; sourceTree = ""; }; 2C33B3A82566C9B100A954A6 /* VMContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMContextMenuModifier.swift; sourceTree = ""; }; 2C6D9E02256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayQemuTerminalWindowController.swift; sourceTree = ""; }; 4B224B9C279D4D8100B63CFF /* InListButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InListButtonStyle.swift; sourceTree = ""; }; @@ -2699,6 +2704,7 @@ 841619A9284315F9000034B2 /* UTMConfigurationInfo.swift */, 843BF83728451B380029D60D /* UTMConfigurationTerminal.swift */, 848D99BB28636AC90055C215 /* UTMConfigurationDrive.swift */, + 03FA9C712B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift */, 848A98AF286A0F74006F0550 /* UTMAppleConfiguration.swift */, 848A98BF286A20E3006F0550 /* UTMAppleConfigurationBoot.swift */, 848A98B1286A0FDE006F0550 /* UTMAppleConfigurationSystem.swift */, @@ -3516,6 +3522,7 @@ CE2D92AA24AD46670059923A /* UTMSpiceIO.m in Sources */, 84909A9127CADAE0005605F1 /* UTMUnavailableVMView.swift in Sources */, CE2D958524AD4F990059923A /* VMDrivesSettingsView.swift in Sources */, + 03FA9C722B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift in Sources */, 848D99BC28636AC90055C215 /* UTMConfigurationDrive.swift in Sources */, CED814E924C79F070042F0F1 /* VMConfigDriveCreateView.swift in Sources */, 842B9F8D28CC58B700031EE7 /* UTMPatches.swift in Sources */, @@ -3797,6 +3804,7 @@ 847BF9AC2A49C783000BD9AA /* VMData.swift in Sources */, CE25124729BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift in Sources */, CE2D958824AD4F990059923A /* VMConfigPortForwardForm.swift in Sources */, + 03FA9C752B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift in Sources */, 845F170D289CB3DE00944904 /* VMDisplayTerminal.swift in Sources */, 84C4D9042880CA8A00EC3B2B /* VMSettingsAddDeviceMenuView.swift in Sources */, CEBE820526A4C1B5007AAB12 /* VMWizardDrivesView.swift in Sources */, @@ -3883,6 +3891,7 @@ 8401865F2887B1620050AC51 /* VMDisplayTerminalViewController.swift in Sources */, CEA45E8F263519B5002FA97D /* VMContextMenuModifier.swift in Sources */, 85EC516527CC8D0F004A51DE /* VMConfigAdvancedNetworkView.swift in Sources */, + 03FA9C732B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift in Sources */, CEA45E91263519B5002FA97D /* VMDisplayMetalViewController+Pencil.m in Sources */, CEA45E94263519B5002FA97D /* UTMLegacyQemuConfiguration+Drives.m in Sources */, 848A98C5286F332D006F0550 /* UTMConfiguration.swift in Sources */, @@ -4069,6 +4078,7 @@ CEF7F5E72AEEDCC400E34952 /* UTMRegistry.swift in Sources */, CEF7F5E82AEEDCC400E34952 /* VMDisplayViewControllerDelegate.swift in Sources */, CEF7F5EA2AEEDCC400E34952 /* VMConfigConstantPicker.swift in Sources */, + 03FA9C742B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift in Sources */, CEF7F5EC2AEEDCC400E34952 /* VMToolbarModifier.swift in Sources */, CEF7F5ED2AEEDCC400E34952 /* VMCursor.m in Sources */, CEF7F5EE2AEEDCC400E34952 /* VMConfigDriveDetailsView.swift in Sources */,