Encrypted file transfer over WebSocket
Install · Usage · How It Works · Development
drift is a single binary that lets you securely copy files and folders between two machines over WebSocket. It includes a built-in web UI with a two-pane file browser — no setup, no cloud, no SSH keys.
- Single self-contained binary (React frontend embedded)
- End-to-end encryption via X25519 key exchange + ChaCha20-Poly1305
- Optional password authentication
- Two-pane file browser UI (
hostname:/pwdon each side) - Large file support with chunked streaming and progress indication
- Recursive directory transfer (auto-compressed via tar.gz)
- Direct CLI file send mode (
--file) — no web UI needed - CLI commands to explore remote hosts (
ls,pull) — no web UI needed - Bidirectional — push files to remote or pull files from remote
- Both directions work from the browser UI (left pane = local, right pane = remote)
- Zero configuration — just run it
# Requires Rust 1.82+ and bun (or npm)
git clone https://github.com/aeroxy/drift.git
cd drift
cargo build --release
# Binary at target/release/drift# Serve on port 8000, browsing the current directory
drift serve --port 8000Visit http://localhost:8000 to see the file browser.
# Machine A (server)
drift serve --port 8000
# Machine B (connects to A)
drift serve --port 9000 --target 192.168.0.2:8000Both machines now show a two-pane UI. Select files on either side and copy them across. Both localhost:8000 and localhost:9000 show each other's files.
# List files at the root of a running drift server
drift ls --target 192.168.0.2:8000
# List a subdirectory
drift ls --target 192.168.0.2:8000 src# Pull a file
drift pull --target 192.168.0.2:8000 report.pdf
# Pull a directory (automatically decompressed)
drift pull --target 192.168.0.2:8000 my-project
# Pull to a specific output directory
drift pull --target 192.168.0.2:8000 data.csv --output /tmp/downloads# Send a file to a running drift server
drift send --target 192.168.0.2:8000 video.mp4
# Send a folder (automatically compressed)
drift send --target 192.168.0.2:8000 ./my-project
# With password
drift send --target 192.168.0.2:8000 data.zip --password secretThis connects, transfers the file, prints progress, and exits. No web server is started.
# Machine A
drift serve --port 8000 --password mysecret
# Machine B
drift serve --port 9000 --target 192.168.0.2:8000 --password mysecretMany modern file transfer tools optimize for maximum throughput using UDP-based protocols (WebRTC DataChannels, QUIC, etc.). Those can be faster for large files, but they routinely fail in the environments where you actually need ad-hoc transfers.
drift prioritizes maximum compatibility and zero configuration:
- Works out of the box on cloud GPU providers, serverless platforms, and inference hosts — most only permit outbound HTTP/WebSocket; no firewall exceptions needed
- Runs reliably inside Docker and Kubernetes pods — no UDP, no port-forwarding, no network admin required
- No auxiliary infrastructure — no STUN/TURN servers, no hole-punching, no coordination service
- Friction-free for AI agents — agents can exchange code, CSVs, model weights, and logs without any setup beyond a single binary
Trade-off: TCP head-of-line blocking means slightly lower throughput on very large files compared to UDP-based solutions. For the primary use case — fast, secure exchange of artifacts between machines — this is an acceptable trade. Future versions may add optional QUIC/WebTransport support for less restricted environments.
- Machine A starts a WebSocket server on the specified port
- Machine B connects and performs an X25519 Diffie-Hellman key exchange
- Both derive symmetric keys via HKDF-SHA256 (separate keys for each direction)
- If
--passwordis set, an HMAC challenge-response authenticates the peer - All subsequent messages are encrypted with ChaCha20-Poly1305
- Files are streamed in 64KB chunks over encrypted WebSocket binary frames
- Folders are compressed to
.tar.gzbefore transfer and extracted on the other side - The web UI at
localhost:<port>shows both file trees side by side
| Frame Type | Format | Purpose |
|---|---|---|
| Text (encrypted) | JSON ControlMessage |
Browse, transfer control, progress |
| Binary (encrypted) | [16B UUID][8B offset][chunk] |
File data |
- Forward secrecy: ephemeral X25519 keys per session
- Path traversal protection: all paths are canonicalized and validated against the root directory
- Authenticated encryption: ChaCha20-Poly1305 with monotonic nonce counters
drift <COMMAND> [OPTIONS]
Commands:
serve Start the drift server with web UI
send Send a file or folder to a remote drift server
ls List files on a remote drift server
pull Pull a file or folder from a remote drift server
| Command | Example | Description |
|---|---|---|
serve |
drift serve --port 8000 |
Starts web UI, waits for connections |
serve |
drift serve --port 9000 --target host:8000 |
Starts web UI and connects to remote |
send |
drift send --target host:8000 video.mp4 |
Sends file/folder and exits |
ls |
drift ls --target host:8000 [path] |
Lists files on remote |
pull |
drift pull --target host:8000 file.txt |
Pulls file/folder from remote |
Legacy flat args (drift --port 8000, drift --target host --file path) are still supported for backward compatibility.
# Frontend dev server (hot reload)
cd frontend && bun dev
# Rust backend (in another terminal)
cargo run -- --port 8000
# Build everything (frontend + backend)
cargo buildThe build.rs script automatically builds the React frontend before compiling Rust. The built assets are embedded in the binary via rust-embed.
Integration tests live in frontend/test/. They start real drift instances, transfer files via WebSocket, and verify MD5 checksums.
Set up test-resources (not committed — create your own):
test-resources/
├── host/ # Put at least one subdirectory with files here (tests folder transfer)
│ └── some-dir/
│ └── file.ext
└── client/ # Put at least one file here (tests file transfer)
└── file.ext
Any files work — the tests discover entries dynamically. The host directory should contain at least one subdirectory to exercise the tar.gz folder transfer path.
# Run integration tests (builds cargo first)
cd frontend && bun run testThe test suite:
- Backs up
test-resources/→test-resources-bak/ - Starts two drift instances (host + client) on random ports
- Pushes host files to client and client files to host via WebSocket
- Verifies MD5 checksums of all transferred files
- Verifies
.drift/temp directories are cleaned up - Restores
test-resources/from backup
drift/
├── src/
│ ├── main.rs # CLI entry point
│ ├── server/
│ │ ├── mod.rs # AppState, axum router
│ │ ├── ws_handler.rs # WebSocket connection handler
│ │ ├── file_api.rs # REST API (browse, info)
│ │ ├── browser_transfer.rs # Transfer orchestration (browser-initiated)
│ │ └── transfer_receiver.rs # Incoming file writer + decompression
│ ├── client/
│ │ ├── mod.rs # Outbound WS connection to --target
│ │ ├── send.rs # Direct file send mode
│ │ ├── browse.rs # Remote file listing (ls command)
│ │ └── pull.rs # Remote file pull (pull command)
│ ├── protocol/
│ │ ├── messages.rs # ControlMessage enum, TransferEntry
│ │ └── codec.rs # Binary frame encoding/decoding
│ ├── crypto/
│ │ ├── handshake.rs # X25519 key exchange
│ │ └── stream.rs # ChaCha20-Poly1305 encrypt/decrypt
│ ├── fileops/
│ │ ├── browse.rs # Directory listing with traversal protection
│ │ ├── reader.rs # Chunked async file reader
│ │ ├── writer.rs # Chunked async file writer with .part files
│ │ ├── compress.rs # Folder → tar.gz compression
│ │ └── decompress.rs # tar.gz → folder extraction
│ └── frontend.rs # rust-embed static serving
├── frontend/ # React + TypeScript + Tailwind v4
└── build.rs # Builds frontend before Rust compile
MIT