From 4e20ea8f72d75d1d17229d65c0b27390fc9dd25d Mon Sep 17 00:00:00 2001 From: Nikolay Edigaryev Date: Wed, 14 Sep 2022 17:53:04 +0400 Subject: [PATCH] tart run: introduce --net-bridged (#245) * tart run: introduce --net-bridged * tart.entitlements: add com.apple.vm.networking --- Resources/tart.entitlements | 4 +- Sources/tart/Commands/Run.swift | 54 ++++++++++++++++++++++- Sources/tart/Network/Network.swift | 7 +++ Sources/tart/Network/NetworkBridged.swift | 22 +++++++++ Sources/tart/Network/NetworkShared.swift | 16 +++++++ Sources/tart/{ => Network}/Softnet.swift | 8 +++- Sources/tart/VM.swift | 38 +++++----------- 7 files changed, 119 insertions(+), 30 deletions(-) create mode 100644 Sources/tart/Network/Network.swift create mode 100644 Sources/tart/Network/NetworkBridged.swift create mode 100644 Sources/tart/Network/NetworkShared.swift rename Sources/tart/{ => Network}/Softnet.swift (88%) diff --git a/Resources/tart.entitlements b/Resources/tart.entitlements index f7f5d7c..dccbe21 100644 --- a/Resources/tart.entitlements +++ b/Resources/tart.entitlements @@ -4,5 +4,7 @@ com.apple.security.virtualization + com.apple.vm.networking + - \ No newline at end of file + diff --git a/Sources/tart/Commands/Run.swift b/Sources/tart/Commands/Run.swift index b1d58b1..ce829a7 100644 --- a/Sources/tart/Commands/Run.swift +++ b/Sources/tart/Commands/Run.swift @@ -54,10 +54,21 @@ struct Run: AsyncParsableCommand { """, valueName: "name:path[:ro]")) var dir: [String] = [] + @Option(help: ArgumentHelp(""" + Use bridged networking instead of the default shared (NAT) networking \n(e.g. --net-bridged=en0 or --net-bridged=\"Wi-Fi\") + """, discussion: """ + Specify "list" as an interface name (--net-bridged=list) to list the available bridged interfaces. + """, valueName: "interface name")) + var netBridged: String? + func validate() throws { if vnc && vncExperimental { throw ValidationError("--vnc and --vnc-experimental are mutually exclusive") } + + if withSoftnet && netBridged != nil { + throw ValidationError("--with-softnet and --net-bridged are mutually exclusive") + } } @MainActor @@ -65,7 +76,7 @@ struct Run: AsyncParsableCommand { let vmDir = try VMStorageLocal().open(name) vm = try VM( vmDir: vmDir, - withSoftnet: withSoftnet, + network: userSpecifiedNetwork(vmDir: vmDir) ?? NetworkShared(), additionalDiskAttachments: additionalDiskAttachments(), directoryShares: directoryShares() ) @@ -125,6 +136,47 @@ struct Run: AsyncParsableCommand { } } + func userSpecifiedNetwork(vmDir: VMDirectory) throws -> Network? { + if withSoftnet { + let config = try VMConfig.init(fromURL: vmDir.configURL) + + return try Softnet(vmMACAddress: config.macAddress.string) + } + + if let netBridged = netBridged { + let matchingInterfaces = VZBridgedNetworkInterface.networkInterfaces.filter { interface in + interface.identifier == netBridged || interface.localizedDisplayName == netBridged + } + + if matchingInterfaces.isEmpty { + let available = bridgeInterfaces().joined(separator: ", ") + throw ValidationError("no bridge interfaces matched \"\(netBridged)\", " + + "available interfaces: \(available)") + } + + if matchingInterfaces.count > 1 { + throw ValidationError("more than one bridge interface matched \"\(netBridged)\", " + + "consider refining the search criteria") + } + + return NetworkBridged(interface: matchingInterfaces.first!) + } + + return nil + } + + func bridgeInterfaces() -> [String] { + VZBridgedNetworkInterface.networkInterfaces.map { interface in + var bridgeDescription = interface.identifier + + if let localizedDisplayName = interface.localizedDisplayName { + bridgeDescription += " (or \"\(localizedDisplayName)\")" + } + + return bridgeDescription + } + } + func additionalDiskAttachments() throws -> [VZDiskImageStorageDeviceAttachment] { var result: [VZDiskImageStorageDeviceAttachment] = [] let readOnlySuffix = ":ro" diff --git a/Sources/tart/Network/Network.swift b/Sources/tart/Network/Network.swift new file mode 100644 index 0000000..7149624 --- /dev/null +++ b/Sources/tart/Network/Network.swift @@ -0,0 +1,7 @@ +import Virtualization + +protocol Network { + func attachment() -> VZNetworkDeviceAttachment + func run() throws + func stop() throws +} diff --git a/Sources/tart/Network/NetworkBridged.swift b/Sources/tart/Network/NetworkBridged.swift new file mode 100644 index 0000000..6fe7fb1 --- /dev/null +++ b/Sources/tart/Network/NetworkBridged.swift @@ -0,0 +1,22 @@ +import Foundation +import Virtualization + +class NetworkBridged: Network { + let interface: VZBridgedNetworkInterface + + init(interface: VZBridgedNetworkInterface) { + self.interface = interface + } + + func attachment() -> VZNetworkDeviceAttachment { + VZBridgedNetworkDeviceAttachment(interface: interface) + } + + func run() throws { + // no-op, only used for Softnet + } + + func stop() throws { + // no-op, only used for Softnet + } +} diff --git a/Sources/tart/Network/NetworkShared.swift b/Sources/tart/Network/NetworkShared.swift new file mode 100644 index 0000000..9abacc6 --- /dev/null +++ b/Sources/tart/Network/NetworkShared.swift @@ -0,0 +1,16 @@ +import Foundation +import Virtualization + +class NetworkShared: Network { + func attachment() -> VZNetworkDeviceAttachment { + VZNATNetworkDeviceAttachment() + } + + func run() throws { + // no-op, only used for Softnet + } + + func stop() throws { + // no-op, only used for Softnet + } +} diff --git a/Sources/tart/Softnet.swift b/Sources/tart/Network/Softnet.swift similarity index 88% rename from Sources/tart/Softnet.swift rename to Sources/tart/Network/Softnet.swift index 5f4b5c4..1144688 100644 --- a/Sources/tart/Softnet.swift +++ b/Sources/tart/Network/Softnet.swift @@ -1,10 +1,11 @@ import Foundation +import Virtualization enum SoftnetError: Error { case InitializationFailed(why: String) } -class Softnet { +class Softnet: Network { private let process = Process() let vmFD: Int32 @@ -57,4 +58,9 @@ class Softnet { throw SoftnetError.InitializationFailed(why: "setsockopt(SO_SNDBUF) returned \(ret)") } } + + func attachment() -> VZNetworkDeviceAttachment { + let fh = FileHandle.init(fileDescriptor: vmFD) + return VZFileHandleNetworkDeviceAttachment(fileHandle: fh) + } } diff --git a/Sources/tart/VM.swift b/Sources/tart/VM.swift index ee07b83..5607ca2 100644 --- a/Sources/tart/VM.swift +++ b/Sources/tart/VM.swift @@ -34,10 +34,10 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { // VM's config var config: VMConfig - var softnet: Softnet? = nil + var network: Network init(vmDir: VMDirectory, - withSoftnet: Bool = false, + network: Network = NetworkShared(), additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [], directoryShares: [DirectoryShare] = [] ) throws { @@ -49,13 +49,10 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { } // Initialize the virtual machine and its configuration - if withSoftnet { - softnet = try Softnet(vmMACAddress: config.macAddress.string) - } - + self.network = network let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL, vmConfig: config, - softnet: softnet, additionalDiskAttachments: additionalDiskAttachments, + network: network, additionalDiskAttachments: additionalDiskAttachments, directoryShares: directoryShares) virtualMachine = VZVirtualMachine(configuration: configuration) @@ -114,7 +111,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { vmDir: VMDirectory, ipswURL: URL?, diskSizeGB: UInt16, - withSoftnet: Bool = false, + network: Network = NetworkShared(), additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [] ) async throws { let ipswURL = ipswURL != nil ? ipswURL! : try await VM.retrieveLatestIPSW(); @@ -149,12 +146,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { try config.save(toURL: vmDir.configURL) // Initialize the virtual machine and its configuration - if withSoftnet { - softnet = try Softnet(vmMACAddress: config.macAddress.string) - } - + self.network = network let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL, - vmConfig: config, softnet: softnet, + vmConfig: config, network: network, additionalDiskAttachments: additionalDiskAttachments, directoryShares: []) virtualMachine = VZVirtualMachine(configuration: configuration) @@ -193,9 +187,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { } func run(_ recovery: Bool) async throws { - if let softnet = softnet { - try softnet.run() - } + try network.run() DispatchQueue.main.sync { Task { @@ -225,16 +217,14 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { } } - if let softnet = softnet { - try softnet.stop(); - } + try network.stop() } static func craftConfiguration( diskURL: URL, nvramURL: URL, vmConfig: VMConfig, - softnet: Softnet? = nil, + network: Network = NetworkShared(), additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment], directoryShares: [DirectoryShare] ) throws -> VZVirtualMachineConfiguration { @@ -268,13 +258,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { // Networking let vio = VZVirtioNetworkDeviceConfiguration() - - if let softnet = softnet { - let fh = FileHandle.init(fileDescriptor: softnet.vmFD) - vio.attachment = VZFileHandleNetworkDeviceAttachment(fileHandle: fh) - } else { - vio.attachment = VZNATNetworkDeviceAttachment() - } + vio.attachment = network.attachment() vio.macAddress = vmConfig.macAddress configuration.networkDevices = [vio]