Distributed Store
Peer-to-peer file storage for an existing Headscale/Tailscale mesh.
The design is documented in AGENT.md. The deployed desktop watcher encrypts files, splits each chunk into 5 data shards plus 2 parity shards, distributes those shards across online peers, and writes a portable .dstore manifest that can restore the original file. New files are processed in bounded 16 MiB chunks so memory use does not grow with the file size; standalone commands retain the legacy 2+1 defaults unless their layout flags are set.
Important: the .dstore manifest contains the AES key. Anyone who receives the manifest and can reach at least 5 shard URLs for every version-2 chunk can recreate the original file.
Prerequisites
- Go 1.24+
- Tailscale or Headscale running on each peer
- Each peer must have a unique hostname configured in Tailscale
Quick Start
Build the binary:
go build -o bin/dstore ./cmd/dstore
Run the agent (serves shards + watches for new files):
bin/dstore agent -origin outfiles -shards .dstore-shards -addr :8080
To use specific shard nodes instead of random online Tailscale peers, pass -peers:
bin/dstore agent -origin outfiles -shards .dstore-shards -addr :8080 \ -peers node-a=100.64.0.10:8080,node-b=100.64.0.11:8080,node-c=100.64.0.12:8080
The peer format is hostname=host[:port]. If fewer peers are listed than shard count, shards are assigned round-robin across those peers.
Drop a file or directory into outfiles/. The agent will process regular files recursively and leave .dstore manifests beside each original path:
- Encrypt it with AES-256-GCM
- Split each encrypted chunk into 7 Reed-Solomon shards (5 data + 2 parity)
- Randomly select online peers from your Tailscale mesh, or use the explicit
-peerslist - Push each shard to a peer via HTTP PUT
- Store a local copy as well
- Create
filename.dstoreinoutfiles/ - Remove the original file
Peer-to-Peer Workflow
On each mesh peer
Install and start the agent:
./scripts/install-user-service.sh
systemctl --user start peertopeer-dstore
systemctl --user enable peertopeer-dstoreOr run directly:
bin/dstore agent -origin outfiles -shards .dstore-shards -addr :8080
Storing a file
Drop any file into the outfiles/ directory on any peer. The agent automatically:
- Picks random online peers from your Tailscale mesh, or uses the configured
-peersshard nodes - Pushes shards to each peer's shard server
- Creates a
.dstoremanifest inoutfiles/
Restoring a file
Double-click (after desktop integration):
./scripts/install-desktop-integration.sh xdg-open outfiles/example.txt.dstore
CLI:
bin/dstore restore -stub outfiles/example.txt.dstore
Restore fetches shards from the URLs in the manifest. Only 5 of 7 shards per chunk need to be reachable. If a peer is offline, the remaining shards are sufficient.
Sharing a file
Email the .dstore file to another mesh member. They can restore it on any peer that can reach at least 5 of the 7 shard URLs for every chunk.
Commands
| Command | Description |
|---|---|
peers |
List Tailscale peers visible to this node |
agent |
Combined shard server + file watcher (recommended) |
watch |
Watch a directory for new files to shard |
process |
One-shot: process all current files in a directory |
restore |
Restore a file from a .dstore manifest |
serve-shards |
Run only the shard HTTP server |
peers
bin/dstore peers bin/dstore peers -online-only
agent
bin/dstore agent \
-origin outfiles \
-shards .dstore-shards \
-addr :8080 \
-base-url "" \
-interval 2s \
-peers node-a=100.64.0.10:8080,node-b=100.64.0.11:8080| Flag | Default | Description |
|---|---|---|
-origin |
outfiles |
Directory to watch for files to shard |
-shards |
.dstore-shards |
Directory for local shard storage |
-addr |
:8080 |
HTTP listen address |
-base-url |
auto | Override URL in manifests (default: Tailscale IPv4) |
-interval |
2s |
Watch poll interval |
-peers |
auto | Comma-separated explicit shard nodes as hostname=host[:port]; overrides random peer selection |
-peer-ports |
none | Comma-separated hostname:port overrides for auto-selected peers |
-data-shards |
2 |
Number of data shards (the active service passes 5) |
-parity-shards |
1 |
Number of parity shards (the active service passes 2) |
-chunk-size |
16 MiB |
Plaintext bytes processed at a time |
-max-file-bytes |
64 MiB |
Maximum input file size for this command |
restore
bin/dstore restore \ -stub outfiles/photo.dstore \ -shards .dstore-shards \ -output photo.jpg
| Flag | Default | Description |
|---|---|---|
-stub |
(required) | Path to the .dstore manifest |
-shards |
(none) | Local shard directory fallback |
-output |
(stub path minus .dstore) |
Output path for restored file |
Service Management
systemctl --user start peertopeer-dstore # start systemctl --user stop peertopeer-dstore # stop systemctl --user status peertopeer-dstore # status systemctl --user enable peertopeer-dstore # auto-start on login journalctl --user -u peertopeer-dstore -f # follow logs
Docker peers on the Headscale server
To run multiple storage peers on the same Linux machine as an existing
Headscale server, use the included Compose stack. It creates two separate
Tailscale nodes (peer-a and peer-b) by default with persistent identities
and storage; --profile extra adds peer-c and peer-d. No public dstore
ports are exposed.
The current erasure layout needs three ready storage peers for automatic file placement, so enable the extra profile or add a remote dstore node before using a peer as a file origin.
Create a reusable Headscale pre-authentication key, copy the environment template, and start the stack:
headscale users create dstore
headscale preauthkeys create --user dstore --reusable --expiration 24h
cp .env.peers.example .env.peers
# Set HEADSCALE_URL and HEADSCALE_AUTHKEY in .env.peers.
docker compose --env-file .env.peers -f docker-compose.peers.yml up -d --buildSee docs/docker-peers.md for verification, storage
paths, and the required /dev/net/tun Docker capability.
Desktop Integration
Install .dstore file association:
./scripts/install-desktop-integration.sh
After installation, double-clicking a .dstore file restores the original file.
Security
| Concern | Mechanism |
|---|---|
| Transport encryption | WireGuard (Tailscale/Headscale) |
| Peer authentication | Tailscale identity (tailscale whois) |
| Shard confidentiality | AES-256-GCM before sharding |
| Key distribution | Lives only in the .dstore manifest |
| Tamper detection | Content-addressed shard IDs (hash = filename) |
| Delete abuse prevention | Only the original writer can delete a shard |