Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions Coder-Desktop/Coder-DesktopHelper/Manager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ actor Manager {
Task { try? await appXPCServerDelegate.onPeerUpdate(update: msg.peerUpdate) }
case let .log(logMsg):
writeVpnLog(logMsg)
case .networkSettings, .start, .stop:
case .networkSettings, .start, .stop, .wake:
logger.critical("received unexpected message: `\(String(describing: msgType))`")
}
}
Expand All @@ -182,7 +182,7 @@ actor Manager {
}
})
}
case .log, .peerUpdate, .start, .stop:
case .log, .peerUpdate, .start, .stop, .wake:
logger.critical("received unexpected rpc: `\(String(describing: msgType))`")
}
}
Expand Down Expand Up @@ -222,6 +222,16 @@ actor Manager {
logger.info("startVPN done")
}

// Notifies the tunnel that the system has woken from sleep.
func wake() async {
logger.info("sending wake rpc")
do {
try await speaker.wake()
} catch {
logger.error("wake rpc failed: \(error.localizedDescription, privacy: .public)")
}
}

func stopVPN() async throws(ManagerError) {
logger.info("sending stop rpc")
let resp: Vpn_TunnelMessage
Expand Down
54 changes: 54 additions & 0 deletions Coder-Desktop/Coder-DesktopHelper/SystemWakeObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Foundation
import IOKit.pwr_mgt
import os

// From <IOKit/IOMessage.h>; the iokit_common_msg macros aren't imported into Swift.
private let kIOMessageCanSystemSleep: UInt32 = 0xE000_0270
private let kIOMessageSystemWillSleep: UInt32 = 0xE000_0280
private let kIOMessageSystemHasPoweredOn: UInt32 = 0xE000_0300

/// Calls `onWake` when the system fully wakes from sleep (never on dark wake).
/// Unlike `NSWorkspace.didWakeNotification`, IOKit power events are delivered
/// to daemons running outside a GUI login session.
final class SystemWakeObserver {
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "system-wake-observer")
private var rootPort: io_connect_t = 0
private let onWake: @Sendable () -> Void

init(onWake: @escaping @Sendable () -> Void) {
self.onWake = onWake
var notifyPort: IONotificationPortRef?
var notifier: io_object_t = 0
rootPort = IORegisterForSystemPower(
Unmanaged.passUnretained(self).toOpaque(),
&notifyPort,
{ refcon, _, messageType, messageArgument in
let observer = Unmanaged<SystemWakeObserver>.fromOpaque(refcon!).takeUnretainedValue()
observer.handle(messageType: messageType, messageArgument: messageArgument)
},
&notifier
)
guard rootPort != 0, let notifyPort else {
logger.error("failed to register for system power notifications")
return
}
CFRunLoopAddSource(
CFRunLoopGetMain(),
IONotificationPortGetRunLoopSource(notifyPort).takeUnretainedValue(),
.defaultMode
)
}

private func handle(messageType: natural_t, messageArgument: UnsafeMutableRawPointer?) {
switch messageType {
case kIOMessageSystemHasPoweredOn:
logger.info("system woke from sleep")
onWake()
case kIOMessageCanSystemSleep, kIOMessageSystemWillSleep:
// Sleep is delayed until acknowledged.
IOAllowPowerChange(rootPort, Int(bitPattern: messageArgument))
default:
break
}
}
}
7 changes: 7 additions & 0 deletions Coder-Desktop/Coder-DesktopHelper/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,11 @@ let appXPCServer = NSXPCListener(machServiceName: helperAppMachServiceName)
appXPCServer.delegate = appXPCServerDelegate
appXPCServer.resume()

// Nudge the tunnel to rediscover network paths on system wake.
let wakeObserver = SystemWakeObserver {
Task { @MainActor in
await globalManager?.wake()
}
}

RunLoop.main.run()
13 changes: 13 additions & 0 deletions Coder-Desktop/VPNLib/Speaker+Wake.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import SwiftProtobuf

public extension Speaker where SendMsg == Vpn_ManagerMessage, RecvMsg == Vpn_TunnelMessage {
/// Asks the tunnel to rediscover network paths after system wake.
/// No-op if the negotiated protocol version predates WakeRequest (1.3).
func wake() async throws {
guard let negotiatedVersion, negotiatedVersion >= ProtoVersion(1, 3) else { return }
_ = try await unaryRPC(
.with { msg in
msg.wake = .init()
})
}
}
19 changes: 12 additions & 7 deletions Coder-Desktop/VPNLib/Speaker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,21 @@ enum ProtoRole: String {
}

/// A version of the VPN protocol that can be negotiated.
public struct ProtoVersion: CustomStringConvertible, Equatable, Codable, Sendable {
public struct ProtoVersion: CustomStringConvertible, Equatable, Comparable, Codable, Sendable {
let major: Int
let minor: Int

public var description: String { "\(major).\(minor)" }

init(_ major: Int, _ minor: Int) {
public init(_ major: Int, _ minor: Int) {
self.major = major
self.minor = minor
}

public static func < (lhs: ProtoVersion, rhs: ProtoVersion) -> Bool {
(lhs.major, lhs.minor) < (rhs.major, rhs.minor)
}

init(parse str: String) throws(HandshakeError) {
let parts = str.split(separator: ".").map { Int($0) }
if parts.count != 2 {
Expand Down Expand Up @@ -60,6 +64,8 @@ public actor Speaker<SendMsg: RPCMessage & Message, RecvMsg: RPCMessage & Messag
private let receiver: Receiver<RecvMsg>
private let secretary = RPCSecretary<RecvMsg>()
let role: ProtoRole
/// The version negotiated during the handshake, or nil before it completes.
public private(set) var negotiatedVersion: ProtoVersion?

/// Creates an instance that communicates over the provided file handles.
public init(writeFD: FileHandle, readFD: FileHandle) {
Expand Down Expand Up @@ -91,11 +97,10 @@ public actor Speaker<SendMsg: RPCMessage & Message, RecvMsg: RPCMessage & Messag
/// Does the VPN Protocol handshake and validates the result
public func handshake() async throws(HandshakeError) {
let hndsh = Handshaker(writeFD: writeFD, dispatch: dispatch, queue: queue, role: role,
versions: [ProtoVersion(1, 1)])
// ignore the version for now because we know it can only be 1.0 or 1.1.
// 1.1 adds support for telemetry to StartRequest, but since setting these
// fields won't adversely affect a 1.0 speaker, we set them regardless.
try _ = await hndsh.handshake()
versions: [ProtoVersion(1, 3)])
// Minor versions are backwards compatible, so the negotiated version
// is only recorded to gate messages newer than the peer.
negotiatedVersion = try await hndsh.handshake()
}

/// Send a unary RPC message and handle the response
Expand Down
129 changes: 129 additions & 0 deletions Coder-Desktop/VPNLib/vpn.pb.swift

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading