WebAssembly-based SSH client for the browser with transport-agnostic architecture. Built with Go's golang.org/x/crypto/ssh package and compiled to WASM for browser usage.
- 🔐 SSH client running entirely in the browser via WebAssembly
- 🔌 Transport-agnostic architecture supporting multiple protocols
- 🌐 Built-in support for WebSocket and AWS IoT Secure Tunneling
- 📦 Packet-level send/receive hooks for monitoring and transformation
- 🔄 Support for custom packet transformations (e.g., Protobuf encoding)
- 🔑 Password and private key authentication
- 📘 TypeScript support with full type definitions
- 🚀 ES Module compatible for modern frontend frameworks
graph TB
subgraph "Browser Environment"
A["ES Module<br/>(TypeScript)"]
B["Transport Translation Layer"]
C["WASM Layer<br/>(Go SSH Client)"]
A --> B
B <--> C
end
subgraph "Transport Implementations"
D["WebSocket Transport"]
E["AWS IoT Secure Tunnel<br/>Transport"]
F["Custom Transport<br/>(User-defined)"]
B --> D
B --> E
B --> F
end
subgraph "Network Layer"
G["Direct WebSocket<br/>Connection"]
H["AWS IoT<br/>Secure Tunneling"]
I["Custom Protocol<br/>Endpoint"]
D <--> G
E <--> H
F <--> I
end
subgraph "Destination"
J["SSH Server"]
G --> J
H --> J
I --> J
end
style A fill:#0277bd,color:#fff
style B fill:#7b1fa2,color:#fff
style C fill:#ef6c00,color:#fff
style D fill:#388e3c,color:#fff
style E fill:#388e3c,color:#fff
style F fill:#388e3c,color:#fff
style G fill:#d32f2f,color:#fff
style H fill:#d32f2f,color:#fff
style I fill:#d32f2f,color:#fff
style J fill:#424242,color:#fff
npm install sshclient-wasmnpm install sshclient-wasmCopy WASM files to your public directory:
# Copy these files to your public/ directory:# - sshclient.wasm# - wasm_exec.js// Next.js optimizedimport{initializeSSHClient,SSHClient,NextJSConfig}from"sshclient-wasm/next";// Vite optimizedimport{initializeSSHClient,SSHClient,ViteConfig}from"sshclient-wasm/vite";// React hooks and utilitiesimport{useSSHClient,SSHClient}from"sshclient-wasm/react";// Generic/universal importimport{SSHClient}from"sshclient-wasm";// Framework-specific (auto-optimized)import{initializeSSHClient}from"sshclient-wasm/next";awaitinitializeSSHClient();// Or generic with auto-detectionimport{SSHClient}from"sshclient-wasm";awaitSSHClient.initialize();import{SSHClient,WebSocketTransport}from"sshclient-wasm";// Simple initialization - auto-detects assets in public directoryawaitSSHClient.initialize();// Create a WebSocket transportconsttransport=newWebSocketTransport("transport-1","wss://ssh-gateway.example.com",["ssh"]);// Connect to SSH serverconstsession=awaitSSHClient.connect({host: "example.com",port: 22,user: "username",password: "password",},transport);// Send dataawaitsession.send(newUint8Array([0x01,0x02,0x03]));// Disconnectawaitsession.disconnect();import{initializeSSHClient,NextJSConfig}from"sshclient-wasm/next";import{useEffect,useState}from"react";functionSSHComponent(){const[isReady,setIsReady]=useState(false);useEffect(()=>{// Initialize with Next.js optimizationsinitializeSSHClient().then(()=>setIsReady(true)).catch((error)=>{console.error("Failed to initialize SSH client:",error);// Error messages will guide you to fix asset placement});},[]);if(!isReady)return<div>Loading SSH client...</div>;// Your SSH logic herereturn<div>SSH client ready!</div>;}// Alternative: Use the React hookimport{useSSHClient}from"sshclient-wasm/react";functionSSHComponentWithHook(){const{ isInitialized, initError, isLoading }=useSSHClient();if(isLoading)return<div>Loading SSH client...</div>;if(initError)return<div>Error: {initError.message}</div>;if(!isInitialized)return<div>SSH client not ready</div>;return<div>SSH client ready!</div>;}import{initializeSSHClient,ViteConfig}from"sshclient-wasm/vite";import{useSSHConnection}from"sshclient-wasm/react";functionViteSSHComponent(){const{ connect, disconnect, session, connectionState }=useSSHConnection();useEffect(()=>{initializeSSHClient();},[]);consthandleConnect=async()=>{consttransport=newWebSocketTransport("transport-1","wss://example.com");awaitconnect({host: "example.com",port: 22,user: "username",password: "password"},transport);};return(<div><p>Status: {connectionState}</p><buttononClick={handleConnect}>Connect SSH</button><buttononClick={disconnect}>Disconnect</button></div>);}import{SSHClient,SecureTunnelTransport}from"sshclient-wasm";// Auto-initializeawaitSSHClient.initialize();// Create AWS IoT Secure Tunnel transportconsttransport=newSecureTunnelTransport("tunnel-1",{region: "us-east-1",accessToken: "your-tunnel-access-token",clientMode: "source",serviceId: "SSH",protocol: "V3",});// Connect through the secure tunnelconstsession=awaitSSHClient.connect({host: "internal-server",port: 22,user: "username",privateKey: "ssh-private-key",},transport);// Use the SSH sessionawaitsession.send(encoder.encode("ls -la\n"));// Disconnectawaitsession.disconnect();consttransport=newWebSocketTransport("transport-1","wss://example.com");constsession=awaitSSHClient.connect({host: "example.com",port: 22,user: "username",password: "password",},transport,{onPacketSend: (data,metadata)=>{console.log("Sending packet:",data,metadata);},onPacketReceive: (data,metadata)=>{console.log("Received packet:",data,metadata);},onStateChange: (state)=>{console.log("Connection state:",state);},});import{PacketTransformer}from"sshclient-wasm";// Transform to/from Base64constbase64=PacketTransformer.toBase64(data);constbinary=PacketTransformer.fromBase64(base64);// Custom Protobuf transformation (implement your own logic)constprotobuf=PacketTransformer.toProtobuf(data,schema);1. Copy WASM files to public/:
cp node_modules/sshclient-wasm/dist/sshclient.wasm public/
cp node_modules/sshclient-wasm/dist/wasm_exec.js public/2. Use the Next.js configuration helper:
// next.config.jsimport{NextJSConfig}from"sshclient-wasm/next";/** @type {import('next').NextConfig} */constnextConfig=NextJSConfig.getNextConfig({// Your custom Next.js config here});module.exports=nextConfig;Or configure manually:
/** @type {import('next').NextConfig} */constnextConfig={webpack: (config)=>{config.experiments={ ...config.experiments,asyncWebAssembly: true};returnconfig;},asyncheaders(){return[{source: "/(.*)",headers: [{key: "Cross-Origin-Embedder-Policy",value: "require-corp"},{key: "Cross-Origin-Opener-Policy",value: "same-origin"},],},];},};module.exports=nextConfig;3. Use in your components:
import{initializeSSHClient,useSSHClient}from"sshclient-wasm/next";// Method 1: Direct initializationuseEffect(()=>{initializeSSHClient().then(()=>console.log("SSH client ready")).catch(console.error);},[]);// Method 2: Using the hook (if using sshclient-wasm/react)const{ isInitialized, initError, isLoading }=useSSHClient();1. Copy WASM files to public/:
cp node_modules/sshclient-wasm/dist/sshclient.wasm public/
cp node_modules/sshclient-wasm/dist/wasm_exec.js public/2. Use the Vite configuration helper:
// vite.config.tsimport{defineConfig}from"vite";importreactfrom"@vitejs/plugin-react";import{ViteConfig}from"sshclient-wasm/vite";exportdefaultdefineConfig(ViteConfig.getViteConfig({plugins: [react()],// Your custom Vite config here}));Or configure manually:
import{defineConfig}from"vite";importreactfrom"@vitejs/plugin-react";exportdefaultdefineConfig({plugins: [react()],server: {headers: {"Cross-Origin-Embedder-Policy": "require-corp","Cross-Origin-Opener-Policy": "same-origin",},},});3. Use in your components:
import{initializeSSHClient}from"sshclient-wasm/vite";import{useSSHClient}from"sshclient-wasm/react";// Method 1: Direct initializationuseEffect(()=>{initializeSSHClient().then(()=>console.log("SSH client ready")).catch(console.error);},[]);// Method 2: Using React hooksconst{ isInitialized, initError, isLoading }=useSSHClient();The sshclient-wasm/react module provides React-specific hooks and utilities:
import{useSSHClient,useSSHConnection,SSHClientProvider,withSSHClient}from"sshclient-wasm/react";// Hook for initializationconst{ isInitialized, initError, isLoading }=useSSHClient();// Hook for connection managementconst{ connect, disconnect, send, session, connectionState }=useSSHConnection();// Provider for context<SSHClientProvideroptions={{cacheBusting: false}}><YourApp/></SSHClientProvider>// HOC wrapperconstWrappedComponent=withSSHClient(YourComponent);1. Copy WASM files to your public directory
2. Initialize with custom options:
import{SSHClient}from"sshclient-wasm";awaitSSHClient.initialize({publicDir: "/assets/",// Your public directory pathautoDetect: true,cacheBusting: process.env.NODE_ENV==="development",});import{SSHClient}from"sshclient-wasm";awaitSSHClient.initialize({wasmPath: "/custom/path/sshclient.wasm",wasmExecPath: "/custom/path/wasm_exec.js",autoDetect: false,cacheBusting: false,timeout: 15000,});import{SSHClientHelpers}from"sshclient-wasm";// Test if assets are properly placedconst{ wasmAvailable, wasmExecAvailable }=awaitSSHClientHelpers.testAssetAvailability("/sshclient.wasm","/wasm_exec.js");if(!wasmAvailable){console.error("❌ Please copy sshclient.wasm to your public directory");}if(!wasmExecAvailable){console.error("❌ Please copy wasm_exec.js to your public directory");}try{awaitSSHClient.initialize();}catch(error){if(error.message.includes("WASM file not found")){console.error("Copy sshclient.wasm to public/ directory");}elseif(error.message.includes("wasm_exec.js not found")){console.error("Copy wasm_exec.js to public/ directory");}elseif(error.message.includes("Failed to fetch WASM")){console.error("Check network connection and CORS headers");}else{console.error("Initialization failed:",error.message);}}- Go 1.21+
- Node.js 16+
- Make
# Clone the repository
git clone https://github.com/andrew/sshclient-wasm.git
cd sshclient-wasm
# Install dependencies
go mod download
npm install
# Build WASM and TypeScript
make build
# Run example
make devsshclient-wasm/
├── main.go # WASM entry point
├── pkg/sshclient/ # Go SSH client implementation
│ ├── client.go # Main client logic
│ └── interceptor.go # Packet interception
├── lib/ # TypeScript/JavaScript bindings
│ └── index.ts # Main TypeScript API
├── example/ # Example application
└── dist/ # Build output
The main entry point for establishing SSH connections through WebAssembly.
classSSHClient{/** * Initialize the WASM module. Must be called before any other methods. * @param options - Initialization options or legacy string path */staticasyncinitialize(options?: InitializationOptions|string): Promise<void>;/** * Connect to an SSH server through a transport * @param options - SSH connection configuration * @param transport - Transport implementation for network communication * @param callbacks - Optional callbacks for monitoring packet flow * @returns SSH session handle for sending data and disconnecting */staticasyncconnect(options: ConnectionOptions,transport: Transport,callbacks?: SSHClientCallbacks): Promise<SSHSession>;/** * Disconnect a specific SSH session * @param sessionId - The ID of the session to disconnect */staticasyncdisconnect(sessionId: string): Promise<void>;/** * Get the library version * @returns Version string */staticgetVersion(): string;}Utilities for transforming SSH packet data.
classPacketTransformer{/** * Convert binary data to Base64 encoding * @param data - Binary data to encode * @returns Base64 encoded string */statictoBase64(data: Uint8Array): string;/** * Convert Base64 string to binary data * @param base64 - Base64 encoded string * @returns Decoded binary data */staticfromBase64(base64: string): Uint8Array;/** * Transform binary data to Protobuf format (user-implemented) * @param data - Binary data to transform * @param schema - Protobuf schema definition * @returns Protobuf encoded data */statictoProtobuf(data: Uint8Array,schema?: any): Uint8Array;/** * Transform Protobuf data to binary format (user-implemented) * @param data - Protobuf encoded data * @param schema - Protobuf schema definition * @returns Decoded binary data */staticfromProtobuf(data: Uint8Array,schema?: any): Uint8Array;}Direct WebSocket connection to SSH servers.
classWebSocketTransportimplementsTransport{/** * Create a WebSocket transport * @param id - Unique identifier for this transport * @param url - WebSocket URL (e.g., 'wss://ssh-gateway.example.com') * @param protocols - Optional WebSocket subprotocols */constructor(id: string,url: string,protocols?: string[]);asyncconnect(): Promise<void>;asyncdisconnect(): Promise<void>;asyncsend(data: Uint8Array): Promise<void>;}AWS IoT Secure Tunneling transport for end-to-end encrypted connections.
classSecureTunnelTransportimplementsTransport{/** * Create an AWS IoT Secure Tunnel transport * @param id - Unique identifier for this transport * @param config - Tunnel configuration */constructor(id: string,config: SecureTunnelConfig);asyncconnect(): Promise<void>;asyncdisconnect(): Promise<void>;asyncsend(data: Uint8Array): Promise<void>;}User-defined transport for custom protocols.
classCustomTransportimplementsTransport{/** * Create a custom transport with user-provided implementations * @param id - Unique identifier for this transport * @param connectImpl - Custom connection implementation * @param disconnectImpl - Custom disconnection implementation * @param sendImpl - Custom send implementation */constructor(id: string,connectImpl?: ()=>Promise<void>,disconnectImpl?: ()=>Promise<void>,sendImpl?: (data: Uint8Array)=>Promise<void>);/** * Inject received data into the transport * @param data - Data received from the custom protocol */injectData(data: Uint8Array): void;}Framework-specific helpers and utilities.
classSSHClientHelpers{/** * Get recommended asset paths for the detected framework */staticgetAssetPaths: (publicDir?: string)=>{wasmPath: string;wasmExecPath: string;};/** * Detect the current framework */staticdetectFramework: ()=>"nextjs"|"vite"|"webpack"|"generic";/** * Test if WASM assets are available at the given paths */statictestAssetAvailability: (wasmPath: string,wasmExecPath: string)=>Promise<{wasmAvailable: boolean;wasmExecAvailable: boolean;}>;/** * Next.js specific initialization helper */staticinitializeForNextJS: (options?: Partial<InitializationOptions>)=>Promise<void>;/** * Vite specific initialization helper */staticinitializeForVite: (options?: Partial<InitializationOptions>)=>Promise<void>;/** * Generic initialization with sensible defaults */staticinitializeWithDefaults: (customOptions?: Partial<InitializationOptions>)=>Promise<void>;}Configuration options for initializing the SSH client.
interfaceInitializationOptions{/** Path to the WASM file (default: auto-detected) */wasmPath?: string;/** Path to the wasm_exec.js file (default: auto-detected) */wasmExecPath?: string;/** Enable automatic path detection (default: true) */autoDetect?: boolean;/** Public directory path for auto-detection (default: '/') */publicDir?: string;/** Enable cache busting for development (default: true) */cacheBusting?: boolean;/** Timeout for loading assets in milliseconds (default: 10000) */timeout?: number;}Base interface for all transport implementations.
interfaceTransport{id: string;connect(): Promise<void>;disconnect(): Promise<void>;send(data: Uint8Array): Promise<void>;// Event handlers (set by the library)onData?: (data: Uint8Array)=>void;onError?: (error: Error)=>void;onClose?: ()=>void;}SSH connection configuration.
interfaceConnectionOptions{/** Target SSH server hostname */host: string;/** SSH server port (default: 22) */port: number;/** SSH username */user: string;/** Password for authentication (optional) */password?: string;/** Private key for authentication (optional) */privateKey?: string;/** Connection timeout in milliseconds (optional) */timeout?: number;}AWS IoT Secure Tunnel configuration.
interfaceSecureTunnelConfig{/** AWS region where the tunnel is created */region: string;/** Tunnel access token */accessToken: string;/** Client mode: 'source' or 'destination' */clientMode: "source"|"destination";/** Service identifier for multiplexed tunnels (optional) */serviceId?: string;/** Protocol version: 'V2' or 'V3' (default: 'V2') */protocol?: "V2"|"V3";}SSH session handle returned from connect().
interfaceSSHSession{/** Unique session identifier */sessionId: string;/** * Send data through the SSH connection * @param data - Binary data to send */send(data: Uint8Array): Promise<void>;/** * Close the SSH connection */disconnect(): Promise<void>;}Optional callbacks for monitoring SSH packet flow.
interfaceSSHClientCallbacks{/** * Called when sending an SSH packet * @param data - Raw packet data being sent * @param metadata - Packet metadata (type, size, etc.) */onPacketSend?: (data: Uint8Array,metadata: PacketMetadata)=>void;/** * Called when receiving an SSH packet * @param data - Raw packet data received * @param metadata - Packet metadata (type, size, etc.) */onPacketReceive?: (data: Uint8Array,metadata: PacketMetadata)=>void;/** * Called when SSH connection state changes * @param state - New connection state */onStateChange?: (state: SSHConnectionState)=>void;}Metadata about SSH packets.
interfacePacketMetadata{/** Packet type identifier */packetType: string;/** Packet type code */packetTypeCode: number;/** Packet size in bytes */size: number;/** Timestamp of packet */timestamp?: number;}SSH connection states.
typeSSHConnectionState=|"connecting"|"connected"|"authenticating"|"authenticated"|"ready"|"disconnecting"|"disconnected"|"error";AWS IoT Secure Tunnel message types.
enumTunnelMessageType{UNKNOWN=0,DATA=1,STREAM_START=2,STREAM_RESET=3,SESSION_RESET=4,SERVICE_IDS=5,CONNECTION_START=6,CONNECTION_RESET=7,}# Solution: Copy WASM files to public directory
cp node_modules/sshclient-wasm/dist/sshclient.wasm public/
cp node_modules/sshclient-wasm/dist/wasm_exec.js public/- Verify files are in your public directory
- Check that your web server serves static files from public/
- Ensure file paths are correct (case-sensitive on some servers)
- Ensure
wasm_exec.jsis accessible and loads successfully - Check browser console for script loading errors
- Verify CORS headers allow script loading
Next.js users: Add CORS headers to next.config.js:
asyncheaders(){return[{source: '/(.*)',headers: [{key: 'Cross-Origin-Embedder-Policy',value: 'require-corp'},{key: 'Cross-Origin-Opener-Policy',value: 'same-origin'},],}];}Vite users: Add to vite.config.ts:
server: {headers: {'Cross-Origin-Embedder-Policy': 'require-corp','Cross-Origin-Opener-Policy': 'same-origin',},}- Your browser may not support WebAssembly
- WASM file may be corrupted or incompatible
- Try clearing browser cache
import{SSHClientHelpers}from"sshclient-wasm";constresult=awaitSSHClientHelpers.testAssetAvailability("/sshclient.wasm","/wasm_exec.js");console.log("WASM available:",result.wasmAvailable);console.log("Exec available:",result.wasmExecAvailable);import{SSHClientHelpers}from"sshclient-wasm";console.log("Detected framework:",SSHClientHelpers.detectFramework());console.log("Recommended paths:",SSHClientHelpers.getAssetPaths());All async methods throw errors that can be caught:
try{awaitSSHClient.initialize();awaitSSHClient.connect(options,transport);}catch(error){if(error.message.includes("WASM file not found")){console.error("📁 Copy sshclient.wasm to public/ directory");}elseif(error.message.includes("wasm_exec.js not found")){console.error("📁 Copy wasm_exec.js to public/ directory");}elseif(error.message.includes("Failed to fetch WASM")){console.error("🌐 Check network connection and CORS headers");}elseif(error.message.includes("authentication")){console.error("🔐 Check SSH credentials");}elseif(error.message.includes("timeout")){console.error("⏰ Connection or initialization timeout");}else{console.error("❌ Unknown error:",error.message);}}- Cache Busting: Disable in production for better performance
- Asset Preloading: Consider preloading WASM files for faster initialization
- Memory Usage: Monitor memory usage for long-running sessions
- Connection Pooling: Reuse transports when possible
constsession=awaitSSHClient.connect(connectionOptions,transport,{onPacketSend: (data,metadata)=>{// Transform outgoing packetsconsttransformed=PacketTransformer.toProtobuf(data,mySchema);console.log(`Sending ${metadata.packetType} packet (${metadata.size} bytes)`);returntransformed;},onPacketReceive: (data,metadata)=>{// Process incoming packetsconstdecoded=PacketTransformer.fromProtobuf(data,mySchema);console.log(`Received ${metadata.packetType} packet`);returndecoded;},});classMyCustomTransportextendsCustomTransport{constructor(id: string,config: MyConfig){super(id,async()=>{// Custom connection logicawaitthis.establishConnection(config);},async()=>{// Custom disconnection logicawaitthis.closeConnection();},async(data: Uint8Array)=>{// Custom send logicawaitthis.sendData(data);});}privateasyncestablishConnection(config: MyConfig){// Implementation specific to your protocol}// Call this.injectData() when receiving data from your protocolhandleIncomingData(data: Uint8Array){this.injectData(data);}}BSD-3-Clause