fix(macos): honor discovered gateway ports

This commit is contained in:
Nima Karimi
2026-01-07 09:05:43 +00:00
committed by Peter Steinberger
parent eef90b47a3
commit a5b29623b8
7 changed files with 66 additions and 5 deletions

View File

@@ -123,6 +123,35 @@ enum ClawdbotConfigFile {
return nil return nil
} }
static func remoteGatewayPort() -> Int? {
let root = self.loadDict()
guard let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any],
let raw = remote["url"] as? String
else {
return nil
}
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let url = URL(string: trimmed), let port = url.port, port > 0 else {
return nil
}
return port
}
static func setRemoteGatewayUrl(host: String, port: Int?) {
guard let port, port > 0 else { return }
let trimmedHost = host.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedHost.isEmpty else { return }
self.updateGatewayDict { gateway in
var remote = gateway["remote"] as? [String: Any] ?? [:]
let existingUrl = (remote["url"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let scheme = URL(string: existingUrl)?.scheme ?? "ws"
remote["url"] = "\(scheme)://\(trimmedHost):\(port)"
gateway["remote"] = remote
}
}
private static func parseConfigData(_ data: Data) -> [String: Any]? { private static func parseConfigData(_ data: Data) -> [String: Any]? {
if let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { if let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
return root return root

View File

@@ -18,6 +18,7 @@ final class GatewayDiscoveryModel {
var lanHost: String? var lanHost: String?
var tailnetDns: String? var tailnetDns: String?
var sshPort: Int var sshPort: Int
var gatewayPort: Int?
var cliPath: String? var cliPath: String?
var stableID: String var stableID: String
var debugID: String var debugID: String
@@ -138,6 +139,7 @@ final class GatewayDiscoveryModel {
lanHost: parsedTXT.lanHost, lanHost: parsedTXT.lanHost,
tailnetDns: parsedTXT.tailnetDns, tailnetDns: parsedTXT.tailnetDns,
sshPort: parsedTXT.sshPort, sshPort: parsedTXT.sshPort,
gatewayPort: parsedTXT.gatewayPort,
cliPath: parsedTXT.cliPath, cliPath: parsedTXT.cliPath,
stableID: stableID, stableID: stableID,
debugID: BridgeEndpointID.prettyDescription(result.endpoint), debugID: BridgeEndpointID.prettyDescription(result.endpoint),
@@ -207,11 +209,12 @@ final class GatewayDiscoveryModel {
} }
static func parseGatewayTXT(_ txt: [String: String]) static func parseGatewayTXT(_ txt: [String: String])
-> (lanHost: String?, tailnetDns: String?, sshPort: Int, cliPath: String?) -> (lanHost: String?, tailnetDns: String?, sshPort: Int, gatewayPort: Int?, cliPath: String?)
{ {
var lanHost: String? var lanHost: String?
var tailnetDns: String? var tailnetDns: String?
var sshPort = 22 var sshPort = 22
var gatewayPort: Int?
var cliPath: String? var cliPath: String?
if let value = txt["lanHost"] { if let value = txt["lanHost"] {
@@ -228,12 +231,18 @@ final class GatewayDiscoveryModel {
{ {
sshPort = parsed sshPort = parsed
} }
if let value = txt["gatewayPort"],
let parsed = Int(value.trimmingCharacters(in: .whitespacesAndNewlines)),
parsed > 0
{
gatewayPort = parsed
}
if let value = txt["cliPath"] { if let value = txt["cliPath"] {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
cliPath = trimmed.isEmpty ? nil : trimmed cliPath = trimmed.isEmpty ? nil : trimmed
} }
return (lanHost, tailnetDns, sshPort, cliPath) return (lanHost, tailnetDns, sshPort, gatewayPort, cliPath)
} }
static func buildSSHTarget(user: String, host: String, port: Int) -> String { static func buildSSHTarget(user: String, host: String, port: Int) -> String {

View File

@@ -694,6 +694,7 @@ extension GeneralSettings {
host: host, host: host,
port: gateway.sshPort) port: gateway.sshPort)
self.state.remoteCliPath = gateway.cliPath ?? "" self.state.remoteCliPath = gateway.cliPath ?? ""
ClawdbotConfigFile.setRemoteGatewayUrl(host: host, port: gateway.gatewayPort)
} }
} }

View File

@@ -202,6 +202,19 @@ final class MacNodeModeCoordinator {
return 18790 return 18790
} }
static func remoteBridgePort() -> Int {
let fallback = Int(Self.loopbackBridgePort() ?? 18790)
let base = ClawdbotConfigFile.remoteGatewayPort() ?? GatewayEnvironment.gatewayPort()
guard base > 0 else { return fallback }
return Self.derivePort(base: base, offset: 1, fallback: fallback)
}
private static func derivePort(base: Int, offset: Int, fallback: Int) -> Int {
let derived = base + offset
guard derived > 0, derived <= Int(UInt16.max) else { return fallback }
return derived
}
static func probeEndpoint(_ endpoint: NWEndpoint, timeoutSeconds: Double) async -> Bool { static func probeEndpoint(_ endpoint: NWEndpoint, timeoutSeconds: Double) async -> Bool {
let connection = NWConnection(to: endpoint, using: .tcp) let connection = NWConnection(to: endpoint, using: .tcp)
let stream = Self.makeStateStream(for: connection) let stream = Self.makeStateStream(for: connection)
@@ -269,7 +282,10 @@ final class MacNodeModeCoordinator {
if mode == .remote { if mode == .remote {
do { do {
if self.tunnel == nil || self.tunnel?.process.isRunning == false { if self.tunnel == nil || self.tunnel?.process.isRunning == false {
self.tunnel = try await RemotePortTunnel.create(remotePort: 18790) let remotePort = Self.remoteBridgePort()
self.tunnel = try await RemotePortTunnel.create(
remotePort: remotePort,
allowRemoteUrlOverride: false)
} }
if let localPort = self.tunnel?.localPort, if let localPort = self.tunnel?.localPort,
let port = NWEndpoint.Port(rawValue: localPort) let port = NWEndpoint.Port(rawValue: localPort)

View File

@@ -29,6 +29,7 @@ extension OnboardingView {
user: user, user: user,
host: host, host: host,
port: gateway.sshPort) port: gateway.sshPort)
ClawdbotConfigFile.setRemoteGatewayUrl(host: host, port: gateway.gatewayPort)
} }
self.state.remoteCliPath = gateway.cliPath ?? "" self.state.remoteCliPath = gateway.cliPath ?? ""

View File

@@ -12,6 +12,7 @@ extension OnboardingView {
lanHost: "bridge.local", lanHost: "bridge.local",
tailnetDns: "bridge.ts.net", tailnetDns: "bridge.ts.net",
sshPort: 2222, sshPort: 2222,
gatewayPort: 18789,
cliPath: "/usr/local/bin/clawdbot", cliPath: "/usr/local/bin/clawdbot",
stableID: "bridge-1", stableID: "bridge-1",
debugID: "bridge-1", debugID: "bridge-1",

View File

@@ -38,7 +38,11 @@ final class RemotePortTunnel {
Task { await PortGuardian.shared.removeRecord(pid: pid) } Task { await PortGuardian.shared.removeRecord(pid: pid) }
} }
static func create(remotePort: Int, preferredLocalPort: UInt16? = nil) async throws -> RemotePortTunnel { static func create(
remotePort: Int,
preferredLocalPort: UInt16? = nil,
allowRemoteUrlOverride: Bool = true
) async throws -> RemotePortTunnel {
let settings = CommandResolver.connectionSettings() let settings = CommandResolver.connectionSettings()
guard settings.mode == .remote, let parsed = CommandResolver.parseSSHTarget(settings.target) else { guard settings.mode == .remote, let parsed = CommandResolver.parseSSHTarget(settings.target) else {
throw NSError( throw NSError(
@@ -49,7 +53,7 @@ final class RemotePortTunnel {
let localPort = try await Self.findPort(preferred: preferredLocalPort) let localPort = try await Self.findPort(preferred: preferredLocalPort)
let sshHost = parsed.host.trimmingCharacters(in: .whitespacesAndNewlines) let sshHost = parsed.host.trimmingCharacters(in: .whitespacesAndNewlines)
let remotePortOverride = Self.resolveRemotePortOverride(for: sshHost) let remotePortOverride = allowRemoteUrlOverride ? Self.resolveRemotePortOverride(for: sshHost) : nil
let resolvedRemotePort = remotePortOverride ?? remotePort let resolvedRemotePort = remotePortOverride ?? remotePort
if let override = remotePortOverride { if let override = remotePortOverride {
Self.logger.info( Self.logger.info(