Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

peeroxide-cli is a command-line toolkit for interacting with the peeroxide P2P networking stack. It provides a set of tools for peer discovery, connectivity diagnostics, and decentralized data transfer, all while maintaining full wire-compatibility with the existing Hyperswarm and HyperDHT networks.

The binary is named peeroxide.

Core Tools

The toolkit consists of five primary commands:

  • lookup: Query the DHT to find peers announcing a specific topic.
  • announce: Announce your presence on a topic so others can discover you.
  • ping: Diagnose reachability through bootstrap checks, NAT classification, or targeted peer pings.
  • cp: Transfer files directly between peers over an encrypted swarm connection.
  • deaddrop: Perform anonymous store-and-forward messaging via the DHT.

Key Concepts

To use peeroxide effectively, it helps to understand the underlying architecture:

DHT and Routing

Peeroxide uses a Kademlia-based Distributed Hash Table (DHT) for peer discovery and coordination. This DHT is wire-compatible with the HyperDHT network used by Hyperswarm.

Kademlia Basics

The DHT operates on several core Kademlia principles:

  • XOR Distance: The “distance” between two nodes or a node and a key is calculated using the bitwise XOR of their 32-byte IDs. This metric defines the topology of the network.
  • Routing Table & k-buckets: Each node maintains a routing table organized into buckets (k-buckets). Each bucket covers a specific range of distances from the node’s own ID.
  • Iterative Lookup: Finding a value or node involves querying the closest known nodes to the target key, which then return even closer nodes, eventually converging on the target.

Peeroxide relies on the pkarr and mainline crates for much of its underlying DHT logic.

Bootstrap Nodes

A DHT is a decentralized network, but new nodes need an entry point to join. These entry points are called bootstrap nodes.

  • Public Network: By default, peeroxide uses a set of stable public bootstrap nodes to connect to the global HyperDHT network.
  • Configuration: You can specify custom bootstrap nodes using the --bootstrap flag or the network.bootstrap setting in your config file.
  • Isolated Mode: If no bootstrap nodes are provided and the --public flag is not set, the node runs in isolated mode. In this mode, discovery is only possible if peers connect to each other directly by address.

Connectivity

The DHT doesn’t just store peer records; it also facilitates connectivity:

  • Holepunching: The DHT helps two firewalled peers coordinate a direct UDP connection.
  • Relaying: If a direct connection is impossible, the DHT can help set up a relayed connection through other nodes.

For more details on how these primitives are used in practice, see the lookup and announce command documentation.

Keys and Identity

Identity in peeroxide is anchored in cryptography. Every peer is identified by an Ed25519 keypair.

Peer Identity

A peer’s Public Key is its stable identity on the network. This key is 32 bytes long and is typically represented as a 64-character hexadecimal string, often prefixed with an @ symbol in the CLI (e.g., @ab12cd34...).

Keypair Types

When running peeroxide commands, you can use different types of keypairs:

  • Ephemeral Keypairs: Generated randomly for a single session. These are used by default when no seed is provided. Once the process exits, the identity is lost.
  • Seeded Keypairs: Derived deterministically from a secret seed string using the --seed flag. The seed is hashed to produce a 32-byte secret, which is then used to generate the Ed25519 keypair. This allows a peer to maintain a stable identity across multiple runs.

Secure Connections

Peeroxide uses the Noise XX handshake protocol to establish authenticated, end-to-end encrypted connections between peers.

  • Authentication: During the handshake, peers exchange and verify their public keys. This ensures that you are communicating with exactly the peer you intended to, and that no man-in-the-middle can impersonate them.
  • Encryption: Once the handshake is complete, all data is sent over a SecretStream, which provides confidentiality and integrity.

For more information on the cryptographic protocols, see the documentation for the ed25519-dalek and noise-protocol crates.

Topics and Discovery

In peeroxide, discovery is organized around topics. A topic is a 32-byte key that serves as a rendezvous point for peers with shared interests.

What is a Topic?

Technically, a topic is always a 32-byte value. However, the CLI allows you to specify topics in two ways:

  1. Raw Hex: A 64-character hexadecimal string is interpreted directly as the 32-byte topic key.
  2. Plaintext Name: Any other string is hashed using BLAKE2b-256 to derive a 32-byte topic key. For example, the topic name "my-application" becomes the hash of those bytes.

This dual approach allows for both human-readable “namespaced” discovery and opaque, randomly generated “private” rendezvous points.

How Discovery Works

The discovery process involves two main actions:

Announcing

When you announce on a topic, you are telling the DHT that your peer identity (public key) is available for connections related to that topic. The swarm automatically handles re-announcing at regular intervals to ensure your record stays fresh on the DHT.

Looking Up

When you lookup a topic, you query the DHT for the public keys and addresses of all peers currently announcing on that topic.

Usage in Tools

Topic-based discovery is the foundation for several peeroxide commands:

  • lookup: Find peers on a topic.
  • announce: Join a topic.
  • cp: Uses a topic as a one-time rendezvous point for a file transfer.
  • ping: Can resolve a topic name to find and ping the associated peers.

By using the same topic name or hex key across different tools and peers, you can easily build decentralized workflows without needing a central coordinator.

Lookup Overview

The lookup command queries the Distributed Hash Table (DHT) to discover peers announcing a specific topic. It provides a way to find connection points (relay addresses and public keys) for any given service or dataset.

