Skip to content

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:

xe node -provide -ssh-port 2222
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:

  1. Client connects with ssh -i <key> -p 2222 <leaseHash>@<host>
  2. Gateway extracts the lease hash from the username field
  3. Gateway looks up the lease in the store:
    • Lease must exist
    • Lease must not be settled
    • Lease must have a non-empty AccessPubKey
  4. Gateway compares the client's presented ed25519 public key against the lease's AccessPubKey (both as hex-encoded 32-byte keys)
  5. 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 by ssh -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:

  1. Accepts the SSH channel (session or direct-tcpip)
  2. Looks up the provider's peer ID via the account directory (PeerForAccount(lease.Provider))
  3. Opens a libp2p tunnel to the provider using the lease hash
  4. Copies bytes bidirectionally between the SSH channel and the tunnel stream
  5. Closes when either end disconnects

Usage

The xe ssh command handles the connection automatically:

xe ssh <leaseHash>

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

/xe/tunnel/2.0.0

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:

  1. Set 30-second handshake deadline
  2. Read 32-byte lease hash from the stream
  3. 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)
  4. Dial the VM's SSH port via DialSSH(leaseHash):
    • Returns a net.Conn to 127.0.0.1:{sshPort} on the provider host
  5. Send OK byte (0x00)
  6. Clear deadline (data phase has no timeout)
  7. Bidirectional copy between the libp2p stream and the TCP connection to the VM
  8. Close both sides when either end disconnects

Consumer-side client

OpenTunnel(ctx, host, peerstore, leaseHash, providerPeerID):

  1. Look up provider peer in the DHT if not already in the peerstore
  2. Open a new libp2p stream to the provider using the tunnel protocol
  3. Write the 32-byte lease hash (hex-decoded to binary)
  4. Read the status byte:
    • 0x00 → success, return the stream as an io.ReadWriteCloser
    • 0x01 → read error message (up to 4KB), return error
  5. 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:

POST /tunnel/{leaseHash}/tcp

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)