Compare commits

..
6 Commits
Author SHA1 Message Date
Nikolay Edigaryev c25364b2d4 Homebrew: depend on Softnet package (#136) 2022-06-21 14:55:51 -04:00
Nikolay Edigaryev e12f95878e Softnet: an alternative to built-in NAT with better isolation (#48)
* Softnet: an alternative to built-in NAT with better isolation

* Softnet: increase socketpair(2) socket buffer sizes to 1 MiB

* Pass VM's FD and MAC address to the Softnet

* Softnet: implement graceful shutdown

* Bring back the dispatchMain() and task cancellation

* tart pull: check for cancellation when pulling response body

* Don't dispatchMain() in withTaskCancellationHandler()

* Move VNC URL opening logic into VNCWrapper.open()
2022-06-21 19:17:02 +03:00
Fedor Korotkov 7efef28250 Allow VNC for Recovery mode (#134) 2022-06-20 20:08:31 +03:00
Nikolay Edigaryev 1e90752ded Full-fledged VNC support (#126) 2022-06-20 18:53:17 +03:00
Fedor Korotkov 2020324ef5 Fixed --no-graphics (#132)
Followup to #129 which broke CLI integration because `--no-graphics` option didn't wait for anything.
2022-06-20 10:28:10 -04:00
Nikolay Edigaryev b581db5be4 OCI: make annotations optional (#130)
* OCI: make annotations optional

* Don't initialize annotations by default
2022-06-17 10:53:52 -04:00
10 changed files with 2271 additions and 38 deletions
+2
View File
@@ -36,5 +36,7 @@ brews:
homepage: https://github.com/cirruslabs/tart
description: Run macOS VMs on Apple Silicon
skip_upload: auto
dependencies:
- "cirruslabs/cli/softnet"
custom_block: |
depends_on :macos => :monterey
+31 -27
View File
@@ -27,20 +27,31 @@ struct Run: AsyncParsableCommand {
discussion: "Useful since VNC supports copy/paste, drag and drop, etc.\nNote that Remote Login option should be enabled inside the VM."))
var vnc: Bool = false
@MainActor
func run() async throws {
if recovery && vnc {
print("You can't run in recovery and use VNC!")
Foundation.exit(1)
}
let vmDir = try VMStorageLocal().open(name)
vm = try VM(vmDir: vmDir)
@Flag var withSoftnet: Bool = false
Task {
@MainActor
func run() async throws {
let vmDir = try VMStorageLocal().open(name)
vm = try VM(vmDir: vmDir, withSoftnet: withSoftnet)
var vncWrapper: VNCWrapper?
if vnc {
vncWrapper = VNCWrapper(virtualMachine: vm!.virtualMachine)
}
let task = Task {
do {
if let vncWrapper = vncWrapper {
await vncWrapper.open()
}
try await vm!.run(recovery)
if let vncWrapper = vncWrapper {
try vncWrapper.stop()
}
Foundation.exit(0)
} catch {
if error.localizedDescription.contains("Failed to lock auxiliary storage.") {
@@ -52,23 +63,16 @@ struct Run: AsyncParsableCommand {
Foundation.exit(1)
}
}
if vnc {
do {
print("Waiting for the VM to boot...")
let resolvedIP = try await IP.resolveIP(vm!.config, secondsToWait: 60)
guard let ip = resolvedIP else {
throw IPNotFound()
}
let url = URL(string: "vnc://\(ip)")!
print("Opening \(url)")
NSWorkspace.shared.open(url)
} catch {
print("Failed to get an IP for screen sharing: \(error)")
}
while !(vm?.inFinalState ?? false) {
try await Task.sleep(nanoseconds: 1_000_000)
}
} else if !noGraphics {
let sigintSrc = DispatchSource.makeSignalSource(signal: SIGINT)
sigintSrc.setEventHandler {
task.cancel()
}
sigintSrc.activate()
if noGraphics || vnc {
dispatchMain()
} else {
runUI()
}
}
+2 -2
View File
@@ -11,7 +11,7 @@ struct OCIManifest: Codable, Equatable {
var mediaType: String = ociManifestMediaType
var config: OCIManifestConfig
var layers: [OCIManifestLayer] = Array()
var annotations: Dictionary<String, String> = Dictionary()
var annotations: Dictionary<String, String>?
init(config: OCIManifestConfig, layers: [OCIManifestLayer], uncompressedDiskSize: UInt64? = nil) {
self.config = config
@@ -29,7 +29,7 @@ struct OCIManifest: Codable, Equatable {
}
func uncompressedDiskSize() -> UInt64? {
guard let value = annotations[uncompressedDiskSizeAnnotation] else {
guard let value = annotations?[uncompressedDiskSizeAnnotation] else {
return nil
}
+2
View File
@@ -200,6 +200,8 @@ class Registry {
}
for try await part in response.body {
try Task.checkCancellation()
try handler(part)
}
}
@@ -0,0 +1,13 @@
import Foundation
struct PassphraseGenerator: Sequence {
func makeIterator() -> PassphraseIterator {
PassphraseIterator()
}
}
struct PassphraseIterator: IteratorProtocol {
mutating func next() -> String? {
passphrases[Int(arc4random_uniform(UInt32(passphrases.count)))]
}
}
File diff suppressed because it is too large Load Diff
-1
View File
@@ -28,7 +28,6 @@ struct Root: AsyncParsableCommand {
let sigintSrc = DispatchSource.makeSignalSource(signal: SIGINT)
sigintSrc.setEventHandler {
task.cancel()
Darwin.exit(1)
}
sigintSrc.activate()
+77
View File
@@ -0,0 +1,77 @@
import Foundation
enum SoftnetError: Error {
case InitializationFailed(why: String)
}
class Softnet {
private let process = Process()
let vmFD: Int32
init(vmMACAddress: String) throws {
let binaryName = "softnet"
guard let executableURL = Self.resolveBinaryPath(binaryName) else {
throw SoftnetError.InitializationFailed(why: "\(binaryName) not found in PATH")
}
let fds = UnsafeMutablePointer<Int32>.allocate(capacity: MemoryLayout<Int>.stride * 2)
let ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, fds)
if ret != 0 {
throw SoftnetError.InitializationFailed(why: "socketpair() failed with exit code \(ret)")
}
vmFD = fds[0]
let softnetFD = fds[1]
try setSocketBuffers(vmFD, 1 * 1024 * 1024);
try setSocketBuffers(softnetFD, 1 * 1024 * 1024);
process.executableURL = executableURL
process.arguments = ["--vm-fd", String(STDIN_FILENO), "--vm-mac-address", vmMACAddress]
process.standardInput = FileHandle(fileDescriptor: softnetFD, closeOnDealloc: false)
}
func run() throws {
try process.run()
}
func stop() throws {
process.interrupt()
process.waitUntilExit()
}
private static func resolveBinaryPath(_ name: String) -> URL? {
guard let path = ProcessInfo.processInfo.environment["PATH"] else {
return nil
}
for pathComponent in path.split(separator: ":") {
let url = URL(fileURLWithPath: String(pathComponent))
.appendingPathComponent(name, isDirectory: false)
if FileManager.default.fileExists(atPath: url.path) {
return url
}
}
return nil
}
private func setSocketBuffers(_ fd: Int32, _ sizeBytes: Int) throws {
var option_value = sizeBytes
let option_len = socklen_t(MemoryLayout<Int>.size)
var ret = setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &option_value, option_len)
if ret != 0 {
throw SoftnetError.InitializationFailed(why: "setsockopt(SO_RCVBUF) returned \(ret)")
}
ret = setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &option_value, option_len)
if ret != 0 {
throw SoftnetError.InitializationFailed(why: "setsockopt(SO_SNDBUF) returned \(ret)")
}
}
}
+38 -8
View File
@@ -23,17 +23,24 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
// VM's config
var config: VMConfig
init(vmDir: VMDirectory) throws {
var softnet: Softnet? = nil
init(vmDir: VMDirectory, withSoftnet: Bool = false) throws {
let auxStorage = VZMacAuxiliaryStorage(contentsOf: vmDir.nvramURL)
name = vmDir.name
config = try VMConfig.init(fromURL: vmDir.configURL)
let configuration = try VM.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config)
// Initialize the virtual machine and its configuration
if withSoftnet {
softnet = try Softnet(vmMACAddress: config.macAddress.string)
}
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config,
softnet: softnet)
virtualMachine = VZVirtualMachine(configuration: configuration)
super.init()
virtualMachine.delegate = self
}
@@ -87,7 +94,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
}
init(vmDir: VMDirectory, ipswURL: URL?, diskSizeGB: UInt16) async throws {
init(vmDir: VMDirectory, ipswURL: URL?, diskSizeGB: UInt16, withSoftnet: Bool = false) async throws {
let ipswURL = ipswURL != nil ? ipswURL! : try await VM.retrieveLatestIPSW();
// Load the restore image and try to get the requirements
@@ -120,11 +127,15 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
try config.save(toURL: vmDir.configURL)
// Initialize the virtual machine and its configuration
let configuration = try VM.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config)
if withSoftnet {
softnet = try Softnet(vmMACAddress: config.macAddress.string)
}
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config,
softnet: softnet)
virtualMachine = VZVirtualMachine(configuration: configuration)
super.init()
virtualMachine.delegate = self
// Run automated installation
@@ -143,6 +154,10 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
func run(_ recovery: Bool) async throws {
if let softnet = softnet {
try softnet.run()
}
try await virtualMachine.start(recovery)
await withTaskCancellationHandler(operation: {
@@ -158,9 +173,18 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
}
}
if let softnet = softnet {
try softnet.stop();
}
}
static func craftConfiguration(diskURL: URL, auxStorage: VZMacAuxiliaryStorage, vmConfig: VMConfig) throws -> VZVirtualMachineConfiguration {
static func craftConfiguration(
diskURL: URL,
auxStorage: VZMacAuxiliaryStorage,
vmConfig: VMConfig,
softnet: Softnet? = nil
) throws -> VZVirtualMachineConfiguration {
let configuration = VZVirtualMachineConfiguration()
// Boot loader
@@ -212,7 +236,13 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
// Networking
let vio = VZVirtioNetworkDeviceConfiguration()
vio.attachment = VZNATNetworkDeviceAttachment()
if let softnet = softnet {
let fh = FileHandle.init(fileDescriptor: softnet.vmFD)
vio.attachment = VZFileHandleNetworkDeviceAttachment(fileHandle: fh)
} else {
vio.attachment = VZNATNetworkDeviceAttachment()
}
vio.macAddress = vmConfig.macAddress
configuration.networkDevices = [vio]
+53
View File
@@ -0,0 +1,53 @@
import Foundation
import Dynamic
import Virtualization
class VNCWrapper {
private let password: String
private let vnc: Dynamic
init(virtualMachine: VZVirtualMachine) {
password = Array(PassphraseGenerator().prefix(4)).joined(separator: "-")
let securityConfiguration = Dynamic._VZVNCAuthenticationSecurityConfiguration(password: password)
vnc = Dynamic._VZVNCServer(port: 0, queue: DispatchQueue.global(),
securityConfiguration: securityConfiguration)
vnc.virtualMachine = virtualMachine
vnc.start()
}
func open() async {
do {
let port = try await Self.waitForPort(vnc: vnc)
let url = URL(string: "vnc://:\(password)@127.0.0.1:\(port)")!
print("Opening \(url)...")
if ProcessInfo.processInfo.environment["CI"] == nil {
NSWorkspace.shared.open(url)
}
} catch {
print("Failed to retrieve a VNC server port: \(error)")
}
}
func stop() throws {
vnc.stop()
}
deinit {
try? stop()
}
private static func waitForPort(vnc: Dynamic) async throws -> UInt16 {
while true {
// Port is 0 shortly after start(),
// but will be initialized later
if let port = vnc.port.asUInt16, port != 0 {
return port
}
// Wait 50 ms.
try await Task.sleep(nanoseconds: 50_000_000)
}
}
}