swift-cloudflared

February 8, 2026 ยท View on GitHub

GitHub Twitter Email Discord Support me

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:<port>) 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:

dependencies: [
    .package(url: "https://github.com/wiedymi/swift-cloudflared.git", from: "0.1.0")
]

Then add the library target:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "Cloudflared", package: "swift-cloudflared")
    ]
)

Quick Start (Service Token)

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: "<CF_ACCESS_CLIENT_ID>",
        clientSecret: "<CF_ACCESS_CLIENT_SECRET>"
    )
)

// 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:

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:

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:

// Connect libssh2 socket to 127.0.0.1:<localPort>
// then run regular libssh2 handshake/auth/channel flow.

Example runtime mapping:

  • Host: 127.0.0.1
  • Port: <localPort from session.connect(...)>

Token Storage Customization

If you need your own keychain layout or storage backend, implement TokenStore:

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:

// 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:

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:

swift run cloudflared-e2e

It prints a local endpoint (127.0.0.1:<port>) you can test with SSH/libssh2.

Development

swift test
swift build

Optional keychain integration test:

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