Topic Resolution

Topics can be provided as either a plaintext string or a raw 64-character hexadecimal key.

  • Raw Key: If the input is exactly 64 hex characters, it is treated as a raw 32-byte key.
  • Plaintext: Otherwise, the input string is hashed using BLAKE2b-256 via the discovery_key function to derive the target topic key.

Usage

peeroxide lookup <TOPIC> [FLAGS]

Flags

FlagDescription
--with-dataFetch metadata stored on the DHT for each discovered peer.
--jsonOutput results as Newline Delimited JSON (NDJSON) to stdout.

Peer Discovery and Deduplication

The lookup command identifies unique peers by their 32-byte public key. If multiple DHT records are found for the same public key, their relay addresses are merged into a single union set, ensuring no duplicate addresses are displayed for a single peer.

Peers are displayed in the order they were first seen during the lookup process.

Metadata Retrieval

When the --with-data flag is used, peeroxide performs a mutable_get for each discovered peer’s public key (using seq=0). This process runs concurrently with a concurrency limit of 16 to ensure high performance even when many peers are found.

The status of the data retrieval is reported for each peer, indicating whether data was found, missing, or if an error occurred during retrieval.

See Also

Lookup Output Formats

The lookup command supports two output modes: human-readable (default) and NDJSON.

Human-Readable Output

By default, lookup writes diagnostic information and peer records to stderr. The stdout stream remains empty.

The output begins with the resolved topic:

  • LOOKUP <hex> (if raw hex was provided)
  • LOOKUP blake2b("topic") (if plaintext was provided)
  • found <N> peers

Peer Record

For each peer found:

  • @<hex_public_key>
  • relays: host:port, ... (or (direct only) if no relays are registered)

Metadata (with --with-data)

If data is successfully retrieved:

  • data: "string" (escaped UTF-8) or 0x<hex> (if binary)
  • seq: <u64>

Statuses for missing or failed data:

  • data: (not stored)
  • data: (error: <message>)

JSON Output (NDJSON)

When the --json flag is used, lookup emits Newline Delimited JSON to stdout. Diagnostic logs may still appear on stderr.

Peer Schema

Each discovered peer is emitted as a separate JSON object:

{
  "type": "peer",
  "public_key": "<hex>",
  "relay_addresses": ["host:port", ...]
}

Peer Schema (with --with-data)

If metadata is requested, the peer object includes status fields:

  • Success:

    {
      "type": "peer",
      "public_key": "...",
      "relay_addresses": [...],
      "data_status": "ok",
      "data": "<string>",
      "data_encoding": "utf8"|"hex",
      "seq": <u64>
    }
    

    Note: The data field contains raw hexadecimal characters (without a 0x prefix) when the encoding is "hex".

  • Missing:

    {
      "type": "peer",
      "data_status": "none",
      "data": null,
      "seq": null,
      ...
    }
    
  • Error:

    {
      "type": "peer",
      "data_status": "error",
      "data": null,
      "seq": null,
      "error": "<message>",
      ...
    }
    

Summary Schema

A final summary object is emitted after all peers have been processed:

{
  "type": "summary",
  "topic": "<hex>",
  "peers_found": <int>
}

Exit Codes

CodeMeaning
0Success
1Fatal error (e.g., DHT failure, invalid arguments)
130Terminated by SIGINT (Ctrl+C)

Announce Overview

The announce command makes your node discoverable on the DHT for a specific topic. It allows other peers using lookup to find your public key and connection details.

Identity and Keypairs

By default, announce generates a random ephemeral keypair for each session.

  • Ephemeral: A new identity is created on startup and lost when the process exits.
  • Seeded: Using the --seed <string> flag, you can derive a deterministic keypair. The seed is hashed using the discovery_key function to produce the 32-byte secret seed.

Usage

peeroxide announce <TOPIC> [FLAGS]

Flags

FlagDescription
--seed <string>Use a seed to maintain a stable identity across restarts.
--data <string>Store metadata (max 1000 bytes) on the DHT.
--duration <sec>Exit automatically after the specified number of seconds.
--pingEnable the Echo Protocol to accept and respond to connectivity probes.

Metadata

The --data flag allows you to attach a small payload (up to 1000 UTF-8 bytes) to your DHT record. This is useful for sharing service versioning, protocol capabilities, or small state updates.

  • Sequence Numbers: Metadata is stored with a sequence number (seq) based on the current Unix epoch in seconds.
  • Refresh: To ensure your record does not expire, announce automatically refreshes the metadata every 600 seconds (10 minutes) while the process is running.

Output

All output from announce is written to stderr. The stdout stream is always empty.

Startup

  • ANNOUNCE blake2b("topic") as @<pk_hex>
  • announced to closest nodes
  • metadata: "..." (<N> bytes, seq=<u64>) (if applicable)

Shutdown

  • UNANNOUNCE blake2b("topic") (or UNANNOUNCE <hex> if raw hex was provided)
  • done

Exit Codes

CodeMeaning
0Success (including exit via SIGINT or SIGTERM)
1Fatal error (e.g., DHT failure, invalid data size)

See Also

Announce Architecture

The announce command manages a long-running swarm session, coordinating DHT presence and optionally handling incoming connections.

