go

module
v0.0.0-...-5cb2dda Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 5, 2026 License: MIT

README

Dev Tunnels SSH Library for Go

A Secure Shell (SSH2) client and server protocol implementation for Go.

Feature Highlights

  • SSH over any io.ReadWriteCloser (including but not limited to TCP sockets)
  • Configurable, extensible, negotiated algorithms for key exchange, encryption, integrity (HMAC), and public-key authentication
  • Channel multiplexing, with ability to stream data to/from channels
  • Extensibility for handling custom session requests and channel requests
  • Session reconnection over new connections (preserving channels and state)
  • Port forwarding (local-to-remote and remote-to-local)
  • Session piping for relay/proxy scenarios
  • Non-blocking per-channel request dispatch
  • Compatible with common SSH software (tested against OpenSSH, C#, and TypeScript implementations)

Requirements

  • Go 1.17+
  • No external dependencies (standard library only)

Crypto algorithms use Go's standard crypto/* packages, which delegate to platform-native implementations (OpenSSL on Linux, Security.framework on macOS, CNG on Windows).

Packages

Package Import Path Description
ssh github.com/microsoft/dev-tunnels-ssh/src/go/ssh Core SSH protocol — sessions, channels, auth, config
keys github.com/microsoft/dev-tunnels-ssh/src/go/keys Key import/export (PKCS#1, PKCS#8, SEC1, OpenSSH, SSH2, JWK)
tcp github.com/microsoft/dev-tunnels-ssh/src/go/tcp TCP client/server wrappers and port forwarding service

Basic Examples

Note: the client and server examples below use the tcp.Client and tcp.Server convenience wrappers from the tcp package. For SSH over non-TCP streams, use ssh.NewClientSession / ssh.NewServerSession directly with any io.ReadWriteCloser.

Client example

This example connects to an SSH server, authenticates with a password, opens a channel, sends a command, and reads the result.

package main

import (
	"context"
	"fmt"
	"io"
	"log"

	"github.com/microsoft/dev-tunnels-ssh/src/go/ssh"
	"github.com/microsoft/dev-tunnels-ssh/src/go/tcp"
)

func main() {
	client := tcp.NewClient(ssh.NewDefaultConfig())
	defer client.Close()

	ctx := context.Background()
	session, err := client.OpenSession(ctx, "localhost", 2222)
	if err != nil {
		log.Fatal(err)
	}

	// Handle server public key verification.
	session.OnAuthenticating = func(args *ssh.AuthenticatingEventArgs) {
		// TODO: Validate the server's public key (args.PublicKey).
		// Return nil (don't set AuthenticationResult) to reject.
		args.AuthenticationResult = struct{}{}
	}

	// Authenticate with username and password.
	authenticated, err := session.Authenticate(ctx, &ssh.ClientCredentials{
		Username: "user",
		Password: "password",
	})
	if err != nil {
		log.Fatal(err)
	}
	if !authenticated {
		log.Fatal("authentication failed")
	}

	// Open a channel, send a request, and read the response.
	channel, err := session.OpenChannel(ctx)
	if err != nil {
		log.Fatal(err)
	}

	// Read data from the channel.
	channel.SetDataReceivedHandler(func(data []byte) {
		fmt.Printf("Received: %s\n", data)
		channel.AdjustWindow(uint32(len(data)))
	})

	// Send data.
	if err := channel.Send(ctx, []byte("hello")); err != nil {
		log.Fatal(err)
	}

	// Wait for the session to end.
	<-session.Done()
}
Server example

This example runs an SSH server that authenticates clients and echoes channel data back to them.

package main

import (
	"context"
	"log"

	"github.com/microsoft/dev-tunnels-ssh/src/go/ssh"
	"github.com/microsoft/dev-tunnels-ssh/src/go/tcp"
)

func main() {
	server := tcp.NewServer(ssh.NewDefaultConfig())

	// Generate a host key for server authentication.
	hostKey, err := ssh.GenerateKeyPair(ssh.AlgoPKEcdsaSha2P384)
	if err != nil {
		log.Fatal(err)
	}
	server.Credentials = &ssh.ServerCredentials{
		PublicKeys: []ssh.KeyPair{hostKey},
	}

	// Handle client authentication.
	server.OnSessionAuthenticating = func(args *ssh.AuthenticatingEventArgs) {
		// TODO: Validate credentials based on args.AuthenticationType.
		// For password auth: check args.Username and args.Password.
		// For public key auth: check args.PublicKey.
		// Set AuthenticationResult to non-nil to approve, leave nil to reject.
		args.AuthenticationResult = struct{}{}
	}

	// Handle new sessions — set up channel echo.
	server.OnSessionOpened = func(session *ssh.ServerSession) {
		go func() {
			ctx := context.Background()
			for {
				ch, err := session.AcceptChannel(ctx)
				if err != nil {
					return // session closed
				}
				go echoChannel(ch)
			}
		}()
	}

	// Listen on port 2222 (blocks until context cancelled or Close called).
	ctx := context.Background()
	if err := server.AcceptSessions(ctx, 2222, ""); err != nil {
		log.Fatal(err)
	}
}

func echoChannel(ch *ssh.Channel) {
	ch.SetDataReceivedHandler(func(data []byte) {
		_ = ch.Send(context.Background(), data)
		ch.AdjustWindow(uint32(len(data)))
	})
}
Stream-based (non-TCP) example

SSH sessions can run over any io.ReadWriteCloser, not just TCP:

// Create sessions with no TCP involved.
clientSession := ssh.NewClientSession(ssh.NewDefaultConfig())
serverSession := ssh.NewServerSession(ssh.NewDefaultConfig())

// Connect over any bidirectional stream (pipes, WebSockets, etc.).
go serverSession.Connect(ctx, serverStream)
clientSession.Connect(ctx, clientStream)

Extensibility

This library prioritizes flexibility over completeness; if something SSH-related is not implemented directly in the library, there is generally a way to plug in that support without changing the library itself.

Algorithms

Algorithms for an SSH session are configured using SessionConfig. Algorithm lists are in preference order — client and server negotiate the most-preferred algorithm supported by both.

config := ssh.NewDefaultConfig()

// Restrict to specific algorithms.
config.KeyExchangeAlgorithms = []string{
	ssh.AlgoKexEcdhNistp384,
	ssh.AlgoKexEcdhNistp256,
}
config.EncryptionAlgorithms = []string{
	ssh.AlgoEncAes256Gcm,
}

Three built-in configurations are available:

  • NewDefaultConfig() — secure defaults, recommended for production
  • NewDefaultConfigWithReconnect() — adds reconnection protocol extensions
  • NewNoSecurityConfig() — all "none" algorithms, for testing only
Authentication

Two-way authentication is supported. A server or client MUST handle the OnAuthenticating callback to confirm authentication, otherwise authentication fails. For public keys, the library verifies the cryptographic signature automatically. The callback decides whether to accept the identity.

// Server: validate client credentials.
session.OnAuthenticating = func(args *ssh.AuthenticatingEventArgs) {
	switch args.AuthenticationType {
	case ssh.AuthClientPassword:
		if args.Username == "admin" && args.Password == "secret" {
			args.AuthenticationResult = struct{}{}
		}
	case ssh.AuthClientPublicKey:
		// args.PublicKey contains the verified key — check against allowed keys.
		args.AuthenticationResult = struct{}{}
	}
}

// Client: authenticate with public key.
authenticated, err := session.Authenticate(ctx, &ssh.ClientCredentials{
	Username:   "admin",
	PublicKeys: []ssh.KeyPair{privateKey},
})
Key Management

The library does not implement any key management scheme, though the keys package can import and export RSA and ECDSA keys in many formats:

import "github.com/microsoft/dev-tunnels-ssh/src/go/keys"

// Import a key from PEM data (auto-detects format).
keyPair, err := keys.ImportKey(pemBytes, "passphrase")

// Import from file.
keyPair, err := keys.ImportKeyFile("/path/to/key.pem", "")

// Export to PKCS#8 PEM.
pemData, err := keys.ExportPrivateKey(keyPair, keys.KeyFormatPkcs8, "passphrase")

// Export public key in SSH wire format.
pubData, err := keys.ExportPublicKey(keyPair, keys.KeyFormatSSH)

Supported formats: PKCS#1, PKCS#8, SEC1, OpenSSH, SSH2 (RFC 4716), JWK.

Channel Requests

A client or server can handle Channel.OnRequest to process custom channel requests. The request can be inspected and authorized:

serverCh.SetRequestHandler(func(args *ssh.RequestEventArgs) {
	if args.RequestType == "my-custom-request" {
		args.IsAuthorized = true
		// Process the request...
	}
})

// Client sends a custom request.
success, err := clientCh.Request(ctx, &messages.ChannelRequestMessage{
	RequestType: "my-custom-request",
	WantReply:   true,
})
Services

Custom services can be registered on the session configuration to be activated automatically when matching requests or channels are received:

config.AddService("my-service", ssh.ServiceActivation{
	SessionRequest: "my-service",
}, func(session *ssh.Session, cfg interface{}) ssh.Service {
	return &MyService{session: session}
}, nil)

The port forwarding implementation in the tcp package is built on this extensibility mechanism.

Custom Message Handlers

The Go implementation exposes MessageHandlers on SessionConfig, allowing applications to handle arbitrary SSH message types:

config.MessageHandlers = map[byte]ssh.MessageHandler{
	200: func(payload []byte) error {
		// Handle custom message type 200.
		return nil
	},
}

Port Forwarding

The tcp package provides SSH port forwarding (RFC 4254 sections 6-7):

import "github.com/microsoft/dev-tunnels-ssh/src/go/tcp"

// Register port forwarding on both client and server configs.
config := ssh.NewDefaultConfig()
tcp.AddPortForwardingService(config)

// After connecting, get the port forwarding service.
pfs := tcp.GetPortForwardingService(&session.Session)

// Stream to a remote port (direct-tcpip channel).
stream, err := pfs.StreamToRemotePort(ctx, "127.0.0.1", 8080)

// Forward a local port to a remote port.
fwd, err := pfs.ForwardToRemotePort(ctx, 3000, "127.0.0.1", 8080)
defer fwd.Close()

// Forward a remote port to a local port.
err = pfs.ForwardFromRemotePort(ctx, 8080, "127.0.0.1", 3000)

Reconnection

Sessions can reconnect over a new stream after a connection loss, preserving all channels and state:

// Use reconnect-enabled config.
config := ssh.NewDefaultConfigWithReconnect()

// Server: share reconnectable sessions across connections.
server := tcp.NewServer(config)

// Client: reconnect after connection loss.
err := client.ReconnectSession(ctx, session, host, port)

Piping

An SSH server can create a "pipe" between two sessions to support relay scenarios. Once piped, messages from one client are forwarded to the other:

// Pipe two sessions bidirectionally.
err := ssh.PipeSession(ctx, sessionA, sessionB)

// Or pipe individual channels.
err := channelA.Pipe(ctx, channelB)

Piped messages include session requests, channel open requests, channel requests (with payloads), channel data, and channel close. Each session is independently authenticated and encrypted — the relay decrypts and re-encrypts when forwarding.

Algorithms

Category Algorithms
Key Exchange ecdh-sha2-nistp521, ecdh-sha2-nistp384, ecdh-sha2-nistp256, diffie-hellman-group16-sha512, diffie-hellman-group14-sha256
Public Key rsa-sha2-512, rsa-sha2-256, ecdsa-sha2-nistp521, ecdsa-sha2-nistp384, ecdsa-sha2-nistp256
Encryption aes256-gcm@openssh.com, aes256-cbc, aes256-ctr
HMAC hmac-sha2-512-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512, hmac-sha2-256
Compression none

All algorithm lists support a "none" entry for no-security testing.

Cross-Implementation Compatibility

This Go library is a full-parity implementation alongside the C# and TypeScript versions of Dev Tunnels SSH. All three share:

  • Identical algorithm negotiation and preference ordering
  • Same protocol extensions (session-reconnect, session-latency, open-channel-request)
  • Same authentication flow and event model
  • Wire-compatible port forwarding, piping, and reconnection

Cross-implementation interoperability is validated by ~48 E2E tests that run Go, C#, and TypeScript clients and servers against each other over real TCP.

Future Work

Compression

Currently only the "none" compression algorithm is implemented. Adding "zlib" support would be straightforward, though compression at the SSH protocol level is generally considered to have limited value in most scenarios.

Shell / Terminal Support

The library doesn't currently offer built-in support for executing commands or starting a persistent shell in the host OS on the server side, or for integrating with a terminal on the client side.

Directories

Path Synopsis
Package keys provides SSH key generation, import, and export for Dev Tunnels.
Package keys provides SSH key generation, import, and export for Dev Tunnels.
ssh
Package ssh provides the core SSH2 protocol implementation for Microsoft Dev Tunnels.
Package ssh provides the core SSH2 protocol implementation for Microsoft Dev Tunnels.
algorithms
Package algorithms provides cryptographic algorithm implementations for SSH key exchange, encryption, and message authentication.
Package algorithms provides cryptographic algorithm implementations for SSH key exchange, encryption, and message authentication.
io
Package sshio provides binary data readers and writers for the SSH wire protocol.
Package sshio provides binary data readers and writers for the SSH wire protocol.
messages
Package messages defines SSH protocol message types and their serialization.
Package messages defines SSH protocol message types and their serialization.
Package tcp provides TCP client/server wrappers and port forwarding for Dev Tunnels SSH.
Package tcp provides TCP client/server wrappers and port forwarding for Dev Tunnels SSH.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL