Croc's Relay Communication Protocol Internals: How the PAKE Handshake and Room-Based Pairing Work

Croc's relay communication protocol uses a lightweight TCP server to perform Password-Authenticated Key Exchange (PAKE), derive strong symmetric keys, and pair two clients into an opaque byte-streaming "room" without ever inspecting the transferred payload.

Croc's relay communication protocol powers the secure file transfer tool in the schollz/croc repository by acting as a cryptographic coordinator rather than a data processor. The relay, implemented primarily in src/tcp/tcp.go, handles client authentication, NAT traversal signaling, and bidirectional byte piping while remaining blind to the actual file contents.

PAKE Handshake and Initial Connection

When a client connects to the relay, the server initiates a cryptographic handshake in the clientCommunication function. The process begins with the client sending its PAKE initiation value A, which the relay receives and processes using the SIEC curve.

In src/tcp/tcp.go (lines 78-100), the relay computes its response value B using the weak passphrase and the SIEC elliptic curve:

// 1️⃣  Read A from the client
Abytes, err := c.Receive()
...
// 2️⃣  Compute B and send it back
B, err := pake.InitCurve(weakKey, 1, "siec")
...
err = c.Send(B.Bytes())

Both endpoints then derive an identical strongKey via B.SessionKey(), establishing a symmetric encryption key that protects all subsequent handshake messages.

Password Verification and Session Encryption

After the PAKE exchange, the relay authenticates the client using the derived strong key. The client encrypts its password with strongKeyForEncryption, and the server decrypts and validates it against the relay's configured password (default "pass123").

This verification occurs in src/tcp/tcp.go (lines 119-133):

// 3️⃣  Receive encrypted password
passwordBytesEnc, err := c.Receive()
passwordBytes, err := crypt.Decrypt(passwordBytesEnc, strongKeyForEncryption)
if strings.TrimSpace(string(passwordBytes)) != strings.TrimSpace(s.password) {
    // mismatch → send encrypted error and abort
}

A mismatch results in immediate termination, while successful validation proceeds to the banner exchange phase.

Once authenticated, the relay transmits a banner containing the client's external IP address and available relay ports. This banner enables NAT traversal by informing both peers about network endpoints.

The server constructs the banner in src/tcp/tcp.go (lines 137-147) using the format banner|||<client-IP>:

bSend, err := crypt.Encrypt([]byte(banner+"|||"+c.Connection().RemoteAddr().String()),
                           strongKeyForEncryption)
err = c.Send(bSend)

This encrypted payload tells the client which ports (c.Options.RelayPorts) to use for the actual data transfer and provides the external IP address visible to the other peer.

Room Creation and Peer Pairing

Following the banner exchange, the client submits a room identifier—a random string that determines pairing. The relay maintains a thread-safe map (roomMap) tracking room states.

In src/tcp/tcp.go (lines 152-186), the relay handles room assignment:

roomBytes, err := crypt.Decrypt(enc, strongKeyForEncryption)
room = string(roomBytes)

s.rooms.Lock()
if _, ok := s.rooms.rooms[room]; !ok {
    // first participant – store connection and reply "ok"
    s.rooms.rooms[room] = roomInfo{first: c, opened: time.Now()}
    // …
    return
}
if s.rooms.rooms[room].full {
    // room already has two participants → reject
}

When a second client joins an existing room, the relay marks it as full, retrieves the first connection, and establishes a full-duplex pipe. The piping logic (lines 238-260) launches goroutines copying bytes bidirectionally:

otherConnection := s.rooms.rooms[room].first
go func(com1, com2 *comm.Comm) {
    pipe(com1.Connection(), com2.Connection())
}(otherConnection, c)

The pipe function (lines 87-112) continues streaming raw bytes until either connection closes.

Connection Teardown and Cleanup

When a transfer completes or a connection drops, the relay invokes deleteRoom to release resources. This function, located at lines 390-403 in src/tcp/tcp.go, closes both underlying TCP connections and removes the room entry from the map to prevent memory leaks.

WebRelay Bridge for Browser Clients

For browser-based transfers, Croc includes a WebSocket-to-TCP bridge in src/webrelay/webrelay.go. This component accepts WebSocket connections on /ws?port=<allowed-port> and forwards traffic to the upstream TCP relay.

The bridge validates requests (lines 31-45) and uses copyStream (lines 71-89) to shuttle data bidirectionally between the WebSocket client and the TCP relay:

// Bridge simply forwards bytes; PAKE and encryption happen inside the TCP relay

