Skip to content

Commit 4b15033

Browse files
Brian MendoncaBrian Mendonca
authored andcommitted
iOS/Security: force TLS for non-loopback manual gateway sessions
1 parent 3077c35 commit 4b15033

File tree

2 files changed

+66
-6
lines changed

2 files changed

+66
-6
lines changed

apps/ios/Sources/Gateway/GatewayConnectionController.swift

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ final class GatewayConnectionController {
162162
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
163163
let token = GatewaySettingsStore.loadGatewayToken(instanceId: instanceId)
164164
let password = GatewaySettingsStore.loadGatewayPassword(instanceId: instanceId)
165-
let resolvedUseTLS = useTLS
165+
let resolvedUseTLS = self.resolveManualUseTLS(host: host, useTLS: useTLS)
166166
guard let resolvedPort = self.resolveManualPort(host: host, port: port, useTLS: resolvedUseTLS)
167167
else { return }
168168
let stableID = self.manualStableID(host: host, port: resolvedPort)
@@ -309,7 +309,7 @@ final class GatewayConnectionController {
309309

310310
let manualPort = defaults.integer(forKey: "gateway.manual.port")
311311
let manualTLS = defaults.bool(forKey: "gateway.manual.tls")
312-
let resolvedUseTLS = manualTLS || self.shouldForceTLS(host: manualHost)
312+
let resolvedUseTLS = self.resolveManualUseTLS(host: manualHost, useTLS: manualTLS)
313313
guard let resolvedPort = self.resolveManualPort(
314314
host: manualHost,
315315
port: manualPort,
@@ -320,7 +320,7 @@ final class GatewayConnectionController {
320320
let tlsParams = self.resolveManualTLSParams(
321321
stableID: stableID,
322322
tlsEnabled: resolvedUseTLS,
323-
allowTOFUReset: self.shouldForceTLS(host: manualHost))
323+
allowTOFUReset: self.shouldRequireTLS(host: manualHost))
324324

325325
guard let url = self.buildGatewayURL(
326326
host: manualHost,
@@ -340,7 +340,7 @@ final class GatewayConnectionController {
340340

341341
if let lastKnown = GatewaySettingsStore.loadLastGatewayConnection() {
342342
if case let .manual(host, port, useTLS, stableID) = lastKnown {
343-
let resolvedUseTLS = useTLS || self.shouldForceTLS(host: host)
343+
let resolvedUseTLS = self.resolveManualUseTLS(host: host, useTLS: useTLS)
344344
let stored = GatewayTLSStore.loadFingerprint(stableID: stableID)
345345
let tlsParams = stored.map { fp in
346346
GatewayTLSParams(required: true, expectedFingerprint: fp, allowTOFU: false, storeKey: stableID)
@@ -646,12 +646,41 @@ final class GatewayConnectionController {
646646
return components.url
647647
}
648648

649-
private func shouldForceTLS(host: String) -> Bool {
649+
private func resolveManualUseTLS(host: String, useTLS: Bool) -> Bool {
650+
useTLS || self.shouldRequireTLS(host: host)
651+
}
652+
653+
private func shouldRequireTLS(host: String) -> Bool {
654+
!Self.isLoopbackHost(host)
655+
}
656+
657+
private func shouldUseTLSDefaultPort443(host: String) -> Bool {
650658
let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
651659
if trimmed.isEmpty { return false }
652660
return trimmed.hasSuffix(".ts.net") || trimmed.hasSuffix(".ts.net.")
653661
}
654662

663+
private static func isLoopbackHost(_ rawHost: String) -> Bool {
664+
var host = rawHost.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
665+
guard !host.isEmpty else { return false }
666+
667+
if host.hasPrefix("[") && host.hasSuffix("]") {
668+
host.removeFirst()
669+
host.removeLast()
670+
}
671+
if host.hasSuffix(".") {
672+
host.removeLast()
673+
}
674+
if let zoneIndex = host.firstIndex(of: "%") {
675+
host = String(host[..<zoneIndex])
676+
}
677+
678+
if host == "localhost" || host == "::1" || host == "0.0.0.0" || host == "::" {
679+
return true
680+
}
681+
return host.hasPrefix("127.")
682+
}
683+
655684
private func manualStableID(host: String, port: Int) -> String {
656685
"manual|\(host.lowercased())|\(port)"
657686
}
@@ -691,7 +720,7 @@ final class GatewayConnectionController {
691720
}
692721
let trimmedHost = host.trimmingCharacters(in: .whitespacesAndNewlines)
693722
guard !trimmedHost.isEmpty else { return nil }
694-
if useTLS && self.shouldForceTLS(host: trimmedHost) {
723+
if useTLS && self.shouldUseTLSDefaultPort443(host: trimmedHost) {
695724
return 443
696725
}
697726
return 18789
@@ -942,6 +971,14 @@ extension GatewayConnectionController {
942971
{
943972
self.resolveDiscoveredTLSParams(gateway: gateway, allowTOFU: allowTOFU)
944973
}
974+
975+
func _test_resolveManualUseTLS(host: String, useTLS: Bool) -> Bool {
976+
self.resolveManualUseTLS(host: host, useTLS: useTLS)
977+
}
978+
979+
func _test_resolveManualPort(host: String, port: Int, useTLS: Bool) -> Int? {
980+
self.resolveManualPort(host: host, port: port, useTLS: useTLS)
981+
}
945982
}
946983
#endif
947984

apps/ios/Tests/GatewayConnectionSecurityTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,27 @@ import Testing
102102

103103
#expect(controller._test_didAutoConnect() == false)
104104
}
105+
106+
@Test @MainActor func manualConnectionsForceTLSForNonLoopbackHosts() async {
107+
let appModel = NodeAppModel()
108+
let controller = GatewayConnectionController(appModel: appModel, startDiscovery: false)
109+
110+
#expect(controller._test_resolveManualUseTLS(host: "gateway.example.com", useTLS: false) == true)
111+
#expect(controller._test_resolveManualUseTLS(host: "openclaw.local", useTLS: false) == true)
112+
#expect(controller._test_resolveManualUseTLS(host: "localhost", useTLS: false) == false)
113+
#expect(controller._test_resolveManualUseTLS(host: "127.0.0.1", useTLS: false) == false)
114+
#expect(controller._test_resolveManualUseTLS(host: "::1", useTLS: false) == false)
115+
#expect(controller._test_resolveManualUseTLS(host: "[::1]", useTLS: false) == false)
116+
#expect(controller._test_resolveManualUseTLS(host: "0.0.0.0", useTLS: false) == false)
117+
}
118+
119+
@Test @MainActor func manualDefaultPortUses443OnlyForTailnetTLSHosts() async {
120+
let appModel = NodeAppModel()
121+
let controller = GatewayConnectionController(appModel: appModel, startDiscovery: false)
122+
123+
#expect(controller._test_resolveManualPort(host: "gateway.example.com", port: 0, useTLS: true) == 18789)
124+
#expect(controller._test_resolveManualPort(host: "device.sample.ts.net", port: 0, useTLS: true) == 443)
125+
#expect(controller._test_resolveManualPort(host: "device.sample.ts.net.", port: 0, useTLS: true) == 443)
126+
#expect(controller._test_resolveManualPort(host: "device.sample.ts.net", port: 18789, useTLS: true) == 18789)
127+
}
105128
}

0 commit comments

Comments
 (0)