Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Swift

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: macos-14

steps:
- uses: actions/checkout@v3
- name: Build
run: cd swift && swift build -v
- name: Build Release
run: cd swift && swift build -c release
- name: Run tests
run: cd swift && swift test -v
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,12 @@ MigrationBackup/

# Ionide (cross platform F# VS Code tools) working folder
.ionide/

# Swift
.build/
.swiftpm/
*.xcodeproj
*.xcworkspace
xcuserdata/
DerivedData/
Package.resolved
43 changes: 43 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// swift-tools-version: 5.9

import PackageDescription

let package = Package(
name: "DevTunnelsClient",
platforms: [
.iOS(.v16),
.macOS(.v13),
],
products: [
.library(name: "DevTunnelsClient", targets: ["DevTunnelsClient"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
.package(url: "https://github.com/apple/swift-nio-ssh.git", from: "0.9.0"),
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.20.0"),
],
targets: [
.target(
name: "DevTunnelsClient",
dependencies: [
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
.product(name: "NIOWebSocket", package: "swift-nio"),
.product(name: "NIOSSH", package: "swift-nio-ssh"),
.product(name: "NIOTransportServices", package: "swift-nio-transport-services"),
],
path: "swift/Sources/DevTunnelsClient"
),
.testTarget(
name: "DevTunnelsClientTests",
dependencies: [
"DevTunnelsClient",
.product(name: "NIOEmbedded", package: "swift-nio"),
.product(name: "NIOWebSocket", package: "swift-nio"),
.product(name: "NIOSSH", package: "swift-nio-ssh"),
],
path: "swift/Tests/DevTunnelsClientTests"
),
]
)
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ Dev tunnels allows developers to securely expose local web services to the Inter

## SDK Feature Matrix

| Feature | C# | TypeScript | Java | Go | Rust |
|---|---|---|---|---|---|
| Management API | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tunnel Client Connections | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tunnel Host Connections | ✅ | ✅ | ❌ | ❌ | ✅ |
| Reconnection | ✅ | ✅ | ❌ | ❌ | ❌ |
| SSH-level Reconnection | ✅ | ✅ | ❌ | ❌ | ❌ |
| Automatic tunnel access token refresh | ✅ | ✅ | ❌ | ❌ | ❌ |
| Ssh Keep-alive | ✅ | ✅ | ❌ | ❌ | ❌ |
| Feature | C# | TypeScript | Java | Go | Rust | Swift |
|---|---|---|---|---|---|---|
| Management API | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tunnel Client Connections | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tunnel Host Connections | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ |
| Reconnection | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
| SSH-level Reconnection | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Automatic tunnel access token refresh | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Ssh Keep-alive | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |

✅ - Supported
🚧 - In Progress
Expand Down
7 changes: 7 additions & 0 deletions swift/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Swift build
/.build
# Xcode
*.xcodeproj
*.xcworkspace
xcuserdata/
DerivedData/
41 changes: 41 additions & 0 deletions swift/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// swift-tools-version: 5.9

import PackageDescription

let package = Package(
name: "DevTunnelsClient",
platforms: [
.iOS(.v16),
.macOS(.v13),
],
products: [
.library(name: "DevTunnelsClient", targets: ["DevTunnelsClient"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
.package(url: "https://github.com/apple/swift-nio-ssh.git", from: "0.9.0"),
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.20.0"),
],
targets: [
.target(
name: "DevTunnelsClient",
dependencies: [
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
.product(name: "NIOWebSocket", package: "swift-nio"),
.product(name: "NIOSSH", package: "swift-nio-ssh"),
.product(name: "NIOTransportServices", package: "swift-nio-transport-services"),
]
),
.testTarget(
name: "DevTunnelsClientTests",
dependencies: [
"DevTunnelsClient",
.product(name: "NIOEmbedded", package: "swift-nio"),
.product(name: "NIOWebSocket", package: "swift-nio"),
.product(name: "NIOSSH", package: "swift-nio-ssh"),
]
),
]
)
222 changes: 222 additions & 0 deletions swift/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
# dev-tunnels-swift

A pure Swift client library for [Microsoft Dev Tunnels](https://aka.ms/devtunnels/docs). Connect to tunnel forwarded ports from iOS and macOS apps.

## Features

- **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

## Architecture

```
┌─────────────────────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────────────────────┘
```

### Source Layout

```
Sources/DevTunnelsClient/
├── Contracts/ Tunnel, TunnelEndpoint, TunnelPort, TunnelStatus, enums
├── Management/ TunnelManagementClient, DeviceCodeAuth, HTTPClient protocol
└── Connections/ TunnelRelayClient, TunnelRelayStream, port forward messages
```

### How Relay Connections Work

1. **WebSocket** — Connect to the relay URI (`wss://`) with subprotocol `tunnel-relay-client` and `Authorization: Tunnel <accessToken>` header
2. **SSH over WebSocket** — Binary WebSocket frames carry SSH protocol data. SSH authenticates as user `tunnel` with no password (the access token provides auth)
3. **Port forwarding** — Open a `forwarded-tcpip` SSH channel targeting `127.0.0.1:<port>` on the tunnel host
4. **Data streaming** — Bidirectional data flows through the SSH channel

## Installation

Add to your `Package.swift`:

```swift
dependencies: [
.package(url: "https://github.com/microsoft/dev-tunnels.git", from: "0.1.0"),
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "DevTunnelsClient", package: "dev-tunnels"),
]
),
]
```

> The `Package.swift` at the repository root exposes the Swift library.
> Source code lives in `swift/Sources/`.

## Quick Start

### Authentication + Discovery

```swift
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]
)
```

### Tunnel CRUD

```swift
// 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!
)
```

### Direct Connection (Public Ports)

```swift
// 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
}
```

### Relay Connection (Private Ports)

```swift
// 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()
}
```

### Auto-Reconnecting Connection

```swift
// 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) {
// ...
}
```

## Limitations

> **This library is under active development.** The following limitations apply to the current version.

### Not Yet Implemented

- **Server-initiated port notifications** — The SSH `tcpip-forward` global 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.

### Known Constraints

- **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 `InsecureIgnoreHostKey` behavior, since auth is via the tunnel access token).
- **Single-port streams** — Each `TunnelRelayClient` connects to one port. To forward multiple ports, create multiple clients.

## Dependencies

| Package | Purpose |
|---|---|
| [swift-nio](https://github.com/apple/swift-nio) | Async networking, WebSocket codec |
| [swift-nio-ssh](https://github.com/apple/swift-nio-ssh) | SSH protocol over WebSocket |
| [swift-nio-transport-services](https://github.com/apple/swift-nio-transport-services) | Apple TLS via Network.framework |

## Requirements

- iOS 16+ / macOS 13+
- Swift 5.9+

## Testing

```bash
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.
Loading
Loading