Initialization Flow

  1. Identity Generation: A KeyPair is either generated randomly or derived from a seed.
  2. Swarm Setup: A SwarmConfig is constructed with the identity and DHT configuration. Firewall settings are determined by the global config (Open if public=true, Consistent otherwise).
  3. Joining Topic: The node joins the topic using JoinOpts { client: false }. This instructs the DHT to act as a server for this topic, making the node discoverable to lookup queries.
  4. Flushing: The node waits for the join operation to flush, ensuring at least one announcement has reached the DHT.

Metadata Persistence

If the --data flag is provided, the node performs an initial mutable_put to the DHT.

  • Storage: The data is signed by the node’s private key and stored at the node’s public key address on the DHT.
  • Sequence: The seq field is set to the current Unix epoch in seconds.
  • Lifecycle: A background task triggers every 600 seconds to re-put the data, preventing expiration and keeping the DHT record fresh.

Connection Management

When a peer discovers this node via lookup, they may attempt to open a direct UDX connection.

  • Accepting: The announce loop listens on the conn_rx channel for incoming connections.
  • Modes:
    • Default: Incoming connections are immediately dropped to minimize resource usage.
    • Echo Mode (--ping): Connections are accepted and passed to the Echo Protocol handler.

Shutdown Sequence

The command remains active until it receives a termination signal (SIGINT or SIGTERM) or the --duration timer expires.

  1. Unannounce: The node sends a leave request to the DHT for the active topic.
  2. Cleanup: The background refresh task is aborted, and the swarm handle is destroyed.
  3. Exit: The process exits with code 0.

Echo Protocol

The Echo Protocol is a simple diagnostic service enabled by the --ping flag in the announce command. It allows remote peers to measure latency and verify end-to-end connectivity over the peeroxide swarm.

Protocol Constants

  • PING_MAGIC: 0x50 0x49 0x4E 0x47 (b"PING")
  • PONG_MAGIC: 0x50 0x4F 0x4E 0x47 (b"PONG")
  • MAX_ECHO_SESSIONS: 64 (concurrency limit)
  • HANDSHAKE_TIMEOUT: 5 seconds
  • IDLE_TIMEOUT: 30 seconds
  • ECHO_MSG_LEN: 16 bytes

Echo Probe Frame

Each probe message is exactly 16 bytes long and follows this layout:

OffsetSizeFieldDescription
08sequ64, little-endian sequence number
88timestamp_nanosu64, little-endian Unix timestamp in nanoseconds

Session Lifecycle

  1. Accept: The server accepts an incoming UDX connection.
  2. Semaphore Acquisition: The server attempts to acquire a permit from a 64-slot semaphore. If all slots are full, the connection is dropped.
  3. Handshake:
    • The server waits up to 5 seconds for the client to send the PING_MAGIC (4 bytes).
    • If the magic matches, the server replies with PONG_MAGIC (4 bytes).
  4. Echo Loop:
    • The server enters a loop, waiting for messages from the client with a 30-second idle timeout.
    • Each message must be exactly 16 bytes (ECHO_MSG_LEN).
    • The server echoes the exact 16-byte message back to the client.
  5. Disconnect: The session ends if the timeout is reached, a message of incorrect length is received, or the stream is closed.
  6. Release: The semaphore permit is released, allowing a new session to begin.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Server
    
    Client->>Server: Connect (UDX)
    Note over Server: Acquire Semaphore (max 64)
    
    Client->>Server: "PING" (4 bytes)
    Server->>Client: "PONG" (4 bytes)
    
    loop Echo Loop
        Client->>Server: 16-byte Probe
        Server->>Client: 16-byte Probe (Echo)
    end
    
    Note over Client,Server: Idle Timeout (30s) or Close
    Note over Server: Release Semaphore

Logging

The announce command logs session events to stderr:

  • [connected] @<pk> (echo mode)
  • [disconnected] @<pk> (<N> probes echoed)

Ping Overview

The ping tool is a multi-purpose diagnostic utility for the Peeroxide network. It allows you to verify connectivity to bootstrap nodes, classify your local NAT type, and perform targeted probes of peers discovered on the DHT.

Usage

peeroxide ping [target] [flags]

Targets

The ping tool behaves differently depending on the provided target:

  • No target: Performs a bootstrap check. It probes all configured bootstrap nodes to populate the local routing table, collect reflexive addresses, and classify your NAT type.
  • host:port: Performs a direct UDP probe to a specific network address.
  • @<64-char-hex-pubkey>: Resolves the peer’s relay addresses via find_peer and probes them.
  • <topic>: Resolves up to 20 peers announcing on the given topic (specified as a plain string or 64-char hex) and probes their relay addresses.

Operational Modes

Bootstrap Check

When run without a target, ping sends CMD_FIND_NODE requests to bootstrap nodes. This process:

  1. Verifies reachability to the core network.
  2. Discovers your reflexive (public) IP and port as seen by multiple nodes.
  3. Classifies your NAT Type based on the consistency of these reflexive addresses.
  4. Populates your local DHT routing table with closer nodes.

UDP Probing (Direct, PubKey, Topic)

Standard probes use the DHT CMD_PING RPC. This is a lightweight UDP-based check that verifies the target node is online and responding at the network level.

Connection Probing (--connect)

When the --connect flag is used with a PubKey or Topic target, the tool performs a full Noise XX handshake and establishes a SecretStream. After the secure connection is established, it executes the Echo Protocol to measure end-to-end encrypted latency.

