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.
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:
-peers listfilename.dstore in outfiles/Install and start the agent:
./scripts/install-user-service.sh
systemctl --user start peertopeer-dstore
systemctl --user enable peertopeer-dstore
Or run directly:
bin/dstore agent -origin outfiles -shards .dstore-shards -addr :8080
Drop any file into the outfiles/ directory on any peer. The agent automatically:
-peers shard nodes.dstore manifest in outfiles/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.
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.
| 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 |
bin/dstore peers
bin/dstore peers -online-only
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 |
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 |
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
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 --build
See docs/docker-peers.md for verification, storage
paths, and the required /dev/net/tun Docker capability.
Install .dstore file association:
./scripts/install-desktop-integration.sh
After installation, double-clicking a .dstore file restores the original file.
| 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 |
go test ./...
7 commits
Go
92.4%
Shell
7.1%
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.
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:
-peers listfilename.dstore in outfiles/Install and start the agent:
./scripts/install-user-service.sh
systemctl --user start peertopeer-dstore
systemctl --user enable peertopeer-dstore
Or run directly:
bin/dstore agent -origin outfiles -shards .dstore-shards -addr :8080
Drop any file into the outfiles/ directory on any peer. The agent automatically:
-peers shard nodes.dstore manifest in outfiles/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.
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.
| 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 |
bin/dstore peers
bin/dstore peers -online-only
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 |
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 |
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
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 --build
See docs/docker-peers.md for verification, storage
paths, and the required /dev/net/tun Docker capability.
Install .dstore file association:
./scripts/install-desktop-integration.sh
After installation, double-clicking a .dstore file restores the original file.
| 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 |
go test ./...
7 commits
Go
92.4%
Shell
7.1%