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()
This commit is contained in:
Nikolay Edigaryev
2022-06-21 19:17:02 +03:00
committed by GitHub
parent 7efef28250
commit e12f95878e
6 changed files with 149 additions and 34 deletions
+19 -23
View File
@@ -27,10 +27,12 @@ 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
@Flag var withSoftnet: Bool = false
@MainActor
func run() async throws {
let vmDir = try VMStorageLocal().open(name)
vm = try VM(vmDir: vmDir)
vm = try VM(vmDir: vmDir, withSoftnet: withSoftnet)
var vncWrapper: VNCWrapper?
@@ -38,13 +40,16 @@ struct Run: AsyncParsableCommand {
vncWrapper = VNCWrapper(virtualMachine: vm!.virtualMachine)
}
let runTask = Task {
let task = Task {
do {
if let vncWrapper = vncWrapper {
await vncWrapper.open()
}
try await vm!.run(recovery)
// wait for VM to be in a final state before exit
while !(vm?.inFinalState ?? false) {
try await Task.sleep(nanoseconds: 1_000_000)
if let vncWrapper = vncWrapper {
try vncWrapper.stop()
}
Foundation.exit(0)
@@ -58,26 +63,17 @@ struct Run: AsyncParsableCommand {
Foundation.exit(1)
}
}
if let vncWrapper = vncWrapper {
do {
let (port, password) = try await vncWrapper.credentials()
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 get an IP for screen sharing: \(error)")
}
} else if !noGraphics {
runUI()
let sigintSrc = DispatchSource.makeSignalSource(signal: SIGINT)
sigintSrc.setEventHandler {
task.cancel()
}
sigintSrc.activate()
// wait for VM to get into a final state
try await runTask.value
if let vncWrapper = vncWrapper {
try vncWrapper.stop()
if noGraphics || vnc {
dispatchMain()
} else {
runUI()
}
}
+2
View File
@@ -200,6 +200,8 @@ class Registry {
}
for try await part in response.body {
try Task.checkCancellation()
try handler(part)
}
}
-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]
+13 -2
View File
@@ -15,8 +15,19 @@ class VNCWrapper {
vnc.start()
}
func credentials() async throws -> (UInt16, String) {
(try await Self.waitForPort(vnc: vnc), password)
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 {