Flags

  • --count <N>: Number of probes to send (default: 1). Set to 0 for infinite probing.
  • --interval <seconds>: Delay between probes (default: 1.0s).
  • --connect: Attempt a full Noise handshake and Echo protocol test.
  • --json: Output results as newline-delimited JSON (NDJSON) for machine consumption.
  • --public: Use the public bootstrap network (shorthand for adding default bootstrap nodes).

Exit Codes

  • 0: All probes succeeded.
  • 1: Partial or total failure (e.g., timeouts, resolution errors).
  • 130: Interrupted by SIGINT (Ctrl+C).

Ping Architecture

The ping tool operates at two distinct layers of the Peeroxide stack: the DHT RPC layer (UDP) and the Secure Stream layer (encrypted TCP-like transport).

Control Flow

The primary logic resides in peeroxide-cli/src/cmd/ping.rs. Depending on the target type, it orchestrates a resolution phase followed by a probing phase.

Targeted Probing with --connect

When using @pubkey or <topic> with the --connect flag, the tool follows this sequence:

  1. Resolution: Queries the DHT (find_peer or lookup) to discover relay addresses for the target peer(s).
  2. Handshake: Initiates a Noise XX handshake via the discovered relay addresses.
  3. SecretStream: Establishes a multiplexed, encrypted stream.
  4. Echo Protocol: Once the stream is ready, it initiates the Echo Protocol.
sequenceDiagram
    participant CLI as ping --connect
    participant DHT as HyperDHT
    participant Peer as Remote Peer

    CLI->>DHT: find_peer(remote_pk)
    DHT-->>CLI: relay_addresses[]
    
    rect rgb(240, 240, 240)
    Note over CLI, Peer: Noise XX + SecretStream
    CLI->>Peer: Noise Handshake (connect_with_nodes)
    Peer-->>CLI: Handshake Complete
    end

    rect rgb(220, 240, 220)
    Note over CLI, Peer: Echo Protocol
    CLI->>Peer: PING (4 bytes)
    Peer-->>CLI: PONG (4 bytes)
    loop Every interval
        CLI->>Peer: Probe [seq, timestamp] (16 bytes)
        Peer-->>CLI: Echo [seq, timestamp] (16 bytes)
        Note right of CLI: Latency = send_time.elapsed()
    end
    end

NAT Classification Logic

During a bootstrap check (no target), the tool collects reflexive addresses (Ipv4Peer) returned by multiple bootstrap nodes in their response to field.

The classification is performed by the NatType enum:

  • Open: The reflexive IP is a local interface address (verified by attempting to bind the IP) AND the reflexive port matches the local bound port.
  • Consistent: All reflexive samples report the same host:port, but the IP is external. This indicates a hole-punchable NAT.
  • Random: The reflexive host is the same across samples, but the ports vary. This typically requires relaying.
  • MultiHomed: Different bootstrap nodes report different reflexive hosts.
  • Unknown: No reflexive address samples were collected.

DHT Interaction

  • Bootstrap mode: Uses dht.ping(host, port) which translates to a CMD_FIND_NODE request on the wire. This is used because bootstrap nodes are expected to return closer nodes to help populate the routing table.
  • Direct/Targeted mode: Uses the same dht.ping mechanism but focuses on the RTT and reachability of the specific target.

Ping Output Formats

The ping tool provides human-readable output on stderr by default and machine-parseable NDJSON on stdout when the --json flag is used.

Human-Readable Output (stderr)

Bootstrap Check

BOOTSTRAP CHECK (3 nodes)
  bootstrap1.example.com:49737  OK  12ms  (20 nodes)  node_id=ab12...
  bootstrap2.example.com:49737  TIMEOUT
--- bootstrap summary ---
3 nodes, 2 reachable, 1 unreachable
50 unique peers discovered via routing tables
public address: 1.2.3.4:5000 (consistent across 2 nodes)
NAT type: consistent (hole-punchable)

Targeted Ping

PING 1.2.3.4:49737 (direct)
  [1] OK  12ms  node_id=ab12...
--- 1.2.3.4:49737 ping statistics ---
1 probes, 1 responded, 0 timed out (0% probe loss)
rtt min/avg/max = 12.0/12.0/12.0 ms

JSON Output (stdout)

JSON output is emitted as newline-delimited objects.

Resolve Events

Emitted before probing starts for PubKey and Topic targets.

{"type":"resolve","method":"find_peer","public_key":"<64-hex>"}
{"type":"resolve","status":"found","addresses":2}

Probe Events

Emitted for each individual probe. UDP probes use rtt_ms, while encrypted echo probes use latency_ms.

// UDP probe (direct/bootstrap)
{"type":"probe","seq":1,"status":"ok","rtt_ms":12.3,"node_id":"<hex>"}

// Echo probe (connect mode)
{"type":"probe","seq":1,"status":"ok","latency_ms":48.3}

Summary Events

Emitted at the end of a session if count > 1 or count == 0.

// Bootstrap summary
{
  "type": "bootstrap_summary",
  "nodes": 3,
  "reachable": 2,
  "unreachable": 1,
  "nat_type": "consistent",
  "closer_nodes_total": 50,
  "public_host": "1.2.3.4",
  "public_port": 5000,
  "port_consistent": true
}

