2
0
Эх сурвалжийг харах

Initial support for external/multiple networks in vmnet host mode

Schamper 1 жил өмнө
parent
commit
f1a95cd5af

+ 124 - 0
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
+    }
+}

+ 3 - 0
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)"

+ 8 - 0
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)
+        }
     }
 }
 

+ 20 - 1
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")

+ 83 - 0
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
 

+ 10 - 0
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 = "<group>"; };
 		037DAA212B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
 		037DAA222B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		03FA9C712B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMConfigurationHostNetwork.swift; sourceTree = "<group>"; };
 		2C33B3A82566C9B100A954A6 /* VMContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMContextMenuModifier.swift; sourceTree = "<group>"; };
 		2C6D9E02256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayQemuTerminalWindowController.swift; sourceTree = "<group>"; };
 		4B224B9C279D4D8100B63CFF /* InListButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InListButtonStyle.swift; sourceTree = "<group>"; };
@@ -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 */,