SSH Gateway & Tunnel Protocol¶
The SSH gateway provides transparent remote access to leased VMs. Consumers connect via standard SSH, and the gateway routes their session through the libp2p network to the provider's VM — no direct network connectivity to the VM is required.
Architecture¶
Consumer Machine Gateway Node Provider Node VM
│ │ │ │
│ SSH connect │ │ │
│ user=<leaseHash> │ │ │
│───────────────────────►│ │ │
│ │ auth: verify │ │
│ │ AccessPubKey │ │
│ │ │ │
│ │ libp2p stream │ │
│ │ /xe/tunnel/2.0.0 │ │
│ │──────────────────────►│ │
│ │ │ validate │
│ │ │ lease │
│ │ │ │
│ │ │ DialSSH() │
│ │ │──────────────►│
│ │ │ TCP to │
│ │ │ 127.0.0.1: │
│ │ │ {sshPort} │
│ │ │ │
│◄───────────────────────┼───────────────────────┼───────────────│
│ bidirectional byte stream │ │
│◄──────────────────────►│◄─────────────────────►│◄─────────────►│
The consumer never connects directly to the VM. The entire SSH session is tunnelled through the libp2p overlay network, which handles NAT traversal, peer discovery, and encrypted transport.
SSH Gateway¶
The SSH gateway (node/ssh_gateway.go) is a standard SSH server that authenticates consumers using their lease's AccessPubKey and proxies the session to the provider node.
Configuration¶
The gateway is enabled with the -ssh-port flag:
| Flag | Default | Description |
|---|---|---|
-ssh-port |
0 (disabled) | TCP port for the SSH gateway listener |
Host key¶
The gateway generates and persists an ed25519 host key at {dataDir}/ssh_host_key. This key is created on first run and reused across restarts, so the host fingerprint remains stable.
Authentication¶
The SSH username is the 64-character hex lease hash. This tells the gateway which lease the consumer wants to access.
Authentication flow:
- Client connects with
ssh -i <key> -p 2222 <leaseHash>@<host> - Gateway extracts the lease hash from the username field
- Gateway looks up the lease in the store:
- Lease must exist
- Lease must not be settled
- Lease must have a non-empty
AccessPubKey
- Gateway compares the client's presented ed25519 public key against the lease's
AccessPubKey(both as hex-encoded 32-byte keys) - If the key matches, authentication succeeds
Key format
The AccessPubKey is stored as a 64-character hex string representing the raw 32-byte ed25519 public key. This is the same key the consumer passed in the lease block's access_pub_key field. The SSH client must use the corresponding ed25519 private key.
Channel types¶
The gateway supports two SSH channel types:
session— standard shell channel (PTY + shell request). The gateway opens a tunnel and copies bytes between the SSH channel and the tunnel stream.direct-tcpip— TCP forwarding channel (used byssh -W/ ProxyJump). The gateway opens a tunnel and copies bytes without wrapping in a shell session. This allows the consumer's SSH client to negotiate end-to-end with the VM's sshd — no man in the middle.
The direct-tcpip channel is the recommended connection method. It provides end-to-end encryption between the consumer and the VM.
Session handling¶
After authentication, the gateway:
- Accepts the SSH channel (
sessionordirect-tcpip) - Looks up the provider's peer ID via the account directory (
PeerForAccount(lease.Provider)) - Opens a libp2p tunnel to the provider using the lease hash
- Copies bytes bidirectionally between the SSH channel and the tunnel stream
- Closes when either end disconnects
Usage¶
The xe ssh command handles the connection automatically:
This uses ProxyJump internally — the SSH client negotiates end-to-end with the VM's sshd through a direct-tcpip channel on the gateway:
# What xe ssh does internally:
ssh -o ProxyCommand="ssh -p 2222 -i ~/.xe/wallet.lease_key -W %h:%p <leaseHash>@ldn.test.network" \
-i ~/.xe/wallet.lease_key root@vm
SSH key management¶
SSH keys are per-wallet, derived from the wallet file path:
| Wallet path | SSH key path |
|---|---|
~/.xe/wallet.seed |
~/.xe/wallet.lease_key |
/tmp/other.seed |
/tmp/other.lease_key |
Each wallet generates its own SSH key on first xe lease. The key is reused for all leases from that wallet. Different wallets cannot access each other's VMs — the gateway checks the key against the lease's access_pub_key.
Tunnel Protocol¶
The tunnel protocol (net/tunnel.go) is a libp2p stream protocol that proxies TCP connections from gateway nodes to VMs on provider nodes.
Protocol ID¶
Handshake¶
| Step | Direction | Data | Description |
|---|---|---|---|
| 1 | Client → Provider | 32 bytes (lease hash, hex-decoded) | Identifies which VM to connect to |
| 2 | Provider → Client | 1 byte (0x00 = OK, 0x01 = error) | Status response |
| 3 | Provider → Client | Up to 4KB (if error) | Error message string |
After a successful handshake (0x00 status), the stream becomes a bidirectional byte pipe between the gateway's SSH channel and the VM's SSH server.
Provider-side handler¶
SetupTunnelHandler(host, validator, registry) registers a stream handler for /xe/tunnel/2.0.0. The registry is a TunnelRegistry that tracks active tunnels per lease hash so they can be torn down on settlement:
- Set 30-second handshake deadline
- Read 32-byte lease hash from the stream
- Validate the lease via
ValidateTunnelLease(leaseHash, peerID):- Lease must exist in store
- Lease must not be settled
- This node must be the lease's provider
- Any peer can open a tunnel (allows gateway relay — the gateway already authenticated the consumer via SSH)
- Dial the VM's SSH port via
DialSSH(leaseHash):- Returns a
net.Connto127.0.0.1:{sshPort}on the provider host
- Returns a
- Send OK byte (0x00)
- Clear deadline (data phase has no timeout)
- Bidirectional copy between the libp2p stream and the TCP connection to the VM
- Close both sides when either end disconnects
Consumer-side client¶
OpenTunnel(ctx, host, peerstore, leaseHash, providerPeerID):
- Look up provider peer in the DHT if not already in the peerstore
- Open a new libp2p stream to the provider using the tunnel protocol
- Write the 32-byte lease hash (hex-decoded to binary)
- Read the status byte:
0x00→ success, return the stream as anio.ReadWriteCloser0x01→ read error message (up to 4KB), return error
- Clear deadline for the data phase
Validator interface¶
The tunnel handler uses a TunnelValidator interface (implemented by the node):
type TunnelValidator interface {
ValidateTunnelLease(leaseHash string, peerID peer.ID) error
DialSSH(leaseHash string) (net.Conn, error)
}
ValidateTunnelLease checks that the lease exists, is not yet settled, and that this node is the provider. DialSSH opens a TCP connection to the VM's local SSH port via the LimaManager.
End-to-end flow¶
Complete flow for a consumer accessing a leased VM via SSH:
1. Consumer creates lease block with access_pub_key (ed25519 hex)
2. Provider auto-accepts lease, provisions Lima VM with SSH key
3. VM boots Alpine Linux, cloud-init installs openssh and injects key
4. Consumer connects: ssh -i key -p 2222 <leaseHash>@<gateway>
5. Gateway authenticates: matches key against lease's AccessPubKey
6. Gateway opens libp2p tunnel to provider (protocol /xe/tunnel/2.0.0)
7. Provider validates lease, dials VM's local SSH port (127.0.0.1:N)
8. Bidirectional proxy: SSH ↔ gateway ↔ libp2p ↔ provider ↔ VM
9. Consumer has interactive shell inside the Alpine VM
10. On lease settlement, VM is torn down, tunnel becomes invalid
Security properties¶
| Property | Mechanism |
|---|---|
| Authentication | Ed25519 key matching against on-chain AccessPubKey |
| Authorisation | Only the holder of the lease's private key can connect. Keys are per-wallet, preventing cross-wallet access. |
| End-to-end encryption | With ProxyJump/direct-tcpip, the consumer's SSH negotiates directly with the VM's sshd — the gateway only sees encrypted bytes |
| Transport encryption | libp2p noise protocol encrypts the tunnel between gateway and provider |
| Lease validity | Tunnel handler rejects connections to settled or non-existent leases |
| No direct exposure | VM's SSH port is only on localhost; unreachable without the tunnel |
| Handshake timeout | 30-second deadline prevents hanging connections during handshake |
| Connection limit | maxSSHConnections semaphore (100) limits concurrent SSH sessions |
HTTP tunnel endpoint¶
An alternative tunnel mechanism is available via HTTP upgrade at:
This endpoint (api/tunnel.go) hijacks the HTTP connection and proxies it to the provider's tunnel stream, allowing TCP-level access to the VM from HTTP clients. The lease must exist and not be settled. The caller must provide an X-Signature header containing a hex-encoded ed25519 signature of the lease hash, verified against the lease's AccessPubKey.
File reference¶
| File | Description |
|---|---|
node/ssh_gateway.go |
SSH server implementation, authentication, session proxying |
net/tunnel.go |
libp2p tunnel protocol handler and client |
api/tunnel.go |
HTTP tunnel upgrade endpoint |
vm/lima_manager.go |
DialSSH() implementation (TCP dial to VM's local SSH port) |