// Targeted summary
{
  "type": "summary",
  "target": "1.2.3.4:49737",
  "probes_sent": 5,
  "probes_responded": 4,
  "probes_timed_out": 1,
  "rtt_min_ms": 11.0,
  "rtt_avg_ms": 20.5,
  "rtt_max_ms": 45.0
}

Overview

The cp command provides secure, point-to-point file transfers between peers over the Hyperswarm network. It allows you to send and receive files using a shared topic, which serves as a rendezvous point on the DHT.

Key Features

  • Direct Transfers: Files are streamed directly between peers using UDX, ensuring high performance and reliable delivery.
  • Topic-Based Discovery: Use human-readable strings or 64-character hex keys to coordinate transfers without needing to know the other peer’s IP address.
  • Piping Support: Full support for stdin (-) as a source and stdout (-) as a destination, making it easy to integrate with other command-line tools.
  • Progress Tracking: Real-time progress bars and transfer statistics provided via stderr.
  • End-to-End Encryption: All transfers are secured using Noise handshakes via SecretStream.

Basic Usage

Sending a File

To send a file, specify the file path and a topic. The command will output the topic (useful if you let it generate a random one) and wait for a receiver.

peeroxide cp send my-file.txt "shared-topic"

Receiving a File

To receive a file, use the same topic. You can specify a destination path or a directory.

peeroxide cp recv "shared-topic" ./downloads/

Streaming with Pipes

You can pipe data directly through cp:

# Sender
cat data.tar.gz | peeroxide cp send - "backup-topic"

# Receiver
peeroxide cp recv "backup-topic" - > restored.tar.gz

Command Options

send Options

  • file: Path to the file to send, or - for stdin.
  • topic: Optional topic name or 64-char hex key.
  • --name: Override the filename sent in the metadata.
  • --keep-alive: Keep the sender running for multiple sequential transfers.
  • --progress: Show a transfer progress bar.

recv Options

  • topic: The shared topic from the sender.
  • dest: Optional destination path or directory, or - for stdout.
  • --yes: Skip the confirmation prompt.
  • --force: Overwrite existing files without asking.
  • --timeout: Seconds to wait for a sender (default: 60).
  • --progress: Show a transfer progress bar.

cp — Protocol

The cp tool is built on top of the peeroxide swarm, leveraging the DHT for peer discovery and UDX for efficient data transport.

Data Flow

The transfer process involves two main stages: discovery and streaming.

1. Discovery

  • Sender: Joins the topic on the DHT as a server (JoinOpts { client: false, .. }). It announces its presence and waits for incoming connections.
  • Receiver: Joins the topic on the DHT as a client (JoinOpts { server: false, .. }). It looks for peers announcing the topic and attempts to connect to them.

2. Protocol Handshake

Once a connection is established, the peers perform a simple JSON-based handshake:

  1. Metadata: The sender transmits a JSON object containing file information:
    • filename: The name of the file being sent.
    • size: The total size in bytes (if known; null when streaming from stdin).
    • version: The protocol version (currently 1).
  2. Acceptance: The receiver validates the metadata and (unless --yes is used) prompts the user for confirmation.

3. Streaming

Data is streamed in chunks (CHUNK_SIZE = 65536 bytes) over the established SecretStream.

sequenceDiagram
    participant S as Sender
    participant DHT as DHT / Bootstrap
    participant R as Receiver

    S->>DHT: Announce Topic
    R->>DHT: Lookup Topic
    DHT-->>R: Peer Address (Sender)
    R->>S: Establish Connection (UDX + Noise XX)
    S->>R: Send Metadata (JSON)
    Note over R: Prompt User (unless --yes)
    loop Data Streaming
        S->>R: Data Chunk (64 KB)
    end
    S->>R: Shutdown Stream
    R-->>S: Connection Closed

Implementation Details

Chunking

While the underlying UDX protocol handles packetization, cp reads and writes data in 64 KB blocks. This balances memory usage with throughput efficiency.

File Handling

  • Temporary Files: During a receive operation, data is written to a hidden temporary file in the destination directory.
  • Atomic Rename: Once the transfer is complete and the received size matches the expected size, the temporary file is renamed to the final destination. This prevents partial or corrupted files if a transfer is interrupted.
  • Sanitization: Filenames provided by the sender are sanitized to prevent path traversal attacks (e.g., stripping .. components and leading slashes).

Network Configuration

The cp command respects global peeroxide configuration for bootstrap nodes and firewall settings. If --public is set, the swarm uses public bootstrap nodes with an open firewall. Otherwise, hole-punching is attempted for NAT traversal.

cp — Output Formats

The cp command separates data, status, and progress across output streams.

Standard Output (stdout)

send subcommand

Prints the transfer topic to stdout so it can be captured or piped to the receiver:

a3f2b1...  (64-char hex topic key)

recv subcommand (streaming mode)

When destination is -, file data is written directly to stdout:

peeroxide cp recv <topic> - > received_file.bin

Standard Error (stderr)

All status messages, progress, and connection info go to stderr, keeping stdout clean for data.

Sender output:

CP SEND example.bin (1.2 MB)
topic: a3f2b1...
connected from @deadbeef...
done: 1.2 MB in 2.3s (540 KB/s)

Receiver output:

CP RECV topic: a3f2b1...
looking up sender...
connected to @deadbeef...
Incoming file: example.bin (1.2 MB)
Save to: ./example.bin
done: 1.2 MB in 2.3s (540 KB/s)

