Skip to content

Commit

Permalink
remote: support NAT mapping of SPICE port
Browse files Browse the repository at this point in the history
  • Loading branch information
osy committed Feb 14, 2024
1 parent 9b52022 commit 8ee00d9
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 147 deletions.
3 changes: 0 additions & 3 deletions Configuration/UTMQemuConfigurationQEMU.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,6 @@ struct UTMQemuConfigurationQEMU: Codable {

/// If true, all SPICE channels will be over TLS. Not saved.
var isSpiceServerTlsEnabled: Bool = false

/// Set to TLS public key for SPICE server in SubjectPublicKey. Not saved.
var spiceServerPublicKey: Data?

/// Set to a password shared with the client. Not saved.
var spiceServerPassword: String?
Expand Down
7 changes: 5 additions & 2 deletions Platform/macOS/UTMDataExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ extension UTMData {
/// - options: Start options
/// - server: Remote server
/// - Returns: Port number to SPICE server
func startRemote(vm: VMData, options: UTMVirtualMachineStartOptions, forClient client: UTMRemoteServer.Remote) async throws -> (port: UInt16, publicKey: Data, password: String) {
func startRemote(vm: VMData, options: UTMVirtualMachineStartOptions, forClient client: UTMRemoteServer.Remote) async throws -> UTMRemoteMessageServer.StartVirtualMachine.ServerInformation {
guard let wrapped = vm.wrapped as? UTMQemuVirtualMachine, type(of: wrapped).capabilities.supportsRemoteSession else {
throw UTMDataError.unsupportedBackend
}
Expand All @@ -94,7 +94,10 @@ extension UTMData {
}
try await wrapped.start(options: options.union(.remoteSession))
vmWindows[vm] = session
return (wrapped.config.qemu.spiceServerPort!, wrapped.config.qemu.spiceServerPublicKey!, wrapped.config.qemu.spiceServerPassword!)
guard let spiceServerInfo = wrapped.spiceServerInfo else {
throw UTMDataError.unsupportedBackend
}
return spiceServerInfo
}

func stop(vm: VMData) {
Expand Down
5 changes: 2 additions & 3 deletions Remote/UTMRemoteClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,8 @@ extension UTMRemoteClient {
return fileUrl
}

func startVirtualMachine(id: UUID, options: UTMVirtualMachineStartOptions) async throws -> (port: UInt16, publicKey: Data, password: String) {
let reply = try await _startVirtualMachine(parameters: .init(id: id, options: options))
return (reply.spiceServerPort, reply.spiceServerPublicKey, reply.spiceServerPassword)
func startVirtualMachine(id: UUID, options: UTMVirtualMachineStartOptions) async throws -> UTMRemoteMessageServer.StartVirtualMachine.ServerInformation {
return try await _startVirtualMachine(parameters: .init(id: id, options: options)).serverInfo
}

func stopVirtualMachine(id: UUID, method: UTMVirtualMachineStopMethod) async throws {
Expand Down
12 changes: 9 additions & 3 deletions Remote/UTMRemoteMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,16 @@ extension UTMRemoteMessageServer {
let options: UTMVirtualMachineStartOptions
}

struct ServerInformation: Serializable, Codable {
let spicePortInternal: UInt16
let spicePortExternal: UInt16?
let spiceHostExternal: String?
let spicePublicKey: Data
let spicePassword: String
}

struct Reply: Serializable, Codable {
let spiceServerPort: UInt16
let spiceServerPublicKey: Data
let spiceServerPassword: String
let serverInfo: ServerInformation
}
}

Expand Down
4 changes: 2 additions & 2 deletions Remote/UTMRemoteServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -712,8 +712,8 @@ extension UTMRemoteServer {

private func _startVirtualMachine(parameters: M.StartVirtualMachine.Request) async throws -> M.StartVirtualMachine.Reply {
let vm = try await findVM(withId: parameters.id)
let (port, publicKey, password) = try await data.startRemote(vm: vm, options: parameters.options, forClient: client)
return .init(spiceServerPort: port, spiceServerPublicKey: publicKey, spiceServerPassword: password)
let serverInfo = try await data.startRemote(vm: vm, options: parameters.options, forClient: client)
return .init(serverInfo: serverInfo)
}

private func _stopVirtualMachine(parameters: M.StopVirtualMachine.Request) async throws -> M.StopVirtualMachine.Reply {
Expand Down
57 changes: 35 additions & 22 deletions Remote/UTMRemoteSpiceVirtualMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,33 @@ extension UTMRemoteSpiceVirtualMachine {
}

extension UTMRemoteSpiceVirtualMachine {
private func connect(_ serverInfo: UTMRemoteMessageServer.StartVirtualMachine.ServerInformation, options: UTMSpiceIOOptions, remoteConnection: Bool) async throws -> UTMSpiceIO {
let ioService = UTMSpiceIO(host: remoteConnection ? serverInfo.spiceHostExternal! : server.host,
tlsPort: Int(remoteConnection ? serverInfo.spicePortExternal! : serverInfo.spicePortInternal),
serverPublicKey: serverInfo.spicePublicKey,
password: serverInfo.spicePassword,
options: options)
ioService.logHandler = { (line: String) -> Void in
guard !line.contains("spice_make_scancode") else {
return // do not log key presses for privacy reasons
}
NSLog("%@", line) // FIXME: log to file
}
try ioService.start()
let coordinator = ConnectCoordinator()
try await withCheckedThrowingContinuation { continuation in
coordinator.continuation = continuation
ioService.connectDelegate = coordinator
do {
try ioService.connect()
} catch {
ioService.connectDelegate = nil
continuation.resume(throwing: error)
}
}
return ioService
}

func start(options: UTMVirtualMachineStartOptions) async throws {
try await _state.operation(before: .stopped, during: .starting, after: .started) {
let spiceServer = try await server.startVirtualMachine(id: id, options: options)
Expand All @@ -170,30 +197,16 @@ extension UTMRemoteSpiceVirtualMachine {
options.insert(.hasDebugLog)
}
#endif
let ioService = UTMSpiceIO(host: server.host,
tlsPort: Int(spiceServer.port),
serverPublicKey: spiceServer.publicKey,
password: spiceServer.password,
options: options)
ioService.logHandler = { (line: String) -> Void in
guard !line.contains("spice_make_scancode") else {
return // do not log key presses for privacy reasons
}
NSLog("%@", line) // FIXME: log to file
}
try ioService.start()
let coordinator = ConnectCoordinator()
try await withCheckedThrowingContinuation { continuation in
coordinator.continuation = continuation
ioService.connectDelegate = coordinator
do {
try ioService.connect()
} catch {
ioService.connectDelegate = nil
continuation.resume(throwing: error)
do {
self.ioService = try await connect(spiceServer, options: options, remoteConnection: false)
} catch {
if spiceServer.spiceHostExternal != nil && spiceServer.spicePortExternal != nil {
// retry with external port
self.ioService = try await connect(spiceServer, options: options, remoteConnection: true)
} else {
throw error
}
}
self.ioService = ioService
}
}

Expand Down
44 changes: 38 additions & 6 deletions Services/UTMQemuVirtualMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

import Foundation
import QEMUKit
#if os(macOS)
import SwiftPortmap
#endif

private var SpiceIoServiceGuestAgentContext = 0
private let kSuspendSnapshotName = "suspend"
Expand Down Expand Up @@ -152,6 +155,11 @@ final class UTMQemuVirtualMachine: UTMSpiceVirtualMachine {

private var changeCursorRequestInProgress: Bool = false

#if WITH_SERVER
private var spicePort: SwiftPortmap.Port?
private(set) var spiceServerInfo: UTMRemoteMessageServer.StartVirtualMachine.ServerInformation?
#endif

@MainActor required init(packageUrl: URL, configuration: UTMQemuConfiguration, isShortcut: Bool = false) throws {
self.isScopedAccess = packageUrl.startAccessingSecurityScopedResource()
// load configuration
Expand Down Expand Up @@ -275,13 +283,21 @@ extension UTMQemuVirtualMachine {
}
let isRunningAsDisposible = options.contains(.bootDisposibleMode)
let isRemoteSession = options.contains(.remoteSession)
let spicePort = isRemoteSession ? try UTMSocketUtils.reservePort() : nil
#if WITH_SERVER
let spicePassword = isRemoteSession ? String.random(length: 32) : nil
let spicePort = isRemoteSession ? try SwiftPortmap.Port.TCP() : nil
#else
if isRemoteSession {
throw UTMVirtualMachineError.notImplemented
}
#endif
await MainActor.run {
config.qemu.isDisposable = isRunningAsDisposible
config.qemu.spiceServerPort = spicePort
config.qemu.isSpiceServerTlsEnabled = true
#if WITH_SERVER
config.qemu.spiceServerPort = spicePort?.internalPort
config.qemu.spiceServerPassword = spicePassword
config.qemu.isSpiceServerTlsEnabled = true
#endif
}

// start TPM
Expand Down Expand Up @@ -331,6 +347,7 @@ extension UTMQemuVirtualMachine {
#endif
let spiceSocketUrl = await config.spiceSocketURL
let interface: any QEMUInterface
let spicePublicKey: Data?
if isRemoteSession {
let pipeInterface = UTMPipeInterface()
await MainActor.run {
Expand All @@ -351,9 +368,7 @@ extension UTMQemuVirtualMachine {
}
try await key[1].write(to: config.spiceTlsKeyUrl)
try await key[2].write(to: config.spiceTlsCertUrl)
await MainActor.run {
config.qemu.spiceServerPublicKey = key[3]
}
spicePublicKey = key[3]
} else {
let ioService = UTMSpiceIO(socketUrl: spiceSocketUrl, options: options)
ioService.logHandler = { [weak system] (line: String) -> Void in
Expand All @@ -364,6 +379,7 @@ extension UTMQemuVirtualMachine {
}
try ioService.start()
interface = ioService
spicePublicKey = nil
}
try Task.checkCancellation()

Expand Down Expand Up @@ -408,6 +424,18 @@ extension UTMQemuVirtualMachine {

// test out snapshots
self.snapshotUnsupportedError = await determineSnapshotSupport()

#if WITH_SERVER
// save server details
if let spicePort = spicePort, let spicePublicKey = spicePublicKey, let spicePassword = spicePassword {
self.spiceServerInfo = .init(spicePortInternal: spicePort.internalPort,
spicePortExternal: try? await spicePort.externalPort,
spiceHostExternal: try? await spicePort.externalIpv4Address,
spicePublicKey: spicePublicKey,
spicePassword: spicePassword)
self.spicePort = spicePort
}
#endif
}

func start(options: UTMVirtualMachineStartOptions = []) async throws {
Expand Down Expand Up @@ -629,6 +657,10 @@ extension UTMQemuVirtualMachine: QEMUVirtualMachineDelegate {
}

func qemuVMDidStop(_ qemuVM: QEMUVirtualMachine) {
#if WITH_SERVER
spicePort = nil
spiceServerInfo = nil
#endif
swtpm?.stop()
swtpm = nil
ioService = nil
Expand Down
96 changes: 0 additions & 96 deletions Services/UTMSocketUtils.swift

This file was deleted.

Loading

0 comments on commit 8ee00d9

Please sign in to comment.