https://github.com/wiedymi/swift-cloudflared
Pure Swift Cloudflare Access TCP tunnel SDK for SSH clients on Apple platforms (iOS/macOS).
https://github.com/wiedymi/swift-cloudflared
cloudflare cloudflare-access ios libssh2 macos networking ssh swift swift-package-manager
Last synced: 6 days ago
JSON representation
Pure Swift Cloudflare Access TCP tunnel SDK for SSH clients on Apple platforms (iOS/macOS).
- Host: GitHub
- URL: https://github.com/wiedymi/swift-cloudflared
- Owner: wiedymi
- License: mit
- Created: 2026-02-08T11:55:40.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-02-20T14:05:45.000Z (23 days ago)
- Last Synced: 2026-02-20T18:19:24.426Z (23 days ago)
- Topics: cloudflare, cloudflare-access, ios, libssh2, macos, networking, ssh, swift, swift-package-manager
- Language: Swift
- Size: 104 KB
- Stars: 3
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# swift-cloudflared
[](https://github.com/wiedymi)
[](https://x.com/wiedymi)
[](mailto:contact@wiedymi.com)
[](https://discord.gg/zemMZtrkSb)
[](https://github.com/sponsors/vivy-company)
Pure Swift Cloudflare Access TCP tunnel SDK for SSH clients on Apple platforms.
Use it to open an Access-authenticated local endpoint (`127.0.0.1:`) and connect your SSH stack (for example `libssh2`) through it.
## Features
- OAuth and Service Token auth method support
- Async session API with connection state stream
- Local loopback tunnel endpoint for SSH client libraries
- Secure default local listener policy:
- one active local client by default
- listener closes after first accepted client by default
- Pluggable auth, token storage, and tunnel layers
- Built-in keychain token store on `macOS`, `iOS`, `tvOS`, `watchOS`
## Platforms
- iOS 16+
- macOS 13+
- Swift tools 6.0+
## Installation
Add to your `Package.swift`:
```swift
dependencies: [
.package(url: "https://github.com/wiedymi/swift-cloudflared.git", from: "0.1.0")
]
```
Then add the library target:
```swift
.target(
name: "YourApp",
dependencies: [
.product(name: "Cloudflared", package: "swift-cloudflared")
]
)
```
## Quick Start (Service Token)
```swift
import Cloudflared
let session = SessionActor(
authProvider: ServiceTokenProvider(),
tunnelProvider: CloudflareTunnelProvider(),
retryPolicy: RetryPolicy(maxReconnectAttempts: 2, baseDelayNanoseconds: 500_000_000),
oauthFallback: nil,
sleep: { delay in try? await Task.sleep(nanoseconds: delay) }
)
let localPort = try await session.connect(
hostname: "ssh.example.com",
method: .serviceToken(
teamDomain: "your-team.cloudflareaccess.com",
clientID: "",
clientSecret: ""
)
)
// Use 127.0.0.1:localPort from libssh2
print("Tunnel endpoint: 127.0.0.1:\(localPort)")
```
## OAuth Flow Integration
OAuth UI/token acquisition is app-owned via `OAuthFlow`:
```swift
import Cloudflared
struct MyOAuthFlow: OAuthFlow {
func fetchToken(
teamDomain: String,
appDomain: String,
callbackScheme: String,
hostname: String
) async throws -> String {
// Implement your Access login UX (for example ASWebAuthenticationSession)
// and return CF_Authorization JWT.
throw Failure.auth("not implemented")
}
}
let oauthProvider = OAuthProvider(
flow: MyOAuthFlow(),
tokenStore: KeychainTokenStore()
)
```
For app metadata discovery (`authDomain`, `appDomain`, `appAUD`) you can use `AppInfoResolver`.
## State Stream
Observe state changes from the session:
```swift
Task {
for await state in session.state {
print("state:", state)
}
}
```
States: `idle`, `authenticating`, `connecting`, `connected(localPort)`, `reconnecting(attempt)`, `disconnected`, `failed`.
## libssh2 Integration
Once connected, point your SSH stack to loopback:
```c
// Connect libssh2 socket to 127.0.0.1:
// then run regular libssh2 handshake/auth/channel flow.
```
Example runtime mapping:
- Host: `127.0.0.1`
- Port: ``
## Token Storage Customization
If you need your own keychain layout or storage backend, implement `TokenStore`:
```swift
import Cloudflared
actor CustomTokenStore: TokenStore {
func readToken(for key: String) async throws -> String? { nil }
func writeToken(_ token: String, for key: String) async throws {}
func removeToken(for key: String) async throws {}
}
```
Then inject it into `OAuthProvider`.
`KeychainTokenStore` defaults:
- iOS/tvOS/watchOS: `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`
- macOS: `kSecAttrAccessibleAfterFirstUnlock` + data-protection keychain mode
iCloud Keychain options:
```swift
// Option 1: explicit sync mode on the base store
let store = KeychainTokenStore(syncMode: .iCloud)
// Option 2: dedicated convenience wrapper
let store = ICloudKeychainTokenStore()
```
## Local Security Defaults
`CloudflareTunnelProvider` defaults to:
- `maxConcurrentConnections = 1`
- `stopAcceptingAfterFirstConnection = true`
You can override via:
```swift
let tunnel = CloudflareTunnelProvider(
connectionLimits: .init(
maxConcurrentConnections: 2,
stopAcceptingAfterFirstConnection: false
)
)
```
## Sandbox and Entitlements
- iOS app sandbox: supported (foreground usage; OAuth flow is app-defined).
- macOS App Sandbox: enable network client/server entitlements if sandboxed, because the SDK opens:
- outbound websocket connection to Cloudflare Access
- local loopback listener for SSH client connection
## E2E Harness
Run the local interactive harness:
```bash
swift run cloudflared-e2e
```
It prints a local endpoint (`127.0.0.1:`) you can test with SSH/libssh2.
## Development
```bash
swift test
swift build
```
Optional keychain integration test:
```bash
CLOUDFLARED_KEYCHAIN_TESTS=1 swift test --filter TokenStoreTests/testKeychainStoreRoundTrip
```
## Docs
- `docs/SPEC.md` - requirements and acceptance criteria
- `docs/ARCHITECTURE.md` - design and concurrency model
- `docs/API.md` - API surface and compatibility notes
- `docs/PROTOCOL_MAPPING.md` - upstream behavior mapping
- `docs/TEST_COVERAGE.md` - test traceability/coverage gates
## Repository Notes
- `reference/cloudflared` is included as a git submodule for upstream reference.
## License
- Root project: MIT (`LICENSE`)
- Third-party notices: `THIRD_PARTY_NOTICES.md`