Progress Display

When transfer size is known, cp displays a progress bar on stderr showing percentage, bytes transferred, speed, and ETA. When size is unknown (stdin source), a spinner with running byte count is shown instead.

Reliability

cp does not implement retransmission or resumption at the application layer. Reliability is provided by the UDX transport layer (BBR congestion control, retransmission, ordering). If a transfer fails mid-stream:

  • The temporary file is left in place (not renamed to final destination).
  • Re-running cp recv with the same topic will restart the transfer from the beginning.
  • The sender must still be running and on the same topic.

For large files over unreliable networks, consider compressing the payload before transfer to reduce exposure to interruptions.

Deaddrop Overview

The deaddrop tool provides an anonymous, asynchronous store-and-forward mechanism using the DHT. It allows a sender to “leave” data on the network that a receiver can later “pickup” using a unique key, without requiring both parties to be online at the same time.

Unlike the cp command, which establishes a direct peer-to-peer connection between a sender and receiver, deaddrop uses mutable DHT values to store data. This makes it ideal for scenarios where the sender and receiver have intermittent connectivity or want to avoid direct IP discovery.

Key Features

  • Asynchronous Delivery: Data is stored on DHT nodes. The receiver picks it up whenever they’re ready.
  • Mutable DHT Storage: Uses the HyperDHT mutable_put and mutable_get operations.
  • Chunked Transfers: Large files are automatically split into multiple chunks, linked together in a chain.
  • Passphrase Support: Pickup keys can be derived from human-readable passphrases.
  • Anonymity: No direct connection is established between the sender and receiver.
  • Acknowledgements: Optional pickup notifications (acks) let the sender know when data was retrieved.

Basic Usage

Leaving Data

To leave a message or file on the DHT:

echo "Hello from the void" | peeroxide deaddrop leave - --passphrase "my secret drop"

The tool will print a 64-character hexadecimal pickup key (unless a passphrase is used). It will then continue to run, refreshing the data on the DHT to ensure it doesn’t expire.

Picking Up Data

To retrieve data:

peeroxide deaddrop pickup --passphrase "my secret drop"

The receiver fetches each chunk sequentially, reassembles the original data, and verifies its integrity using a CRC-32C checksum.

How it Differs from cp

Featurecpdeaddrop
ConnectionDirect P2P (UDX)Mediated via DHT storage
Online RequirementBoth must be onlineAsynchronous
DiscoveryTopic-basedKey-based (Public Key)
SpeedHigh (Direct)Moderate (DHT round-trips)
MetadataFilename, sizeSequential chunks

Deaddrop Architecture

The deaddrop protocol enables store-and-forward data delivery using the HyperDHT’s mutable storage capabilities. It builds a linked chain of signed chunks, where each chunk is stored on the DHT at a location derived from a deterministic key derivation scheme.

Data Flow

The following diagram illustrates the interaction between the Sender, the DHT network, and the Receiver.

sequenceDiagram
    participant S as Sender
    participant DHT as DHT Network
    participant R as Receiver

    Note over S: 1. Split data into chunks
    Note over S: 2. Derive keypairs for each chunk
    S->>DHT: 3. mutable_put(chunk_0..N)
    Note over S: 4. Print pickup key (root PK)

    Note over R: 5. Obtain pickup key
    R->>DHT: 6. mutable_get(root_PK)
    DHT-->>R: 7. Returns root chunk (metadata + next_PK)
    loop For each chunk
        R->>DHT: 8. mutable_get(next_PK)
        DHT-->>R: 9. Returns next chunk
    end
    Note over R: 10. Reassemble & Verify CRC
    R->>DHT: 11. announce(ack_topic)
    DHT-->>S: 12. lookup(ack_topic) detected

Key Components

Mutable DHT Storage

Unlike immutable storage (used in cp), deaddrop uses mutable_put and mutable_get. This allows the sender to refresh records to extend their lifespan on the DHT (which typically expires after 20 minutes). Records are signed by the sender, ensuring that DHT nodes or malicious actors cannot modify the data without invalidating the signature.

Chunking and Chaining

Data is split into chunks to fit within the DHT’s payload limits (max 1000 bytes per chunk).

  • Root Chunk: Contains the total chunk count, a CRC-32C checksum of the full payload, and the public key of the next chunk.
  • Continuation Chunks: Contain the payload and the public key of the next chunk in the sequence.
  • Termination: The final chunk in the chain has its next_pk field set to 32 zero bytes.

Key Derivation

All keypairs for the chunks are derived deterministically from a single root_seed.

  • root_seed: 32 bytes (randomly generated or derived from a passphrase).
  • root_kp: KeyPair::from_seed(root_seed).
  • chunk_kp[i]: Derived from blake2b(root_seed || i_as_u16_le).

The pickup key is the public key of the root chunk. Since the receiver only has the public key, they can read the data but cannot derive the private keys required to modify or forge chunks.

Acknowledgement (Ack) Mechanism

When a receiver successfully picks up a deaddrop, they “announce” their presence on a specific ack_topic.

  • ack_topic = discovery_key(root_public_key || b"ack")
  • The sender polls this topic using lookup.
  • To maintain anonymity, the receiver uses an ephemeral keypair for the announcement.

Deaddrop Wire Format

Deaddrop uses a versioned binary format for its DHT records. Each record consists of a header followed by the payload.