Unlike the TCP relay, the WebRelay performs no cryptographic handshaking—it merely transports the already-encrypted byte stream between the browser and the core relay server.

Practical Implementation Examples

Connecting via the Go Library

You can interact with the relay directly using the tcp package:

package main

import (
	"log"
	"time"

	"github.com/schollz/croc/v10/src/tcp"
)

func main() {
	// Connect to the public relay (default host & port)
	relay := "croc.schollz.com:9009"
	password := "pass123"
	room := "my‑unique‑room"

	// Establish a secure PAKE‑based connection
	c, banner, ip, err := tcp.ConnectToTCPServer(relay, password, room,
		5*time.Second) // optional timeout
	if err != nil {
		log.Fatalf("relay connect failed: %v", err)
	}
	defer c.Close()

	log.Printf("Connected! Banner: %s, External IP: %s", banner, ip)

	// From here you can send/receive raw bytes – Croc will pipe them to the peer.
	// Example: send a short message
	_, _ = c.Send([]byte("hello from client"))
}

The tcp.ConnectToTCPServer function (lines 37-49) handles the complete PAKE handshake, password verification, and room registration automatically.

Running a Local Relay

For testing or private networks, run a standalone relay:


# Start a local relay listening on 0.0.0.0:9100

croc relay --host 0.0.0.0 --ports 9100 --relay-password secret

The CLI parses these flags and initializes the TCP server via src/croc/croc.go (lines 147-155).

Browser Client via WebRelay

To enable web-based transfers:


# Serve the web UI + bridge on localhost:9014

croc webrelay --listen-address 127.0.0.1:9014 \
    --relay croc.schollz.com --relay-password pass123

Navigate to http://127.0.0.1:9014 to access the UI, which connects via WebSocket to the bridge at /ws?port=9009.

Key Source Files

The relay protocol spans several critical files in the schollz/croc repository:

  • src/tcp/tcp.go – Core TCP relay server implementing PAKE handshake, password verification, room management, and bidirectional piping.
  • src/webrelay/webrelay.go – HTTP server serving the static UI and implementing the WebSocket-to-TCP bridge for browser clients.
  • src/comm/comm.go – Communication wrapper around net.Conn providing Send and Receive primitives used throughout the relay.
  • src/crypt/crypt.go – Symmetric encryption utilities (Encrypt, Decrypt) protecting handshake messages and payloads.
  • src/croc/croc.go – High-level client orchestration coordinating CLI commands with relay connections and data transfer logic.

Summary

  • PAKE Handshake: The relay uses pake.InitCurve with the SIEC curve to derive strong symmetric keys from weak passwords, implemented in src/tcp/tcp.go.
  • Password Verification: Clients encrypt passwords using the PAKE-derived key; the relay validates against its configured secret before allowing room access.
  • Room-Based Pairing: The relay maintains a roomMap where the first connecting client waits until a second client joins the same room, at which point it creates a full-duplex byte pipe.
  • Opaque Relay: The server never interprets transferred data—it only encrypts handshakes and pipes raw bytes between paired connections.
  • WebSocket Bridge: Browser clients connect through src/webrelay/webrelay.go, which forwards WebSocket traffic to the TCP relay without performing cryptographic operations.

Frequently Asked Questions

What encryption algorithm does Croc's relay use for the initial handshake?

Croc's relay communication protocol uses Password-Authenticated Key Exchange (PAKE) with the SIEC (Simplified Isogeny Elliptic Curve) implementation. The pake.InitCurve function generates ephemeral public keys that allow both parties to derive an identical strong symmetric key without transmitting the actual password over the network.

How does the relay handle NAT traversal between two peers?

During the banner exchange phase, the relay transmits the client's external IP address (extracted from c.Connection().RemoteAddr()) back to the client encrypted within the banner payload. This allows both peers to discover their public-facing endpoints and coordinate direct connections if possible, while falling back to the relay's byte-piping mechanism when necessary.

What happens if a third client tries to join an existing room?

According to the room management logic in src/tcp/tcp.go, the relay checks if a room is already marked as full when a new connection attempts to join. If two participants already occupy the room, the relay rejects the third connection attempt, ensuring that only two endpoints can communicate through any given room identifier.

Does the WebRelay bridge perform any encryption or authentication?

No. The WebRelay bridge in src/webrelay/webrelay.go acts as a transparent proxy that merely forwards bytes between WebSocket clients and the upstream TCP relay. All PAKE handshakes, password verification, and payload encryption occur within the TCP relay itself (src/tcp/tcp.go), meaning the bridge handles only pre-encrypted traffic.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →