How the Communication Layer in comm.go Works in croc

The comm.go file in croc implements a low-level binary communication protocol that wraps net.Conn with magic-byte framing, length-prefixed messages, timeout handling, and optional SOCKS5/HTTP proxy support.

The schollz/croc secure file transfer tool relies on a robust communication layer to handle network I/O across relays and peer connections. At the heart of this layer lies src/comm/comm.go, which abstracts raw TCP connections into a structured binary protocol with built-in error detection and proxy support.

Connection Establishment and Proxy Support

The entry point for creating connections is the NewConnection(address) function. This helper evaluates global configuration variables—specifically Socks5Proxy and HttpProxy—to determine whether to route traffic through an intermediary.

When proxy variables are set and the target address is not a local IP, the function constructs a proxy.Dialer using either golang.org/x/net/proxy for SOCKS5 or github.com/magisterquis/connectproxy for HTTP CONNECT tunnels. If no proxy is configured or the target is local, the code falls back to net.DialTimeout with a default 30-second deadline.

Upon successful dial, the raw net.Conn is passed to comm.New(c net.Conn), which returns a *Comm instance. This constructor immediately applies generous 3-hour read and write deadlines using SetReadDeadline and SetWriteDeadline to keep long-running file transfers alive.

The Comm Struct: Wrapping net.Conn

The Comm type provides a thin, stateful wrapper around the standard library connection:

type Comm struct {
    connection net.Conn
}

The New(c net.Conn) method attaches the connection and configures the initial timeouts. For advanced use cases requiring direct access to the underlying socket, the Connection() method returns the raw net.Conn.

Binary Framing Protocol: Sending Data

Data transmission follows a strict binary framing protocol implemented in the Write method (and its convenience wrapper Send). The process encodes every message with three components:

  1. Magic bytes – The constant MAGIC_BYTES = []byte("croc") (hex 0x63 0x72 0x6f 0x63) identifies valid croc traffic
  2. Length prefix – The payload length as a little-endian uint32 (4 bytes)
  3. Payload – The raw message bytes

The implementation writes these components as a single atomic operation to minimize TCP fragmentation:

func (c *Comm) Write(b []byte) (n int, err error)

Errors during transmission are wrapped with context to aid debugging of network failures.

Receiving Data: Framing Verification

The Read method (exposed via the Receive convenience function) performs the inverse operation with strict validation:

  1. Magic verification – Reads the first 4 bytes and validates against MAGIC_BYTES; mismatches abort immediately with an error
  2. Length decoding – Reads the next 4 bytes as a little-endian uint32 and validates against maxReadMessageSize (64 MiB) to prevent memory exhaustion attacks
  3. Payload extraction – Adjusts the read deadline to 10 minutes (messageBodyReadTimeout) to accommodate large control messages, then reads exactly the declared number of bytes

The signature returns the payload along with metadata:

func (c *Comm) Read() (buf []byte, numBytes int, bs []byte, err error)

Timeout Strategy and Connection Lifecycle

The communication layer implements a dual-timeout strategy to balance responsiveness with reliability:

  • Initial deadline – A 3-hour read deadline applied at instantiation keeps connections alive during idle periods or large file transfers
  • Per-message deadline – After parsing the length header, the deadline tightens to 10 minutes for the actual payload read, protecting against stuck or malformed streams while allowing legitimate large messages (such as resume-range lists) to traverse relays

Write operations inherit the same 3-hour deadline established during Comm initialization.

Error Handling and Safety Limits

The layer includes defensive checks for protocol violations. Messages exceeding maxReadMessageSize (64 MiB) trigger an explicit "message too large" error, as verified by TestReceiveRejectsOversizedMessage. Conversely, TestReceiveAllowsLargeMessage confirms that legitimate messages near the limit are accepted when the extended 10-minute timeout is applied.

Proxy parsing errors are logged and returned early, preventing invalid proxy URLs from causing ambiguous connection failures.

Practical Usage Example

The following pattern demonstrates establishing a connection and exchanging framed messages, mirroring the approach used in src/comm/comm_test.go:

package main

import (
    "fmt"
    "log"

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

func main() {
    // Establish a connection (use a reachable croc relay address)
    c, err := comm.NewConnection("relay.example.com:4000")
    if err != nil {
        log.Fatalf("failed to connect: %v", err)
    }
    defer c.Close()

    // Send a simple control message
    if err := c.Send([]byte("hello, computer")); err != nil {
        log.Fatalf("send error: %v", err)
    }

    // Receive the peer's response
    payload, err := c.Receive()
    if err != nil {
        log.Fatalf("receive error: %v", err)
    }
    fmt.Printf("peer says: %s\n", string(payload))
}

Summary

  • src/comm/comm.go provides the foundational transport abstraction for all croc network operations
  • Magic-byte framing (croc header) and little-endian length prefixes ensure protocol integrity
  • Dual-timeout architecture (3-hour connection, 10-minute message body) prevents hangs while supporting large transfers
  • 64 MiB message limit protects against denial-of-service via memory exhaustion
  • Proxy support for both SOCKS5 and HTTP CONNECT is handled transparently during connection establishment

Frequently Asked Questions

What is the purpose of the MAGIC_BYTES constant in comm.go?

The MAGIC_BYTES constant—defined as []byte("croc")—serves as a protocol identifier prepended to every message. When receiving data, comm.go validates that the first four bytes match this sequence, immediately aborting connections that send malformed or non-croc traffic.

How does croc handle proxy connections in the communication layer?

When global variables Socks5Proxy or HttpProxy are configured, NewConnection instantiates a proxy.Dialer from either golang.org/x/net/proxy or github.com/magisterquis/connectproxy. This dialer wraps the TCP connection establishment, tunneling all subsequent traffic through the specified proxy unless the target is a local IP address.

What is the maximum message size supported by croc's comm.go?

The communication layer enforces a hard limit of 64 MiB (maxReadMessageSize). Any message declaring a larger payload in its length header is rejected with an error before the body is read, preventing memory exhaustion attacks on relay servers or peers.

How does the timeout mechanism prevent connection hangs during file transfers?

The Comm struct applies a liberal 3-hour deadline to the underlying connection to accommodate large file transfers. However, once a message header is parsed, the deadline temporarily reduces to 10 minutes for reading the specific payload. This two-tier approach keeps long-running transfers alive while quickly detecting stalled or zombie connections.

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 →