Skip to content

Commit 0ee025c

Browse files
authored
feat(connections): Cloudflare Access TCP tunnel integration (#1285) (#1407)
* feat(connections): manage cloudflared access tcp tunnels per connection (#1285) * fix(connections): expand ~ in cloudflared path and fix pid-path buffer size (#1285) * chore(connections): add localized strings for Cloudflare tunnel (#1285) * fix(connections): clean up Cloudflare tokens on delete and surface sign-in errors (#1285) * refactor(connections): share tunnel rewrite and reconnect logic between SSH and Cloudflare (#1285)
1 parent dadb8c0 commit 0ee025c

31 files changed

Lines changed: 2036 additions & 93 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Cloudflare Tunnel: connect to a database behind Cloudflare Access by letting TablePro start and stop `cloudflared access tcp` for you, the same way it manages SSH tunnels. Configure it per connection with browser sign-in or a service token. Needs cloudflared installed (`brew install cloudflared`). (#1285)
1213
- Fill Column: right-click a column header and choose Fill Column to set one value across all loaded rows. The change is staged like a normal edit, so you review it and Save before it applies, and one undo reverts the whole fill. Not available on primary key columns. (#1304)
1314
- AWS IAM authentication for PostgreSQL and MySQL connections to RDS and Aurora. Pick AWS IAM in the connection's Authentication field and use an access key, a named AWS profile, or SSO. TablePro generates a fresh login token on every connect and reconnect, so you never paste an expiring token, and SSL is required automatically. (#1291)
1415

TablePro/AppDelegate.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
5959
UserDefaults.standard.set(passwordSyncExpected, forKey: KeychainHelper.passwordSyncEnabledKey)
6060
DatabaseManager.shared.startObservingSystemEvents()
6161

62+
Task { await CloudflareTunnelManager.shared.sweepStalePidsIfNeeded() }
63+
6264
MemoryPressureAdvisor.startMonitoring()
6365
PluginManager.shared.loadPlugins()
6466
UNUserNotificationCenter.current().delegate = self
@@ -136,6 +138,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
136138
LinkedFolderWatcher.shared.stop()
137139
SQLFolderWatcher.shared.stop()
138140
SSHTunnelManager.shared.terminateAllProcessesSync()
141+
CloudflareTunnelManager.shared.terminateAllProcessesSync()
139142
}
140143

141144
@objc func handleSystemDidWake(_ notification: Notification) {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// CloudflareTunnelError.swift
3+
// TablePro
4+
//
5+
6+
import Foundation
7+
8+
/// Errors raised while starting or supervising a cloudflared Access TCP tunnel.
9+
enum CloudflareTunnelError: Error, LocalizedError, Equatable {
10+
case binaryNotFound
11+
case noAvailablePort
12+
case startupFailed(stderrTail: String)
13+
case readinessTimeout(stderrTail: String)
14+
case browserAuthRequired(url: String)
15+
case mutualExclusivityViolation
16+
case tunnelAlreadyExists(UUID)
17+
18+
var errorDescription: String? {
19+
switch self {
20+
case .binaryNotFound:
21+
return String(localized: "cloudflared was not found. Install it with `brew install cloudflared`, or set its path in the connection's Cloudflare Tunnel settings.")
22+
case .noAvailablePort:
23+
return String(localized: "No available local port for the Cloudflare tunnel.")
24+
case .startupFailed(let stderrTail):
25+
return stderrTail.isEmpty
26+
? String(localized: "cloudflared failed to start.")
27+
: String(format: String(localized: "cloudflared failed to start: %@"), stderrTail)
28+
case .readinessTimeout(let stderrTail):
29+
return stderrTail.isEmpty
30+
? String(localized: "The Cloudflare tunnel did not become ready in time.")
31+
: String(format: String(localized: "The Cloudflare tunnel did not become ready in time: %@"), stderrTail)
32+
case .browserAuthRequired(let url):
33+
return String(format: String(localized: "Cloudflare Access needs a browser sign-in. Sign in at %@, then reconnect."), url)
34+
case .mutualExclusivityViolation:
35+
return String(localized: "A connection cannot use SSH and Cloudflare tunnels at the same time.")
36+
case .tunnelAlreadyExists(let id):
37+
return String(format: String(localized: "A Cloudflare tunnel already exists for connection: %@"), id.uuidString)
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)