Files
tailscale/tstest/tailmac/Swift/Common/TailMacConfigHelper.swift
Jonathan Nobels 8fad8c4b9b tstest/tailmac: add customized macOS virtualization tooling ()
updates 

Adds custom macOS vm tooling.  See the README for
the general gist, but this will spin up VMs with unixgram
capable network interfaces listening to a named socket,
and with a virtio socket device for host-guest communication.

We can add other devices like consoles, serial, etc as needed.

The whole things is buildable with a single make command, and
everything is controllable via the command line using the TailMac
utility.

This should all be generally functional but takes a few shortcuts
with error handling and the like.  The virtio socket device support
has not been tested and may require some refinement.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2024-08-19 15:01:19 -04:00

146 lines
5.5 KiB
Swift

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import Foundation
import Virtualization
struct TailMacConfigHelper {
let config: Config
func computeCPUCount() -> Int {
let totalAvailableCPUs = ProcessInfo.processInfo.processorCount
var virtualCPUCount = totalAvailableCPUs <= 1 ? 1 : totalAvailableCPUs - 1
virtualCPUCount = max(virtualCPUCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount)
virtualCPUCount = min(virtualCPUCount, VZVirtualMachineConfiguration.maximumAllowedCPUCount)
return virtualCPUCount
}
func computeMemorySize() -> UInt64 {
// Set the amount of system memory to 4 GB; this is a baseline value
// that you can change depending on your use case.
var memorySize = config.memorySize
memorySize = max(memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize)
memorySize = min(memorySize, VZVirtualMachineConfiguration.maximumAllowedMemorySize)
return memorySize
}
func createBootLoader() -> VZMacOSBootLoader {
return VZMacOSBootLoader()
}
func createGraphicsDeviceConfiguration() -> VZMacGraphicsDeviceConfiguration {
let graphicsConfiguration = VZMacGraphicsDeviceConfiguration()
graphicsConfiguration.displays = [
// The system arbitrarily chooses the resolution of the display to be 1920 x 1200.
VZMacGraphicsDisplayConfiguration(widthInPixels: 1920, heightInPixels: 1200, pixelsPerInch: 80)
]
return graphicsConfiguration
}
func createBlockDeviceConfiguration() -> VZVirtioBlockDeviceConfiguration {
do {
let diskImageAttachment = try VZDiskImageStorageDeviceAttachment(url: config.diskImageURL, readOnly: false)
let disk = VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment)
return disk
} catch {
fatalError("Failed to create Disk image. \(error)")
}
}
func createSocketDeviceConfiguration() -> VZVirtioSocketDeviceConfiguration {
return VZVirtioSocketDeviceConfiguration()
}
func createNetworkDeviceConfiguration() -> VZVirtioNetworkDeviceConfiguration {
let networkDevice = VZVirtioNetworkDeviceConfiguration()
networkDevice.macAddress = VZMACAddress(string: config.ethermac)!
/* Bridged networking requires special entitlements from Apple
if let interface = VZBridgedNetworkInterface.networkInterfaces.first(where: { $0.identifier == "en0" }) {
let networkAttachment = VZBridgedNetworkDeviceAttachment(interface: interface)
networkDevice.attachment = networkAttachment
} else {
print("Assuming en0 for bridged ethernet. Could not findd adapter")
}*/
/// But we can do NAT without Tim Apple's approval
let networkAttachment = VZNATNetworkDeviceAttachment()
networkDevice.attachment = networkAttachment
return networkDevice
}
func createSocketNetworkDeviceConfiguration() -> VZVirtioNetworkDeviceConfiguration {
let networkDevice = VZVirtioNetworkDeviceConfiguration()
networkDevice.macAddress = VZMACAddress(string: config.mac)!
let socket = Darwin.socket(AF_UNIX, SOCK_DGRAM, 0)
// Outbound network packets
let serverSocket = config.serverSocket
// Inbound network packets
let clientSockId = config.vmID
let clientSocket = "/tmp/qemu-dgram-\(clientSockId).sock"
unlink(clientSocket)
var clientAddr = sockaddr_un()
clientAddr.sun_family = sa_family_t(AF_UNIX)
clientSocket.withCString { ptr in
withUnsafeMutablePointer(to: &clientAddr.sun_path.0) { dest in
_ = strcpy(dest, ptr)
}
}
let bindRes = Darwin.bind(socket,
withUnsafePointer(to: &clientAddr, { $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { $0 } }),
socklen_t(MemoryLayout<sockaddr_un>.size))
if bindRes == -1 {
print("Error binding virtual network client socket - \(String(cString: strerror(errno)))")
return networkDevice
}
var serverAddr = sockaddr_un()
serverAddr.sun_family = sa_family_t(AF_UNIX)
serverSocket.withCString { ptr in
withUnsafeMutablePointer(to: &serverAddr.sun_path.0) { dest in
_ = strcpy(dest, ptr)
}
}
let connectRes = Darwin.connect(socket,
withUnsafePointer(to: &serverAddr, { $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { $0 } }),
socklen_t(MemoryLayout<sockaddr_un>.size))
if connectRes == -1 {
print("Error binding virtual network server socket - \(String(cString: strerror(errno)))")
return networkDevice
}
print("Virtual if mac address is \(config.mac)")
print("Client bound to \(clientSocket)")
print("Connected to server at \(serverSocket)")
print("Socket fd is \(socket)")
let handle = FileHandle(fileDescriptor: socket)
let device = VZFileHandleNetworkDeviceAttachment(fileHandle: handle)
networkDevice.attachment = device
return networkDevice
}
func createPointingDeviceConfiguration() -> VZPointingDeviceConfiguration {
return VZMacTrackpadConfiguration()
}
func createKeyboardConfiguration() -> VZKeyboardConfiguration {
return VZMacKeyboardConfiguration()
}
}