Direct, ephemeral file transfers between peers. No server storage.
p2pal is a CLI tool for sending files directly between machines using WebRTC. The server only coordinates the connection — it never sees, stores, or proxies file contents.
sender server (signaling only) receiver
| | |
|-- POST /api/transfer ------> | |
|<-- join code --------------- | |
| |<-- POST /api/join (join code) --- |
|-- WebSocket (offer/ICE) ---> | --- relay --> WebSocket --------> |
|<- WebSocket (answer/ICE) --- | <-- relay <-- WebSocket --------- |
| |
|<================ WebRTC DataChannel (direct P2P) ===============>|
| file bytes never touch server |
When a direct connection is not possible (restrictive NAT/firewall), traffic is relayed via TURN — still end-to-end, without server-side storage.
- Peer-to-peer transfers via WebRTC DataChannels
- Multi-file and directory (
-r) transfers in a single session - Pause/resume: checkpoint-based recovery after interruption
- Automatic reconnect after live network drops (signaling + WebRTC renegotiation)
- SHA-256 integrity verification on every transfer
- Binary protocol over DataChannel (no JSON+base64 overhead)
- Short-lived join codes and tokens, auto-expiring via TTL
- STUN + TURN support for NAT traversal
- Per-file error handling with optional skip-continue (
-skip-errors) - Quiet mode (
-q) for scripting - Progress bar with per-file and transfer-level stats
- Configurable chunk size (
--chunk-size)
- Go 1.25+
- Docker (for Redis and integration tests)
- TURN server credentials (optional — required for non-P2P fallback)
make upThis starts both Redis and the backend server via Docker Compose.
make build
# Produces: bin/server bin/p2pal./bin/p2pal send report.pdfOutput:
Sending file: report.pdf (1 file, 2.3 MB) — TTL: 5m
Creating transfer...
Join code: zebra-luna-47
Waiting for receiver...
./bin/p2pal recv zebra-luna-47Output:
Joining transfer...
Receiving: report.pdf (2.3 MB)
[====================] 100% — SHA-256 verified
./bin/p2pal send [flags] <file> [file2 ...]
./bin/p2pal send -r [flags] <directory>
./bin/p2pal send --resume <transfer-id>| Flag | Default | Description |
|---|---|---|
-server |
http://localhost:8080 |
Backend server URL |
-ttl |
5m |
Transfer expiry (e.g. 10m, 1h) |
-r |
false |
Recursively send a directory |
-y |
false |
Skip confirmation prompt |
-q |
false |
Quiet mode: only print join code and errors |
-skip-errors |
false |
Skip files with errors instead of aborting the batch |
-chunk-size |
63 |
Chunk size in KiB (max: 63) |
-resume |
Resume an interrupted transfer by checkpoint ID | |
-checkpoint-ttl |
168h |
TTL for checkpoint files |
-max-retries |
3 |
Connection retry attempts |
-retry-backoff |
1s |
Initial retry backoff |
-stun |
stun:stun.l.google.com:19302 |
STUN server URL |
-turn |
TURN server URL (repeatable) | |
-turn-user |
$TURN_USER |
TURN username |
-turn-pass |
$TURN_PASS |
TURN password |
./bin/p2pal recv [flags] <join-code>| Flag | Default | Description |
|---|---|---|
-server |
http://localhost:8080 |
Backend server URL |
-out |
current dir | Output path or directory |
-overwrite |
false |
Overwrite existing files |
-y |
false |
Skip confirmation prompts |
-q |
false |
Quiet mode: only print errors |
-checkpoint-ttl |
168h |
TTL for checkpoint files |
-max-retries |
3 |
Connection retry attempts |
-retry-backoff |
1s |
Initial retry backoff |
-stun |
stun:stun.l.google.com:19302 |
STUN server URL |
-turn |
TURN server URL (repeatable) | |
-turn-user |
$TURN_USER |
TURN username |
-turn-pass |
$TURN_PASS |
TURN password |
./bin/p2pal checkpoint list # List resumable transfers
./bin/p2pal checkpoint delete <id> # Delete a specific checkpoint
./bin/p2pal checkpoint clean # Remove expired checkpoints
./bin/p2pal checkpoint clean-all # Remove all checkpointsCheckpoints are stored in ~/.p2pal/checkpoints/ with a default TTL of 7 days.
If the network drops during a transfer, p2pal automatically reconnects, renegotiates the WebRTC session, and resumes from the last checkpoint.
If a transfer is manually interrupted (Ctrl+C), a checkpoint is saved. Resume it with:
# Check available checkpoints
./bin/p2pal checkpoint list
# Resume from the sender side
./bin/p2pal send --resume <transfer-id>make test # Unit tests (with -race)
make test_coverage # Unit tests + coverage report
make lint # go vet
make test_integration # End-to-end round-trip test (rebuilds Docker images)
make test_integration_quick # End-to-end round-trip test (skip build)
make test_resume # Resume-basic integration test (rebuilds Docker images)
make test_resume_quick # Resume-basic integration test (skip build)
make test_reconnect # Live network disconnect + auto-reconnect test (rebuilds Docker images)
make test_reconnect_quick # Live network disconnect + auto-reconnect test (skip build)| Component | Role |
|---|---|
cmd/p2pal |
CLI — send, recv, checkpoint commands |
cmd/server |
HTTP API + WebSocket signaling server |
internal/rtc |
WebRTC session: ICE, DataChannel, chunked transfer, checkpoints |
internal/client |
HTTP + WebSocket client library |
pkg/protocol |
Shared message types and transfer states |
| Redis | Ephemeral session state, tokens, TTL enforcement |
pending → ready → active → completed
↘ aborted
- Control plane (server): session creation, join codes, WebRTC signaling relay, TTL
- Data plane (clients): 64 KiB indexed chunks over WebRTC DataChannel, SHA-256 verification
- File bytes never reach the server
- Join codes and tokens are scoped to a single transfer and expire automatically
- No user accounts or persistent identity
- TURN relay is used only when direct P2P fails; server never has plaintext file access
- Wildcard file patterns (
p2pal send *.pdf) - Compression support (zstd)
- Bandwidth throttling
- Client config file (
~/.p2pal/config.toml) - End-to-end encryption (AES-256-GCM on top of DTLS)
- Shell completion scripts (bash/zsh/fish)