A pure Swift client library for Microsoft Dev Tunnels. Connect to tunnel forwarded ports from iOS and macOS apps.
- Tunnel management — Full CRUD: list, get, create, update, delete tunnels and ports via the REST API
- GitHub authentication — Device code flow for tunnel access tokens
- Direct connections — Connect to publicly accessible tunnel ports via HTTPS
- Relay connections — Connect to private tunnel ports via WebSocket + SSH port forwarding
- Auto-reconnect — Configurable reconnection with exponential backoff on connection drops
- Keepalive — Periodic WebSocket pings to prevent idle connection drops
- TLS — Native Apple TLS via Network.framework for secure relay connections
- Pure Swift — No FFI, no Rust, no cross-compilation — just a Swift Package
┌─────────────────────────────────────────────────────┐
│ Your App │
├─────────────────────────────────────────────────────┤
│ DeviceCodeAuth TunnelManagementClient │ ← Management layer
│ (GitHub OAuth) (REST: list/get/create/update/ │
│ delete tunnels & ports) │
├─────────────────────────────────────────────────────┤
│ TunnelConnection │ ← Connection helpers
│ (direct URLs, token extraction, online detection) │
├─────────────────────────────────────────────────────┤
│ TunnelRelayClient │ ← Relay client
│ (public API: connect/disconnect, state machine) │
├─────────────────────────────────────────────────────┤
│ TunnelRelayStream │ ← NIO pipeline
│ ┌───────────────────────────────────────────────┐ │
│ │ NIOTSConnectionBootstrap (TLS for wss://) │ │
│ │ → WebSocketUpgradeHandler (HTTP → WS) │ │
│ │ → WebSocketBinaryFrameHandler │ │
│ │ → NIOSSHHandler (user: "tunnel") │ │
│ │ → forwardedTCPIP channel │ │
│ │ → SSHPortForwardDataHandler │ │
│ └───────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────┤
│ Contracts: Tunnel, TunnelEndpoint, TunnelPort, │ ← Types
│ TunnelStatus, enums (Codable, Sendable) │
└─────────────────────────────────────────────────────┘
Sources/DevTunnelsClient/
├── Contracts/ Tunnel, TunnelEndpoint, TunnelPort, TunnelStatus, enums
├── Management/ TunnelManagementClient, DeviceCodeAuth, HTTPClient protocol
└── Connections/ TunnelRelayClient, TunnelRelayStream, port forward messages
- WebSocket — Connect to the relay URI (
wss://) with subprotocoltunnel-relay-clientandAuthorization: Tunnel <accessToken>header - SSH over WebSocket — Binary WebSocket frames carry SSH protocol data. SSH authenticates as user
tunnelwith no password (the access token provides auth) - Port forwarding — Open a
forwarded-tcpipSSH channel targeting127.0.0.1:<port>on the tunnel host - Data streaming — Bidirectional data flows through the SSH channel
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/rebornix/dev-tunnels-swift.git", from: "0.1.0"),
]import DevTunnelsClient
// Authenticate via GitHub device code flow
let auth = try await DeviceCodeAuth.start()
print("Go to \(auth.verificationUri) and enter: \(auth.userCode)")
let token = try await DeviceCodeAuth.poll(deviceCode: auth.deviceCode)
// List tunnels
let client = TunnelManagementClient(accessToken: token)
let tunnels = try await client.listTunnels()
// Get tunnel detail with connect token
let detail = try await client.getTunnel(
clusterId: "usw2",
tunnelId: "my-tunnel",
tokenScopes: [TunnelAccessScopes.connect]
)// Create a tunnel
let newTunnel = try await client.createTunnel(Tunnel(name: "my-app"))
// Update a tunnel
var updated = newTunnel
updated.description = "Production endpoint"
let result = try await client.updateTunnel(updated)
// Add a port
let port = try await client.createTunnelPort(
clusterId: newTunnel.clusterId!,
tunnelId: newTunnel.tunnelId!,
port: TunnelPort(portNumber: 8080, protocol: .https)
)
// Delete a port
try await client.deleteTunnelPort(
clusterId: newTunnel.clusterId!,
tunnelId: newTunnel.tunnelId!,
portNumber: 8080
)
// Delete a tunnel
try await client.deleteTunnel(
clusterId: newTunnel.clusterId!,
tunnelId: newTunnel.tunnelId!
)// For public tunnel ports — just use the direct URL
if let url = TunnelConnection.directURL(from: tunnel, port: 8080) {
// Use URLSession, WKWebView, etc. with this URL
}// For private tunnel ports — connect through the relay
if let relay = TunnelRelayClient.fromTunnel(detail, port: 8080) {
let stream = try await relay.connect()
// stream.send(data) / stream.close()
}// Automatically reconnect on connection drops
let relay = TunnelRelayClient(config: config)
// Observe state changes
relay.onStateChangeHandler = { state in
print("State: \(state)") // .connected, .reconnecting(attempt: 1), etc.
}
// Each iteration yields a new stream after (re)connection
for await stream in relay.connectWithReconnect() {
// Use stream until it disconnects; loop yields a new one
}
// Custom retry policy
let policy = ReconnectPolicy(
maxAttempts: 10,
initialDelay: 0.5,
maxDelay: 60,
backoffMultiplier: 2.0
)
for await stream in relay.connectWithReconnect(policy: policy) {
// ...
}This library is under active development. The following limitations apply to the current version.
- Server-initiated port notifications — The SSH
tcpip-forwardglobal request (server telling the client which ports are available) is not yet handled. The client must know the port number in advance. - Local TCP listener — The Go/TS SDKs can open a local TCP socket and forward connections to the tunnel. This library provides the raw stream; local listener forwarding is the caller's responsibility.
- Host-side functionality — This is a client-only library. Hosting a tunnel (registering ports, accepting connections) is out of scope.
- Apple platforms only for TLS — TLS uses
NIOTransportServices(Network.framework), which requires iOS/macOS. Non-Apple platforms would need NIOSSL instead. - No certificate pinning — The relay connection trusts the system TLS certificate store. The SSH layer accepts any host key (matching the Go SDK's
InsecureIgnoreHostKeybehavior, since auth is via the tunnel access token). - Single-port streams — Each
TunnelRelayClientconnects to one port. To forward multiple ports, create multiple clients.
| Package | Purpose |
|---|---|
| swift-nio | Async networking, WebSocket codec |
| swift-nio-ssh | SSH protocol over WebSocket |
| swift-nio-transport-services | Apple TLS via Network.framework |
- iOS 16+ / macOS 13+
- Swift 5.9+
swift test # 155 tests, all offline (no network requests)All tests use mock HTTP clients and NIO EmbeddedChannel — no real network calls are made during testing.