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: How the Kademlia-based Distributed Hash Table handles peer discovery and routing.
- Keys and Identity: How Ed25519 keypairs define peer identity and secure connections.
- Topics and Discovery: How peers group together using 32-byte topic keys.
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,
peeroxideuses a set of stable public bootstrap nodes to connect to the global HyperDHT network. - Configuration: You can specify custom bootstrap nodes using the
--bootstrapflag or thenetwork.bootstrapsetting in your config file. - Isolated Mode: If no bootstrap nodes are provided and the
--publicflag 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
--seedflag. 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:
- Raw Hex: A 64-character hexadecimal string is interpreted directly as the 32-byte topic key.
- 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_keyfunction to derive the target topic key.
Usage
peeroxide lookup <TOPIC> [FLAGS]
Flags
| Flag | Description |
|---|---|
--with-data | Fetch metadata stored on the DHT for each discovered peer. |
--json | Output 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
- Output Formats for details on human-readable and JSON output.
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.
Header
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) or0x<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
datafield contains raw hexadecimal characters (without a0xprefix) 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
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Fatal error (e.g., DHT failure, invalid arguments) |
| 130 | Terminated 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 thediscovery_keyfunction to produce the 32-byte secret seed.
Usage
peeroxide announce <TOPIC> [FLAGS]
Flags
| Flag | Description |
|---|---|
--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. |
--ping | Enable 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,
announceautomatically 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 nodesmetadata: "..." (<N> bytes, seq=<u64>)(if applicable)
Shutdown
UNANNOUNCE blake2b("topic")(orUNANNOUNCE <hex>if raw hex was provided)done
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success (including exit via SIGINT or SIGTERM) |
| 1 | Fatal error (e.g., DHT failure, invalid data size) |
See Also
- Architecture for internal implementation details.
- Echo Protocol for details on the
--pingmode.
Announce Architecture
The announce command manages a long-running swarm session, coordinating DHT presence and optionally handling incoming connections.
Initialization Flow
- Identity Generation: A
KeyPairis either generated randomly or derived from a seed. - Swarm Setup: A
SwarmConfigis constructed with the identity and DHT configuration. Firewall settings are determined by the global config (Open ifpublic=true, Consistent otherwise). - 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. - 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
seqfield 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
announceloop listens on theconn_rxchannel 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.
- Unannounce: The node sends a
leaverequest to the DHT for the active topic. - Cleanup: The background refresh task is aborted, and the swarm handle is destroyed.
- 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:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 8 | seq | u64, little-endian sequence number |
| 8 | 8 | timestamp_nanos | u64, little-endian Unix timestamp in nanoseconds |
Session Lifecycle
- Accept: The server accepts an incoming UDX connection.
- Semaphore Acquisition: The server attempts to acquire a permit from a 64-slot semaphore. If all slots are full, the connection is dropped.
- 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).
- The server waits up to 5 seconds for the client to send the
- 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.
- Disconnect: The session ends if the timeout is reached, a message of incorrect length is received, or the stream is closed.
- 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 viafind_peerand 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:
- Verifies reachability to the core network.
- Discovers your reflexive (public) IP and port as seen by multiple nodes.
- Classifies your NAT Type based on the consistency of these reflexive addresses.
- 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 to0for 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:
- Resolution: Queries the DHT (
find_peerorlookup) to discover relay addresses for the target peer(s). - Handshake: Initiates a Noise XX handshake via the discovered relay addresses.
- SecretStream: Establishes a multiplexed, encrypted stream.
- 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
bindthe 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 aCMD_FIND_NODErequest 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.pingmechanism 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 andstdout(-) 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-forstdin.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-forstdout.--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:
- 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;nullwhen streaming from stdin).version: The protocol version (currently1).
- Acceptance: The receiver validates the metadata and (unless
--yesis 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 recvwith 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_putandmutable_getoperations. - 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
| Feature | cp | deaddrop |
|---|---|---|
| Connection | Direct P2P (UDX) | Mediated via DHT storage |
| Online Requirement | Both must be online | Asynchronous |
| Discovery | Topic-based | Key-based (Public Key) |
| Speed | High (Direct) | Moderate (DHT round-trips) |
| Metadata | Filename, size | Sequential 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_pkfield 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 fromblake2b(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:0x01ROOT_HEADER_SIZE: 39 bytesNON_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”.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | Version | Set to 0x01 |
| 1 | 2 | Total Chunks | Number of chunks in the chain (u16 LE) |
| 3 | 4 | CRC-32C | Checksum of the full reassembled payload |
| 7 | 32 | Next PK | Public key of the next chunk (32 zeros if single chunk) |
| 39 | … | Payload | Data bytes (up to 961 bytes) |
Continuation Chunk (v1)
All subsequent chunks use a smaller header.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | Version | Set to 0x01 |
| 1 | 32 | Next PK | Public key of the next chunk (32 zeros if last chunk) |
| 33 | … | Payload | Data 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
| Property | v1 (Sequential) | v2 (Parallel) |
|---|---|---|
| Fetch Pattern | Entirely sequential | Index 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),
nextindex pointer, and up to 29 data chunk pointers. - Non-Root Index Chunk (0x02):
nextindex 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 (
--seedflag) 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
--seedvalues 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. deaddropis 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.
announcedata 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
| Constant | Value | Context |
|---|---|---|
MAX_ECHO_SESSIONS | 64 | Concurrent echo sessions per announce process |
HANDSHAKE_TIMEOUT | 5 s | Echo protocol handshake timeout |
IDLE_TIMEOUT | 30 s | Echo session idle timeout |
ECHO_MSG_LEN | 16 bytes | Echo probe frame size (fixed) |
ECHO_TIMEOUT (ping) | 5 s | Per-probe timeout in ping --connect mode |
MAX_CHUNKS | 65 535 | Maximum chunks in a single deaddrop message |
MAX_PAYLOAD | 1 000 bytes | Maximum payload per deaddrop chunk |
ROOT_HEADER_SIZE | 39 bytes | deaddrop root chunk header size |
NON_ROOT_HEADER_SIZE | 33 bytes | deaddrop non-root chunk header size |
CHUNK_SIZE (cp) | 65 536 bytes | cp file chunk size |
--data max (announce) | 1 000 bytes | Maximum --data payload for announce |
lookup --with-data concurrency | 16 | buffer_unordered(16) for mutable DHT gets |
| ping topic mode peer cap | 20 | Maximum 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
| Parameter | Value | Notes |
|---|---|---|
announce refresh interval | 600 s | Background mutable put to keep slot alive |
announce seq | Unix epoch seconds | Two refreshes in the same second produce identical seq — see ISSUES.md |
ping --interval default | 1.0 s | Configurable |
ping --count default | 1 | 0 = infinite |
Concurrency
lookup --with-datafetches peer data in parallel with a concurrency window of 16 (buffer_unordered(16)).announcehandles incoming connections concurrently; echo sessions are bounded byMAX_ECHO_SESSIONS = 64.cpuploads/downloads chunks sequentially per file (parallelism may be added in future releases).
Exit Codes
| Code | Meaning | Tools |
|---|---|---|
| 0 | Success / clean shutdown | all tools |
| 1 | Fatal error | all tools |
| 130 | SIGINT received | lookup, ping |
| 0 | SIGINT/SIGTERM received | announce (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.