Constants

  • MAX_PAYLOAD: 1000 bytes (total record size)
  • VERSION: 0x01
  • ROOT_HEADER_SIZE: 39 bytes
  • NON_ROOT_HEADER_SIZE: 33 bytes

Root Chunk (v1)

The root chunk is the entry point of the deaddrop. Its public key is the “pickup key”.

OffsetSizeFieldDescription
01VersionSet to 0x01
12Total ChunksNumber of chunks in the chain (u16 LE)
34CRC-32CChecksum of the full reassembled payload
732Next PKPublic key of the next chunk (32 zeros if single chunk)
39PayloadData bytes (up to 961 bytes)

Continuation Chunk (v1)

All subsequent chunks use a smaller header.

OffsetSizeFieldDescription
01VersionSet to 0x01
132Next PKPublic key of the next chunk (32 zeros if last chunk)
33PayloadData bytes (up to 967 bytes)

Implementation Details

Byte Order

All multi-byte integers (Total Chunks, CRC-32C) are encoded in little-endian byte order.

Integrity Verification

The CRC-32C checksum uses the Castagnoli polynomial. It is computed over the entire reassembled payload, not per-chunk. Receivers must fetch all chunks and reassemble them before verifying the checksum.

Chain Termination

The chain is considered terminated when a chunk (root or continuation) contains a Next PK field consisting of 32 null bytes (0x00).

Deaddrop Output Formats

The deaddrop command supports both human-readable terminal output and machine-readable JSON output for integration with other tools.

Human-Readable Output (Default)

By default, deaddrop prints status messages to stderr and the resulting data (for pickup) or key (for leave) to stdout.

leave status output

DEADDROP LEAVE 5 chunks (4500 bytes)
  published chunk 1/5
  published chunk 2/5
  ...
  published to DHT (best-effort)
  pickup key printed to stdout
  refreshing every 600s, monitoring for acks...

pickup status output

DEADDROP PICKUP @a1b2c3d4...
  fetching chunk 1/5...
  fetching chunk 2/5...
  ...
  reassembled 4500 bytes
  ack sent (ephemeral identity)
  done

Machine-Readable Output (--json)

Using the --json flag changes the output to a single-line JSON object per event or result.

leave result

When data is successfully published, the pickup key is returned:

{
  "type": "result",
  "pickup_key": "a1b2c3d4...",
  "chunks": 5,
  "bytes": 4500
}

pickup result

When data is successfully retrieved:

{
  "type": "result",
  "bytes": 4500,
  "crc": "f3b2a100",
  "output": "stdout"
}

Progress Events

Intermediate progress can also be tracked via JSON:

{
  "type": "progress",
  "chunk": 3,
  "total": 5,
  "action": "fetch"
}

Acknowledgement Events

When the sender detects a pickup via an ack:

{
  "type": "ack",
  "pickup_number": 1,
  "peer": "e5f6g7h8..."
}

Future Direction (Not Yet Implemented)

Note: The following features and protocol changes describe Deaddrop v2 and are not yet implemented.

The current Deaddrop v1 protocol uses a single linked-list of chunks. While functional, this requires sequential fetching where the receiver must download each chunk to discover the address of the next. For large files, this leads to high latency due to sequential round-trips.

Deaddrop v2: Two-Chain Storage Protocol

Deaddrop v2 introduces a “two-chain” architecture to enable parallel data fetching while preserving anonymity and read-only pickup semantics.

Index Chain vs. Data Chain

Instead of a single list, the protocol separates metadata and pointers from the actual data:

  • Index Chain: A small linked-list of records containing public keys (pointers) to data chunks.
  • Data Chain: Independently addressable data chunks stored at random DHT coordinates.
Index chain (sequential fetch, small):
  [root idx] → [idx 1] → [idx 2] → ... → [idx K]
      │            │           │
      ▼            ▼           ▼
Data chain (parallel fetch, bulk):
  [d0..d29]    [d30..d59]   [d60..d89] ...

Benefits of v2

Propertyv1 (Sequential)v2 (Parallel)
Fetch PatternEntirely sequentialIndex sequential + Data parallel
Overhead~3.4-3.9%~0.1%
Max File Size~60 MB~1.9 GB
1MB Fetch Time~1000 round-trips (15-50 min)~34 index + ~1000 parallel (~1 min)

Key Derivation in v2

Keypairs are derived deterministically from the root_seed using domain separation:

  • index_keypair[i] = blake2b(root_seed || "idx" || i)
  • data_keypair[i] = blake2b(root_seed || "dat" || i)

This ensures the sender can refresh the entire structure from a single seed while preventing address correlation between the index and data chains for third parties.

Frame Formats (v2)

  • Data Chunk (0x02): 1-byte version tag + up to 999 bytes of raw payload.
  • Root Index Chunk (0x02): Metadata (size, CRC), next index pointer, and up to 29 data chunk pointers.
  • Non-Root Index Chunk (0x02): next index pointer and up to 30 data chunk pointers.

Security Model

This appendix describes the threat model and security properties of the peeroxide-cli tools.

Transport Security

All peer-to-peer connections established by peeroxide-cli use the Noise XX handshake protocol followed by a SecretStream encrypted channel. This provides:

  • Mutual authentication: Both peers authenticate with their Ed25519 public keys during handshake.
  • Forward secrecy: Session keys are ephemeral and derived per-connection.
  • Confidentiality and integrity: All application data is encrypted and authenticated.

