Files
UTM/Configuration/UTMAppleConfigurationNetwork.swift
T
osy 47eadda150 apple: make the vmnet modes predictable and simpler
Follow-up to the vmnet networking modes:

- Rename "Shared Network (VM to VM)" to "NAT Network" (config value
  "NATNetwork", scripting term "nat network") so the picker does not offer
  two "Shared" modes that differ only by a suffix.
- Request a fixed subnet per mode outside of the ranges vmnet hands out by
  default. Those ranges are also used by NAT attachments and QEMU, so guest
  addresses depended on what else was running whenever the network was
  (re)created. Fall back to a vmnet chosen subnet when ours is taken.
- Show the mode description with DetailedSection. The bare Text row widened
  the form and clipped the labels and controls in the settings window.
- Only require macOS 26 when a network is actually created, so a VM using
  one of these modes can still be edited and saved on older hosts.
- Do not apply locale digit grouping to the vmnet error code.
- Fold the validation flag into the existing one that skipped drives and
  leave the device unattached instead of using a NAT placeholder, remove
  the unused reverse mapping, key the networks by vmnet_mode_t, and isolate
  the manager to the main actor in place of a lock with no concurrent users.

Use .1 as the host-side address within each preferred /24. As reproduced
in PR #7865, vmnet accepts .0 during network creation but the interface
fails later at startup.

Assisted-by: Claude:claude-fable-5-1
Assisted-by: ChatGPT:GPT-6-Astra-Pro
2026-09-18 20:43:49 -04:00

154 lines
5.6 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
import Virtualization
@available(iOS, unavailable, message: "Apple Virtualization not available on iOS")
struct UTMAppleConfigurationNetwork: Codable, Identifiable {
enum NetworkMode: String, CaseIterable, QEMUConstant {
case shared = "Shared"
case natNetwork = "NATNetwork"
case host = "Host"
case bridged = "Bridged"
var prettyValue: String {
switch self {
case .shared: return NSLocalizedString("Shared Network", comment: "UTMAppleConfigurationNetwork")
case .natNetwork: return NSLocalizedString("NAT Network", comment: "UTMAppleConfigurationNetwork")
case .host: return NSLocalizedString("Host Only", comment: "UTMAppleConfigurationNetwork")
case .bridged: return NSLocalizedString("Bridged (Advanced)", comment: "UTMAppleConfigurationNetwork")
}
}
var isHidden: Bool {
switch self {
case .natNetwork, .host:
if #available(macOS 26, *) {
return false
} else {
return true
}
default:
return false
}
}
}
var mode: NetworkMode = .shared
/// Unique MAC address.
var macAddress: String = VZMACAddress.randomLocallyAdministered().string
/// In bridged mode this is the physical interface to bridge.
var bridgeInterface: String?
let id = UUID()
enum CodingKeys: String, CodingKey {
case mode = "Mode"
case macAddress = "MacAddress"
case bridgeInterface = "BridgeInterface"
}
init() {
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
mode = try values.decode(NetworkMode.self, forKey: .mode)
macAddress = try values.decode(String.self, forKey: .macAddress)
bridgeInterface = try values.decodeIfPresent(String.self, forKey: .bridgeInterface)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(mode, forKey: .mode)
try container.encode(macAddress, forKey: .macAddress)
if mode == .bridged {
try container.encodeIfPresent(bridgeInterface, forKey: .bridgeInterface)
}
}
init?(from config: VZNetworkDeviceConfiguration) {
guard let virtioConfig = config as? VZVirtioNetworkDeviceConfiguration else {
return nil
}
macAddress = virtioConfig.macAddress.string
if let attachment = virtioConfig.attachment as? VZBridgedNetworkDeviceAttachment {
mode = .bridged
bridgeInterface = attachment.interface.identifier
} else if let _ = virtioConfig.attachment as? VZNATNetworkDeviceAttachment {
mode = .shared
} else {
return nil
}
}
@MainActor func vzNetworking(forValidation: Bool) throws -> VZNetworkDeviceConfiguration? {
let config = VZVirtioNetworkDeviceConfiguration()
guard let macAddress = VZMACAddress(string: macAddress) else {
return nil
}
config.macAddress = macAddress
switch mode {
case .shared:
let attachment = VZNATNetworkDeviceAttachment()
config.attachment = attachment
case .bridged:
var found: VZBridgedNetworkInterface?
if let bridgeInterface = bridgeInterface {
for interface in VZBridgedNetworkInterface.networkInterfaces {
if interface.identifier == bridgeInterface {
found = interface
break
}
}
} else {
// default to first interface if unspecified
found = VZBridgedNetworkInterface.networkInterfaces.first
}
if let found = found {
let attachment = VZBridgedNetworkDeviceAttachment(interface: found)
config.attachment = attachment
}
case .natNetwork, .host:
// a live network is only created when starting a VM, validation leaves the device unattached
if !forValidation {
guard #available(macOS 26, *) else {
throw UTMAppleConfigurationError.featureNotSupported
}
config.attachment = try UTMAppleVmnetNetworkManager.shared.attachment(for: mode == .host ? .VMNET_HOST_MODE : .VMNET_SHARED_MODE)
}
}
return config
}
}
// MARK: - Conversion of old config format
@available(iOS, unavailable, message: "Apple Virtualization not available on iOS")
extension UTMAppleConfigurationNetwork {
init(migrating oldNetwork: Network) {
switch oldNetwork.networkMode {
case .Bridged: mode = .bridged
case .Shared: mode = .shared
}
macAddress = oldNetwork.macAddress
bridgeInterface = oldNetwork.bridgeInterfaceIdentifier
}
}