Initial support for external/multiple networks in vmnet host mode
This commit is contained in:
parent
2e54e955e0
commit
f1a95cd5af
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)"
|
||||
|
|
|
@ -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,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")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 */,
|
||||
|
|
Loading…
Reference in New Issue