Files
UTM/Configuration/UTMQemuConfigurationNetwork.swift
osy 4c963f833d home: improved display of network IP addresses
1. Network interfaces are refreshed every 10s rather than manually.
2. Removed host IP for emulated VLAN because it has little use and can
   cause confusion amongst users.
3. Display guest IP for each interface separately.
2025-08-18 17:45:54 -07:00

248 lines
10 KiB
Swift

//
// Copyright © 2022 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
/// Network settings for a single device.
struct UTMQemuConfigurationNetwork: Codable, Identifiable {
/// Operating mode of this adapter
var mode: QEMUNetworkMode = .emulated
/// Hardware model to emulate.
var hardware: any QEMUNetworkDevice = QEMUNetworkDevice_x86_64.e1000
/// Unique MAC address.
var macAddress: String = UTMQemuConfigurationNetwork.randomMacAddress()
/// If true, will attempt to isolate the host in the guest VLAN.
var isIsolateFromHost: Bool = false
/// List of forwarded ports.
var portForward: [UTMQemuConfigurationPortForward] = []
/// In bridged mode this is the physical interface to bridge.
var bridgeInterface: String?
/// Guest IPv4 for emulated VLAN.
var vlanGuestAddress: String?
/// Guest IPv6 for emulated VLAN.
var vlanGuestAddressIPv6: String?
/// Host IPv4 for emulated VLAN.
var vlanHostAddress: String?
/// Host IPv6 for emulated VLAN.
var vlanHostAddressIPv6: String?
/// DHCP start address for emulated VLAN.
var vlanDhcpStartAddress: String?
/// DHCP end address for Apple VLAN
var vlanDhcpEndAddress: String?
/// DHCP domain for emulated VLAN.
var vlanDhcpDomain: String?
/// DNS server for emulated VLAN.
var vlanDnsServerAddress: String?
/// DNS server (IPv6) for emulated VLAN.
var vlanDnsServerAddressIPv6: String?
/// DNS search domain for emulated VLAN.
var vlanDnsSearchDomain: String?
/// Network UUID to attach to in host mode
var hostNetUuid: String?
/// Can be set to be displayed to the user. Not saved.
var currentIpAddresses: [String] = []
let id = UUID()
/// Generate a random MAC address
/// - Returns: A random MAC address
static func randomMacAddress() -> String {
var bytes = (0..<6).map { _ in
arc4random() % 256
}
// byte 0 should be local
bytes[0] = (bytes[0] & 0xFC) | 0x2
let string = bytes.reduce("") { partialResult, byte in
partialResult + String(format: ":%02X", byte)
}
return String(string.dropFirst())
}
enum CodingKeys: String, CodingKey {
case mode = "Mode"
case hardware = "Hardware"
case macAddress = "MacAddress"
case isIsolateFromHost = "IsolateFromHost"
case portForward = "PortForward"
case bridgeInterface = "BridgeInterface"
case vlanGuestAddress = "VlanGuestAddress"
case vlanGuestAddressIPv6 = "VlanGuestAddressIPv6"
case vlanHostAddress = "VlanHostAddress"
case vlanHostAddressIPv6 = "VlanHostAddressIPv6"
case vlanDhcpStartAddress = "VlanDhcpStartAddress"
case vlanDhcpEndAddress = "VlanDhcpEndAddress"
case vlanDhcpDomain = "VlanDhcpDomain"
case vlanDnsServerAddress = "VlanDnsServerAddress"
case vlanDnsServerAddressIPv6 = "VlanDnsServerAddressIPv6"
case vlanDnsSearchDomain = "VlanDnsSearchDomain"
case hostNetUuid = "HostNetUuid"
}
init() {
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
mode = try values.decode(QEMUNetworkMode.self, forKey: .mode)
hardware = try values.decode(AnyQEMUConstant.self, forKey: .hardware)
macAddress = try values.decode(String.self, forKey: .macAddress)
isIsolateFromHost = try values.decode(Bool.self, forKey: .isIsolateFromHost)
portForward = try values.decode([UTMQemuConfigurationPortForward].self, forKey: .portForward)
bridgeInterface = try values.decodeIfPresent(String.self, forKey: .bridgeInterface)
vlanGuestAddress = try values.decodeIfPresent(String.self, forKey: .vlanGuestAddress)
vlanGuestAddressIPv6 = try values.decodeIfPresent(String.self, forKey: .vlanGuestAddressIPv6)
vlanHostAddress = try values.decodeIfPresent(String.self, forKey: .vlanHostAddress)
vlanHostAddressIPv6 = try values.decodeIfPresent(String.self, forKey: .vlanHostAddressIPv6)
vlanDhcpStartAddress = try values.decodeIfPresent(String.self, forKey: .vlanDhcpStartAddress)
vlanDhcpEndAddress = try values.decodeIfPresent(String.self, forKey: .vlanDhcpEndAddress)
vlanDhcpDomain = try values.decodeIfPresent(String.self, forKey: .vlanDhcpDomain)
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 {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(mode, forKey: .mode)
try container.encode(hardware.asAnyQEMUConstant(), forKey: .hardware)
try container.encode(macAddress, forKey: .macAddress)
try container.encode(isIsolateFromHost, forKey: .isIsolateFromHost)
try container.encode(portForward, forKey: .portForward)
if mode == .bridged {
try container.encodeIfPresent(bridgeInterface, forKey: .bridgeInterface)
}
try container.encodeIfPresent(vlanGuestAddress, forKey: .vlanGuestAddress)
try container.encodeIfPresent(vlanGuestAddressIPv6, forKey: .vlanGuestAddressIPv6)
try container.encodeIfPresent(vlanHostAddress, forKey: .vlanHostAddress)
try container.encodeIfPresent(vlanHostAddressIPv6, forKey: .vlanHostAddressIPv6)
try container.encodeIfPresent(vlanDhcpStartAddress, forKey: .vlanDhcpStartAddress)
try container.encodeIfPresent(vlanDhcpEndAddress, forKey: .vlanDhcpEndAddress)
try container.encodeIfPresent(vlanDhcpDomain, forKey: .vlanDhcpDomain)
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)
}
}
}
// MARK: - Default construction
extension UTMQemuConfigurationNetwork {
init?(forArchitecture architecture: QEMUArchitecture, target: any QEMUTarget) {
self.init()
let rawTarget = target.rawValue
if rawTarget.hasPrefix("pc") {
if architecture == .i386 {
hardware = QEMUNetworkDevice_i386.ne2k_isa
} else {
hardware = QEMUNetworkDevice_x86_64.rtl8139
}
} else if rawTarget.hasPrefix("q35") {
hardware = QEMUNetworkDevice_x86_64.e1000
} else if rawTarget == "isapc" {
hardware = QEMUNetworkDevice_x86_64.ne2k_isa
} else if rawTarget.hasPrefix("virt-") || rawTarget == "virt" {
hardware = QEMUNetworkDevice_aarch64.virtio_net_pci
} else if [.ppc, .ppc64].contains(architecture) && rawTarget == QEMUTarget_ppc.mac99.rawValue {
hardware = QEMUNetworkDevice_ppc.sungem
} else if architecture == .m68k && rawTarget == QEMUTarget_m68k.q800.rawValue {
hardware = QEMUNetworkDevice_m68k.dp8393x
} else {
let cards = architecture.networkDeviceType.allRawValues
if let first = cards.first {
hardware = AnyQEMUConstant(rawValue: first)!
} else {
return nil
}
}
#if os(macOS)
mode = .shared
#else
mode = .emulated
#endif
}
}
// MARK: - Conversion of old config format
extension UTMQemuConfigurationNetwork {
init?(migrating oldConfig: UTMLegacyQemuConfiguration) {
self.init()
guard oldConfig.networkEnabled else {
return nil
}
guard let oldMode = convertMode(from: oldConfig.networkMode) else {
return nil
}
mode = oldMode
if let hardwareStr = oldConfig.networkCard {
hardware = AnyQEMUConstant(rawValue: hardwareStr)!
}
if let macString = oldConfig.networkCardMac {
macAddress = macString
}
isIsolateFromHost = oldConfig.networkIsolate
bridgeInterface = oldConfig.networkBridgeInterface
vlanGuestAddress = oldConfig.networkAddress
vlanGuestAddressIPv6 = oldConfig.networkAddressIPv6
vlanHostAddress = oldConfig.networkHost
vlanHostAddressIPv6 = oldConfig.networkHostIPv6
vlanDhcpStartAddress = oldConfig.networkDhcpStart
vlanDhcpDomain = oldConfig.networkDhcpDomain
vlanDnsServerAddress = oldConfig.networkDnsServer
vlanDnsServerAddressIPv6 = oldConfig.networkDnsServerIPv6
vlanDnsSearchDomain = oldConfig.networkDnsSearch
for i in 0..<oldConfig.countPortForwards {
let oldPortForward = oldConfig.portForward(for: i)!
portForward.append(UTMQemuConfigurationPortForward(migrating: oldPortForward))
}
}
private func convertMode(from str: String?) -> QEMUNetworkMode? {
if str == "emulated" {
return .emulated
} else if str == "shared" {
return .shared
} else if str == "host" {
return .host
} else if str == "bridged" {
return .bridged
} else {
return nil
}
}
}