Skip to content
Draft
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ Dev tunnels allows developers to securely expose local web services to the Inter
|---|---|---|---|---|---|
| Management API | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tunnel Client Connections | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tunnel Host Connections | ✅ | ✅ | ❌ | | ✅ |
| Reconnection | ✅ | ✅ | ❌ | | ❌ |
| Tunnel Host Connections | ✅ | ✅ | ❌ | | ✅ |
| Reconnection | ✅ | ✅ | ❌ | | ❌ |
| SSH-level Reconnection | ✅ | ✅ | ❌ | ❌ | ❌ |
| Automatic tunnel access token refresh | ✅ | ✅ | ❌ | | ❌ |
| Automatic tunnel access token refresh | ✅ | ✅ | ❌ | | ❌ |
| Ssh Keep-alive | ✅ | ✅ | ❌ | ❌ | ❌ |

✅ - Supported
Expand Down
37 changes: 37 additions & 0 deletions go/tunnels/connection_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package tunnels

// ConnectionStatus represents the connection state of a tunnel host.
type ConnectionStatus int

const (
// ConnectionStatusNone indicates no connection has been made.
ConnectionStatusNone ConnectionStatus = iota

// ConnectionStatusConnecting indicates a connection is in progress.
ConnectionStatusConnecting

// ConnectionStatusConnected indicates the host is connected.
ConnectionStatusConnected

// ConnectionStatusDisconnected indicates the host has disconnected.
ConnectionStatusDisconnected
)

// String returns the string representation of the connection status.
func (s ConnectionStatus) String() string {
switch s {
case ConnectionStatusNone:
return "None"
case ConnectionStatusConnecting:
return "Connecting"
case ConnectionStatusConnected:
return "Connected"
case ConnectionStatusDisconnected:
return "Disconnected"
default:
return "Unknown"
}
}
50 changes: 48 additions & 2 deletions go/tunnels/examples/getting_started.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,55 @@
# Getting Started

To use the example you must do the following setup first:
## Client Example

To use the client example you must do the following setup first:

1. Create a tunnel on the CLI or another SDK and put the tunnelId and clusterId in the constants section of example.go
2. Create ports on the tunnel that you want to be hosted
3. Get a tunnels access token and paste it in the return value of getAccessToken() in example.go or set it as the TUNNELS_TOKEN environment variable
4. Start hosting the tunnel either on the CLI or on a different SDK
5. Run example.go with the command `go run example.go`
5. Run example.go with the command `go run example.go`

## Host Example

To use the host example:

1. Create a tunnel on the CLI or management API
2. Get a host access token for the tunnel and set it as the TUNNELS_TOKEN environment variable
3. Set the `hostTunnelID` and `hostClusterID` constants in host/host_example.go
4. Set the `localPort` constant to the local TCP port you want to forward (default: 8080)
5. Start a local service on that port (e.g., `python -m http.server 8080`)
6. Run the host: `cd host && TUNNELS_TOKEN=<token> go run host_example.go`

The host will:
- Connect to the relay and register an endpoint
- Forward the specified local port to remote clients
- Automatically reconnect if the relay connection drops
- Shut down gracefully on Ctrl+C (unregisters the endpoint)

### Host API Overview

```go
// Create a host
host, err := tunnels.NewHost(logger, manager)

// Optional: enable reconnection and status callbacks
host.EnableReconnect = true
host.ConnectionStatusChanged = func(prev, curr tunnels.ConnectionStatus) { ... }

// Connect to the relay
host.Connect(ctx, tunnel)

// Add/remove forwarded ports dynamically
host.AddPort(ctx, &tunnels.TunnelPort{PortNumber: 8080})
host.RemovePort(ctx, 8080)

// Sync ports with the management service
host.RefreshPorts(ctx)

// Block until disconnected (reconnects automatically if enabled)
host.Wait()

// Graceful shutdown
host.Close()
```
131 changes: 131 additions & 0 deletions go/tunnels/examples/host/host_example.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

// This example demonstrates how to use the Go SDK to host a tunnel,
// forwarding a local TCP port to remote clients through the relay.
//
// Prerequisites:
// - A tunnel created via the CLI or management API
// - A host access token for the tunnel
//
// Usage:
// TUNNELS_TOKEN=<host-access-token> go run host_example.go

package main

import (
"context"
"fmt"
"log"
"net/url"
"os"
"os/signal"
"syscall"

tunnels "github.com/microsoft/dev-tunnels/go/tunnels"
)

// Set the tunnel ID and cluster ID for the tunnel you want to host.
const (
hostTunnelID = ""
hostClusterID = "usw2"

// The local port to forward through the tunnel.
localPort = 8080
)

var (
hostURI = tunnels.ServiceProperties.ServiceURI
hostUserAgent = []tunnels.UserAgent{{Name: "Tunnels-Go-SDK-Host-Example", Version: "0.0.1"}}
)

// getHostAccessToken returns the host access token from the TUNNELS_TOKEN
// environment variable.
func getHostAccessToken() string {
if token := os.Getenv("TUNNELS_TOKEN"); token != "" {
return token
}
return ""
}

func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

logger := log.New(os.Stdout, "[host] ", log.LstdFlags)

parsedURL, err := url.Parse(hostURI)
if err != nil {
logger.Fatalf("Failed to parse service URI: %v", err)
}

// Create management client.
mgr, err := tunnels.NewManager(hostUserAgent, getHostAccessToken, parsedURL, nil, "2023-09-27-preview")
if err != nil {
logger.Fatalf("Failed to create manager: %v", err)
}

// Fetch the tunnel with a host access token.
tunnel := &tunnels.Tunnel{
TunnelID: hostTunnelID,
ClusterID: hostClusterID,
}
options := &tunnels.TunnelRequestOptions{
IncludePorts: true,
TokenScopes: []tunnels.TunnelAccessScope{"host"},
}

tunnel, err = mgr.GetTunnel(ctx, tunnel, options)
if err != nil {
logger.Fatalf("Failed to get tunnel: %v", err)
}
logger.Printf("Got tunnel: %s", tunnel.TunnelID)

// Create the host.
host, err := tunnels.NewHost(logger, mgr)
if err != nil {
logger.Fatalf("Failed to create host: %v", err)
}

// Optional: enable automatic reconnection on relay disconnect.
host.EnableReconnect = true

// Optional: log connection status changes.
host.ConnectionStatusChanged = func(prev, curr tunnels.ConnectionStatus) {
logger.Printf("Connection status: %v -> %v", prev, curr)
}

// Connect to the relay.
if err := host.Connect(ctx, tunnel); err != nil {
logger.Fatalf("Failed to connect: %v", err)
}
logger.Printf("Connected to relay")

// Add a port to forward. This registers the port with the management API
// and notifies any connected clients via SSH tcpip-forward.
port := &tunnels.TunnelPort{PortNumber: localPort}
if err := host.AddPort(ctx, port); err != nil {
logger.Fatalf("Failed to add port: %v", err)
}
logger.Printf("Forwarding local port %d", localPort)

// Wait for the relay connection (blocks until disconnect or signal).
// With EnableReconnect=true, this will automatically reconnect on drops.
go func() {
if err := host.Wait(); err != nil {
logger.Printf("Relay connection ended: %v", err)
}
}()

// Wait for interrupt signal.
<-ctx.Done()
logger.Printf("Shutting down...")

// Close gracefully: closes the SSH session and unregisters the endpoint.
if err := host.Close(); err != nil {
logger.Printf("Close error: %v", err)
}
logger.Printf("Host shut down")

fmt.Println("Done.")
}
Loading