From 2a067a53821969db20751bb9c8435812213e7a6f Mon Sep 17 00:00:00 2001 From: f-trycua Date: Fri, 14 Feb 2025 00:51:07 +0100 Subject: [PATCH] Add local network support for VNC --- src/FileSystem/VMConfig.swift | 17 +++++++++- src/VM/VM.swift | 13 +++++++- src/VNC/VNCService.swift | 63 ++++++++++++++++++++++++++++++----- 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/src/FileSystem/VMConfig.swift b/src/FileSystem/VMConfig.swift index 7a7144d0..98d03a33 100644 --- a/src/FileSystem/VMConfig.swift +++ b/src/FileSystem/VMConfig.swift @@ -25,6 +25,7 @@ struct VMConfig: Codable { private var _display: VMDisplayResolution private var _hardwareModel: Data? private var _machineIdentifier: Data? + private var _vncPort: Int? // MARK: - Initialization init( @@ -35,7 +36,8 @@ struct VMConfig: Codable { macAddress: String? = nil, display: String, hardwareModel: Data? = nil, - machineIdentifier: Data? = nil + machineIdentifier: Data? = nil, + vncPort: Int? = nil ) throws { self.os = os self._cpuCount = cpuCount @@ -45,6 +47,7 @@ struct VMConfig: Codable { self._display = VMDisplayResolution(string: display) ?? VMDisplayResolution(string: "1024x768")! self._hardwareModel = hardwareModel self._machineIdentifier = machineIdentifier + self._vncPort = vncPort } var display: VMDisplayResolution { @@ -81,6 +84,11 @@ struct VMConfig: Codable { get { _macAddress } set { _macAddress = newValue } } + + var vncPort: Int? { + get { _vncPort } + set { _vncPort = newValue } + } mutating func setCpuCount(_ count: Int) { _cpuCount = count @@ -110,6 +118,10 @@ struct VMConfig: Codable { self._display = newDisplay } + mutating func setVNCPort(_ port: Int) { + _vncPort = port + } + // MARK: - Codable enum CodingKeys: String, CodingKey { case _cpuCount = "cpuCount" @@ -120,6 +132,7 @@ struct VMConfig: Codable { case _hardwareModel = "hardwareModel" case _machineIdentifier = "machineIdentifier" case os + case _vncPort = "vncPort" } init(from decoder: Decoder) throws { @@ -133,6 +146,7 @@ struct VMConfig: Codable { _display = VMDisplayResolution(string: try container.decode(String.self, forKey: .display))! _hardwareModel = try container.decodeIfPresent(Data.self, forKey: ._hardwareModel) _machineIdentifier = try container.decodeIfPresent(Data.self, forKey: ._machineIdentifier) + _vncPort = try container.decodeIfPresent(Int.self, forKey: ._vncPort) } func encode(to encoder: Encoder) throws { @@ -146,5 +160,6 @@ struct VMConfig: Codable { try container.encode(display.string, forKey: .display) try container.encodeIfPresent(_hardwareModel, forKey: ._hardwareModel) try container.encodeIfPresent(_machineIdentifier, forKey: ._machineIdentifier) + try container.encodeIfPresent(_vncPort, forKey: ._vncPort) } } diff --git a/src/VM/VM.swift b/src/VM/VM.swift index ad0a1e5e..8e73ed3c 100644 --- a/src/VM/VM.swift +++ b/src/VM/VM.swift @@ -342,7 +342,9 @@ class VM { throw VMError.internalError("Virtualization service not initialized") } - try await vncService.start(port: 0, virtualMachine: service.getVirtualMachine()) + // Use configured port or default to 0 (auto-assign) + let port = vmDirContext.config.vncPort ?? 0 + try await vncService.start(port: port, virtualMachine: service.getVirtualMachine()) guard let url = vncService.url else { throw VMError.vncNotConfigured @@ -399,4 +401,13 @@ class VM { func finalize(to name: String, home: Home) throws { try vmDirContext.finalize(to: name) } + + // Add method to set VNC port + func setVNCPort(_ port: Int) throws { + guard !isRunning else { + throw VMError.alreadyRunning(vmDirContext.name) + } + vmDirContext.config.setVNCPort(port) + try vmDirContext.saveConfig() + } } \ No newline at end of file diff --git a/src/VNC/VNCService.swift b/src/VNC/VNCService.swift index 06b70839..8ec3b2b8 100644 --- a/src/VNC/VNCService.swift +++ b/src/VNC/VNCService.swift @@ -30,8 +30,11 @@ final class DefaultVNCService: VNCService { func start(port: Int, virtualMachine: Any?) async throws { let password = Array(PassphraseGenerator().prefix(4)).joined(separator: "-") let securityConfiguration = Dynamic._VZVNCAuthenticationSecurityConfiguration(password: password) + + // Create VNC server with specified port let server = Dynamic._VZVNCServer(port: port, queue: DispatchQueue.main, securityConfiguration: securityConfiguration) + if let vm = virtualMachine as? VZVirtualMachine { server.virtualMachine = vm } @@ -39,15 +42,21 @@ final class DefaultVNCService: VNCService { vncServer = server - // Wait for port to be assigned - while true { - if let port: UInt16 = server.port.asUInt16, port != 0 { - let url = "vnc://:\(password)@127.0.0.1:\(port)" + // Wait for port to be assigned if using port 0 (auto-assign) + while port == 0 { + if let assignedPort: UInt16 = server.port.asUInt16, assignedPort != 0 { + // Get the local IP address for the URL - prefer IPv4 + let hostIP = try getLocalIPAddress() ?? "127.0.0.1" + let url = "vnc://:\(password)@127.0.0.1:\(assignedPort)" // Use localhost for local connections + let externalUrl = "vnc://:\(password)@\(hostIP):\(assignedPort)" // External URL for remote connections - // Save session information - let session = VNCSession( - url: url - ) + Logger.info("VNC server started", metadata: [ + "local": url, + "external": externalUrl + ]) + + // Save session information with local URL for the client + let session = VNCSession(url: url) try vmDirectory.saveSession(session) break } @@ -55,6 +64,44 @@ final class DefaultVNCService: VNCService { } } + // Modified to prefer IPv4 addresses + private func getLocalIPAddress() throws -> String? { + var address: String? + + var ifaddr: UnsafeMutablePointer? + guard getifaddrs(&ifaddr) == 0 else { + return nil + } + defer { freeifaddrs(ifaddr) } + + var ptr = ifaddr + while ptr != nil { + defer { ptr = ptr?.pointee.ifa_next } + + let interface = ptr?.pointee + let family = interface?.ifa_addr.pointee.sa_family + + // Only look for IPv4 addresses + if family == UInt8(AF_INET) { + let name = String(cString: (interface?.ifa_name)!) + if name == "en0" { // Primary interface + var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + getnameinfo(interface?.ifa_addr, + socklen_t((interface?.ifa_addr.pointee.sa_len)!), + &hostname, + socklen_t(hostname.count), + nil, + 0, + NI_NUMERICHOST) + address = String(cString: hostname, encoding: .utf8) + break + } + } + } + + return address + } + func stop() { if let server = vncServer as? Dynamic { server.stop()