How Croc Multiplexes Multiple Transfers Over a Single Connection

Croc establishes one encrypted TCP connection using a PAKE handshake, then interleaves chunks from different files using unique message identifiers, allowing concurrent transfers without opening additional sockets.

Croc is an open-source command-line file transfer tool that streams multiple files simultaneously over a single encrypted session. Unlike traditional transfer methods that open new TCP connections per file, croc implements message-level multiplexing to push chunks from many files through one persistent socket, maximizing bandwidth utilization while maintaining end-to-end encryption.

Session Establishment with PAKE

Before any data flows, croc creates a secure channel between sender and receiver. The two endpoints perform a Password-Authenticated Key Exchange (PAKE) handshake implemented in croc.Pake. This exchange yields a shared secret key that is stored in the Client struct as Key []byte in src/croc/croc.go (lines 15–19).

All subsequent messages traveling over the single net.Conn are encrypted and decrypted using this shared key, ensuring that the multiplexed stream remains private even when interleaving data from multiple files.

Message Framing and Transfer IDs

Multiplexing relies on a strict message protocol defined in src/message/message.go. Every payload moving across the connection is wrapped in a Message structure that contains metadata allowing the receiver to demultiplex the stream:

type Message struct {
    Kind string      // "chunk", "fileinfo", "close", etc.
    ID   string      // unique identifier for the specific file
    Data []byte      // actual bytes or JSON metadata
}

The transfer ID field is critical: it identifies which file a given chunk belongs to. Because each message carries its own ID, the receiver can process chunks from file A, then file B, then file A again, writing each to the correct destination without confusion.

Chunk-Level Multiplexing Over a Single Channel

Once the session is established, croc breaks each file into fixed-size blocks (chunks). Instead of creating separate network threads or sockets per file, the sender pushes all chunks onto a single comm.Comm channel (c.conn []*comm.Comm).

As implemented in src/comm/comm.go, the Send method encrypts each Message with the shared secret key and writes it to the underlying TCP connection:

func (c *Comm) Send(msg Message) error {
    // Encrypt with the shared secret key (c.key)
    enc, err := encrypt(msg, c.key)
    if err != nil {
        return err
    }
    // Write to the single underlying net.Conn
    _, err = c.conn.Write(enc)
    return err
}

The sender iterates over the file list and calls Send for each chunk immediately, without waiting for previous files to complete. The receiver reads from the same stream, inspects the Kind and ID fields, and writes the decrypted bytes to the appropriate target file. This interleaving allows many files to progress concurrently, fully utilizing the bandwidth of one TCP stream.

Controlling Multiplexing Behavior

Disabling Multiplexing

Multiplexing is enabled by default, but croc provides an escape hatch for debugging or compatibility with restricted endpoints. The --no-multi flag (parsed in src/cli/cli.go) sets the NoMultiplexing boolean in Options. When active, the transfer loop in src/croc/croc.go (referenced at lines 1140, 1438, and 1775) emits a "no multiplexing" debug message and falls back to a sequential, one-file-at-a-time mode.


# Default: multiplexed transfer

croc send fileA.txt fileB.pdf

# Disable multiplexing for sequential transfer

croc send --no-multi fileA.txt fileB.pdf

Flow Control and Rate Limiting

To prevent the single connection from overwhelming the receiver, croc employs a per-second rate limiter (rate.Limiter) that throttles the sending of chunks. This guarantees steady throughput while keeping the pipe full, ensuring that multiplexing does not degrade performance on slower networks.

Key Implementation Files

The multiplexing logic spans several core components:

  • src/croc/croc.go: Contains the Client struct, session setup, and the main transfer loop that orchestrates multiplexing.
  • src/message/message.go: Defines the Message type with Kind, ID, and payload fields—the building blocks of multiplexed streams.
  • src/comm/comm.go: Low-level wrapper around the TCP connection providing Send and Receive methods that handle encryption and the single-socket I/O.
  • src/cli/cli.go: Parses the --no-multi flag and propagates the NoMultiplexing option to the client.
  • src/utils/utils.go: Provides helper functions for chunking files and calculating byte ranges used during multiplexed transfers.

Summary

  • Croc creates one encrypted TCP session using PAKE and reuses that single net.Conn for all files.
  • The Message struct in src/message/message.go wraps every payload with a unique transfer ID, enabling the receiver to demultiplex interleaved chunks.
  • All file chunks flow through a single comm.Comm channel, allowing parallel progress across many files without socket overhead.
  • The --no-multi flag forces sequential transfers for debugging or compatibility purposes.
  • A rate limiter prevents the multiplexed stream from saturating the receiver’s buffer.

Frequently Asked Questions

Does croc open a new connection for each file?

No. Croc opens exactly one TCP connection per transfer session. All files are broken into chunks and streamed over this single encrypted socket using the Message protocol with unique transfer IDs to keep data separated.

What encryption protects the multiplexed stream?

Croc uses PAKE (Password-Authenticated Key Exchange) to negotiate a shared secret, then encrypts every Message using that key with AES-256-GCM via the methods in src/comm/comm.go. This ensures that even though chunks from different files are interleaved on the wire, they remain unreadable to anyone without the session key.

Can I disable multiplexing if transfers fail?

Yes. Use the --no-multi command-line flag when sending. This forces croc to transfer files sequentially (one at a time) rather than interleaving chunks, which can resolve issues with restrictive firewalls or broken middleboxes that cannot handle out-of-order application data.

How does croc prevent chunks from different files getting mixed up?

Each chunk is wrapped in a Message struct that carries a unique string ID identifying its parent file. The receiver maintains a mapping of these IDs to open file handles. When a chunk arrives, croc inspects the ID field and writes the payload to the corresponding destination, ensuring that data never crosses between files even when chunks arrive out of order.

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 →