The Noise XX pattern is the same used by the Node.js Hyperswarm stack, ensuring interoperability.

Peer Identity

A peer’s identity is its Ed25519 public key (32 bytes). There is no central authority — identity is self-sovereign. Anyone who controls a private key controls that identity.

  • Ephemeral identities (default for announce) are generated fresh on each run and leave no persistent trace.
  • Seeded identities (--seed flag) are deterministic: KeyPair::from_seed(discovery_key(seed.as_bytes())). The seed string is a secret — anyone who knows it can derive the same keypair.

Warning: Treat --seed values like passwords. They are not hashed with a KDF — raw BLAKE2b is fast, making brute-force of short seeds feasible.

DHT Trust Model

The DHT is untrusted infrastructure. Any node can relay packets, and routing table entries are not authenticated. Mitigations:

  • Mutable DHT values are Ed25519-signed by the originating keypair. Verifiers (including lookup --with-data) confirm the signature before using the data. Forging data requires the private key.
  • Immutable DHT values (used by cp) are addressed by the SHA-256 hash of their content. Content is verified on retrieval.
  • Topic keys are not secret — anyone who knows the topic can look up its peer list. Do not treat topic confidentiality as a security property.

deaddrop Threat Model

deaddrop uses mutable DHT storage addressed by (public_key, topic). Security properties:

  • Only the holder of the private key can write to a slot (signatures enforced by the DHT).
  • Anyone who knows (public_key, topic) can read the slot — there is no access control on reads.
  • Data is signed but not encrypted at the DHT layer. For sensitive payloads, encrypt the application data before using deaddrop.
  • deaddrop is designed for asynchronous communication where sender and receiver share a topic out-of-band.

cp Threat Model

cp data is immutable and content-addressed. Anyone who knows the cp:// key can retrieve the data. There is no access control. Do not use cp for sensitive data without prior encryption.

Echo Protocol Security

The echo protocol (see Echo Protocol) is intentionally minimal — it echoes arbitrary 16-byte payloads after a PING/PONG handshake. Session concurrency is capped at MAX_ECHO_SESSIONS = 64 to limit resource exhaustion. The handshake uses a 5-second timeout to prevent slowloris-style attacks.

Because announce connections go through the Noise XX handshake, all echo traffic is authenticated and encrypted. An unauthenticated party cannot reach the echo server.

Denial of Service Considerations

  • Bootstrap nodes are configurable. An attacker controlling all bootstrap nodes can eclipse a node from the DHT.
  • The DHT is susceptible to Sybil attacks in the general case — this is a known limitation of permissionless DHTs.
  • announce data refresh runs every 600 seconds. If a node is offline, its announcement expires naturally according to DHT TTL policies.

Limits and Performance

This appendix documents hard limits, configurable bounds, and observed performance characteristics of the peeroxide-cli tools.

Hard Limits

ConstantValueContext
MAX_ECHO_SESSIONS64Concurrent echo sessions per announce process
HANDSHAKE_TIMEOUT5 sEcho protocol handshake timeout
IDLE_TIMEOUT30 sEcho session idle timeout
ECHO_MSG_LEN16 bytesEcho probe frame size (fixed)
ECHO_TIMEOUT (ping)5 sPer-probe timeout in ping --connect mode
MAX_CHUNKS65 535Maximum chunks in a single deaddrop message
MAX_PAYLOAD1 000 bytesMaximum payload per deaddrop chunk
ROOT_HEADER_SIZE39 bytesdeaddrop root chunk header size
NON_ROOT_HEADER_SIZE33 bytesdeaddrop non-root chunk header size
CHUNK_SIZE (cp)65 536 bytescp file chunk size
--data max (announce)1 000 bytesMaximum --data payload for announce
lookup --with-data concurrency16buffer_unordered(16) for mutable DHT gets
ping topic mode peer cap20Maximum peers probed per topic lookup

Derived Limits

Maximum deaddrop message size:

MAX_CHUNKS × MAX_PAYLOAD = 65 535 × 1 000 = ~65.5 MB

Practical limit is lower due to DHT value size constraints and network latency.

Maximum cp file size:

Limited by available DHT storage and client memory. Each 65 536-byte chunk is stored as a separate immutable DHT value. There is no hard-coded upper bound in the CLI, but very large files will require many round-trips.

Timing

ParameterValueNotes
announce refresh interval600 sBackground mutable put to keep slot alive
announce seqUnix epoch secondsTwo refreshes in the same second produce identical seq — see ISSUES.md
ping --interval default1.0 sConfigurable
ping --count default10 = infinite

Concurrency

  • lookup --with-data fetches peer data in parallel with a concurrency window of 16 (buffer_unordered(16)).
  • announce handles incoming connections concurrently; echo sessions are bounded by MAX_ECHO_SESSIONS = 64.
  • cp uploads/downloads chunks sequentially per file (parallelism may be added in future releases).

Exit Codes

CodeMeaningTools
0Success / clean shutdownall tools
1Fatal errorall tools
130SIGINT receivedlookup, ping
0SIGINT/SIGTERM receivedannounce (intentional — clean shutdown is success)

Note: announce returns 0 on SIGINT/SIGTERM because interactive shutdown is the normal workflow. lookup and ping return 130 to allow callers to distinguish interruption from success.