ptopnas est un système de stockage distribué pair-à-pair écrit en Rust. Chaque nœud contribue de l'espace disque au réseau et reçoit en échange la garantie que ses fichiers sont répliqués, chiffrés bout-en-bout et récupérables même si plusieurs nœuds tombent simultanément.
Tolérance aux pannes
Reed-Solomon 10+4 : tout fichier reste récupérable même si 4 nœuds sur 14 disparaissent simultanément.
Chiffrement total
AES-256-GCM sur chaque chunk, clé dérivée par Argon2id depuis la passphrase. Le manifest est chiffré par SQLCipher (AES-256-CBC).
Découverte automatique
mDNS sur le LAN et DHT Kademlia sur Internet. Détection NAT via STUN (RFC 5389).
Montage FUSE
Le NAS s'expose comme un répertoire local. Les modifications n'uploardent que les chunks de 4 Mo qui ont réellement changé (écriture partielle).
Composants
| Binaire | Rôle |
|---|---|
ptopnas | CLI de contrôle (init, start, push, pull, peers, quota, mount…) |
ptopnas-daemon | Processus de fond : API REST, listener P2P, sync, GC |
mount_ptopnas | Pilote FUSE — monté par ptopnas mount |
Architecture globale
┌──────────────────────────────────────────────────────────────┐
│ Utilisateur │
│ ptopnas push / mount_ptopnas (FUSE) │
└────────────────────┬─────────────────────────────────────────┘
│ HTTP REST (localhost)
┌────────────────────▼─────────────────────────────────────────┐
│ ptopnas-daemon │
│ │
│ ┌──────────┐ ┌────────────┐ ┌──────────┐ ┌──────────┐ │
│ │ API │ │ chunker │ │ erasure │ │ crypto │ │
│ │ (axum) │ │ (4 MB) │ │ RS 10+4 │ │ AES-GCM │ │
│ └──────────┘ └────────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────┐ ┌────────────┐ ┌──────────┐ ┌──────────┐ │
│ │ P2P │ │ Kademlia │ │ mDNS │ │ STUN │ │
│ │ TCP │ │ DHT │ │discovery │ │ NAT │ │
│ └──────────┘ └────────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────────┐ ┌────────────────────────────────┐ │
│ │ manifest.db │ │ quota.db │ │
│ │ (SQLCipher AES) │ │ (SQLite WAL) │ │
│ └──────────────────┘ └────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│ TCP (P2P port 7474)
┌────────────────────▼─────────────────────────────────────────┐
│ Autres nœuds ptopnas sur le réseau │
└──────────────────────────────────────────────────────────────┘
Bases de données locales
| Fichier | Contenu | Chiffrement |
|---|---|---|
/var/lib/ptopnas/manifest.db |
Métadonnées des fichiers, chunks, shards | SQLCipher AES-256-CBC, clé dérivée depuis le peer_id |
/var/lib/ptopnas/quota.db |
Pairs, scores, quota, shards hébergés | Non chiffré (pas de données sensibles) |
/var/lib/ptopnas/chunks/ |
Shards RS bruts (<fragment_id>) |
Chiffrés individuellement (ciphertext RS) |
/var/lib/ptopnas/identity/identity.conf |
Clé privée du nœud, sel Argon2, vérificateur | Répertoire mode 0700, fichier mode 0600 |
/var/lib/ptopnas/identity/session.key |
Clé de session pour montage automatique (optionnel) | Mode 0400, généré par ptopnas save-key |
Pipeline de données (upload)
Fichier source
│
▼ chunker: découpe en blocs de 4 MB
[chunk 0] [chunk 1] … [chunk N]
│
▼ crypto: compress (zstd si gain) + chiffre AES-256-GCM
[ciphertext 0] [ciphertext 1] …
│
▼ erasure: Reed-Solomon 10+4 → 14 shards par chunk
[shard 0..13] [shard 0..13] …
│
▼ distribution: les 14 shards sont répartis sur 14 nœuds distincts
Nœuds du réseau : chacun stocke 1 shard dans data/chunks/
Pipeline de données (download)
Collecte des shards disponibles (≥ 10 suffisent)
│
▼ erasure: reconstruct() → reconstitution du ciphertext
│
▼ crypto: déchiffre AES-256-GCM (clé de session)
│
▼ décompresse zstd si is_compressed = true
│
▼ concatène les chunks
Fichier original
Prérequis
- Linux x86_64 ou ARM64 (kernel ≥ 5.4)
- Rust toolchain stable ≥ 1.78 (
rustup update stable) - FUSE 3 :
apt install fuse3 libfuse3-dev - Build deps SQLCipher (OpenSSL compilé depuis les sources) :
apt install build-essential pkg-config perl - Ports réseau ouverts : P2P 7474/TCP et API 7475/TCP (locaux uniquement)
127.0.0.1 par défaut et ne doit jamais
être exposé publiquement. Seul le port P2P (7474) doit être accessible depuis Internet.
Compilation
# Cloner le dépôt
git clone https://…/ptopnas.git && cd ptopnas
# Build de production (optimisé, SQLCipher intégré)
cargo build --release
# Les binaires se trouvent dans target/release/
bundled-sqlcipher-vendored-openssl compile OpenSSL depuis les sources.
Le premier build peut prendre 3 à 5 minutes selon la machine.
Exécuter les tests
# Tests unitaires de tous les crates
cargo test
# Tests d'intégration (lance 3 daemons locaux sur des ports éphémères)
cargo test --test basic_flow
Déploiement via paquet .deb
Le script build_deb.sh compile l'application et produit un paquet Debian standard.
Le script deploy.sh enchaîne la compilation, le packaging et l'installation.
sudo ./deploy.sh
Le paquet installe les fichiers selon les conventions FHS Debian :
| Chemin | Contenu |
|---|---|
/usr/bin/ptopnas | CLI de contrôle |
/usr/sbin/ptopnas-daemon | Daemon P2P |
/usr/lib/ptopnas/mount_ptopnas | Pilote FUSE |
/usr/lib/systemd/system/ptopnas.service | Unité systemd |
/etc/ptopnas/config.conf | Configuration (conffile — préservé lors des mises à jour) |
/var/lib/ptopnas/ | Données persistantes (manifest, chunks, identity) |
/var/log/ptopnas/ | Journaux |
PTOPNAS_ROOT=/chemin/test, tous les chemins FHS sont remplacés
par des sous-répertoires de cette racine. Utile pour faire tourner plusieurs nœuds en dev.
Options de build_deb.sh
./build_deb.sh # compile + empaquète
./build_deb.sh --skip-build # utilise les binaires déjà compilés
./build_deb.sh --arch arm64 # cross-compilation (nécessite `cross`)
Initialisation d'un nœud
# 1. Installer le paquet
sudo ./deploy.sh
# 2. Initialiser l'identité cryptographique
ptopnas init # génère peer_id, sel Argon2, token API
# 3. Éditer la configuration si nécessaire
nano /etc/ptopnas/config.conf
# 4. Activer et démarrer via systemd
sudo systemctl enable --now ptopnas
# 5. Monter le filesystem (demande la passphrase)
ptopnas mount
ptopnas init génère un peer_id (BLAKE3 d'une graine aléatoire de 32 octets),
un sel Argon2id aléatoire, et une configuration par défaut. La commande est idempotente :
elle ne réinitialise pas l'identité si le fichier existe déjà.
À la fin de l'init, la commande affiche une identity string
(ptopnas:v1:…) à conserver précieusement — voir la section
Sauvegarde disaster-recovery.
Service systemd
Le paquet installe l'unité ptopnas.service dans
/usr/lib/systemd/system/. Le script postinst détecte
l'utilisateur courant ($SUDO_USER) et crée automatiquement un drop-in
/etc/systemd/system/ptopnas.service.d/user.conf avec
User= et Group= appropriés.
# Activer le démarrage automatique au boot + démarrer maintenant
sudo systemctl enable --now ptopnas
# Vérifier l'état
systemctl status ptopnas
# Voir les logs
journalctl -u ptopnas -f
# Arrêter
sudo systemctl stop ptopnas
ptopnas mount ou
qu'un fichier session.key soit présent (voir ci-dessous).
Montage automatique après redémarrage
Par défaut, le daemon nécessite une passphrase interactive pour monter le filesystem. Pour un NAS sans surveillance (serveur toujours allumé), ptopnas supporte un fichier de clé qui permet le montage automatique dès le démarrage du daemon.
Fonctionnement
- L'utilisateur exécute
ptopnas save-keyune seule fois (saisit la passphrase). - La clé dérivée (AES-256) est écrite dans
/var/lib/ptopnas/identity/session.keyen mode 0400. - Au démarrage suivant du daemon (systemd ou manuel), il détecte le fichier,
charge la clé en mémoire et spawne
mount_ptopnasautomatiquement. - Le NAS est opérationnel sans aucune intervention.
# Enregistrer la clé de session (daemon doit être en cours d'exécution)
ptopnas save-key
# → demande la passphrase, vérifie, écrit session.key (mode 0400)
# Supprimer le fichier (revient au mode passphrase manuelle)
ptopnas forget-key
session.key est protégé par mode 0400 et se trouve dans
/var/lib/ptopnas/identity/ (répertoire mode 0700, propriétaire = utilisateur
du service). Quiconque peut lire ce fichier peut accéder aux données chiffrées.
Le niveau de sécurité est équivalent à un keyfile LUKS : la protection repose
sur les droits Unix et la sécurité physique du système.
Référence config.conf
[node]
| Clé | Défaut | Description |
|---|---|---|
peer_id | (généré) | Identifiant BLAKE3 hex du nœud (64 chars). |
listen_addr | 0.0.0.0:7474 | Adresse d'écoute P2P. |
api_addr | 127.0.0.1:7475 | Adresse d'écoute de l'API REST. |
[storage]
| Clé | Défaut | Description |
|---|---|---|
contributed_gb | 10 | Espace disque en Go offert au réseau. |
[mount]
| Clé | Défaut | Description |
|---|---|---|
mountpoint | /mnt/ptopnas | Point de montage FUSE. |
cache_mb | 256 | Cache LRU en Mo pour les lectures FUSE. |
fuse_allow_other | false | Autoriser d'autres utilisateurs à accéder au point de montage. |
[crypto]
| Clé | Défaut | Description |
|---|---|---|
argon2_memory_kb | 65536 | Mémoire Argon2id en Ko (min recommandé : 64 Mo). |
argon2_iterations | 3 | Nombre de passes Argon2id. |
argon2_parallelism | 4 | Threads parallèles Argon2id. |
[erasure]
| Clé | Défaut | Description |
|---|---|---|
data_shards | 10 | Shards de données Reed-Solomon. |
parity_shards | 4 | Shards de parité (tolérance aux pannes). |
[network]
| Clé | Défaut | Description |
|---|---|---|
chunk_size_bytes | 4194304 | Taille des chunks (4 Mo par défaut). |
connection_timeout_secs | 10 | Timeout des connexions P2P. |
check_interval_secs | 30 | Intervalle du cycle de sync/challenge (secondes). |
challenge_interval_secs | 3600 | Intervalle entre deux challenges PoS (secondes). |
[discovery]
| Clé | Défaut | Description |
|---|---|---|
scan_mode | auto | auto | manual | disabled |
scan_interval_secs | 60 | Intervalle de réécoute mDNS. |
auto_add | false | Ajouter automatiquement les pairs LAN découverts. |
[gc]
| Clé | Défaut | Description |
|---|---|---|
enabled | true | Activer le GC des shards de pairs absents. |
threshold_days | 180 | Jours d'absence avant éviction. Ce nœud annonce cette valeur aux pairs. |
[relay]
| Clé | Défaut | Description |
|---|---|---|
enabled | true | Relayer les connexions TCP pour les nœuds derrière NAT strict. |
max_connections | 10 | Connexions relayées simultanées maximum. |
Exemple complet
[node]
peer_id = "f5ff57b85bb8c639…"
listen_addr = "0.0.0.0:7474"
api_addr = "127.0.0.1:7475"
[storage]
contributed_gb = 50
[mount]
mountpoint = "/mnt/ptopnas"
cache_mb = 512
fuse_allow_other = false
[crypto]
argon2_memory_kb = 65536
argon2_iterations = 3
argon2_parallelism = 4
[erasure]
data_shards = 10
parity_shards = 4
[network]
chunk_size_bytes = 4194304
connection_timeout_secs = 10
check_interval_secs = 30
challenge_interval_secs = 3600
[discovery]
scan_mode = "auto"
auto_add = false
[gc]
enabled = true
threshold_days = 180
Identité et clés
Le fichier identity/identity.conf (TOML, mode 600) contient :
| Champ | Description |
|---|---|
peer_id | BLAKE3 hex d'une graine de 32 octets aléatoires |
argon2_salt_hex | Sel aléatoire 16 octets pour la dérivation de clé |
argon2_memory_kb | Paramètre Argon2 utilisé à l'init |
argon2_iterations | Paramètre Argon2 utilisé à l'init |
argon2_parallelism | Paramètre Argon2 utilisé à l'init |
passphrase_verifier_hex | Chiffrement AES-GCM de "ptopnas-key-ok" pour valider la passphrase |
Token d'API
À chaque démarrage, le daemon génère un token aléatoire de 32 octets (64 hex) écrit dans
identity/api_token. Toutes les requêtes API doivent inclure
Authorization: Bearer <token>.
Sauvegarde disaster-recovery
À la fin de ptopnas init, le programme affiche une identity string
de la forme :
ptopnas:v1:eyJwZWVyX2lkIjoiZjVm…
Cette chaîne est un JSON base64 qui contient le peer_id, le sel Argon2
et les paramètres de dérivation — mais pas la passphrase.
Pour restaurer complètement un nœud, il faut les deux éléments :
- L'identity string
ptopnas:v1:…affichée à l'init. - La passphrase choisie à l'init (mémorisée ou conservée hors ligne).
peer_id et le sel Argon2 sont perdus :
le manifest chiffré ne peut plus être ouvert. Sans la passphrase, les chunks
ne peuvent plus être déchiffrés. La perte de l'un ou de l'autre rend tous
vos fichiers inaccessibles.
identity.conf
Ce fichier est recréé automatiquement par ptopnas init --backup <identity-string>.
La sauvegarde portable est uniquement la chaîne ptopnas:v1:….
Procédure de restauration
# Sur une nouvelle machine (ou après réinstallation) :
ptopnas init --backup "ptopnas:v1:eyJwZWVyX2lkIjoi…" --peer 192.168.1.5:7474
# --backup : identity string sauvegardée à l'init
# --peer : adresse d'un pair qui héberge des shards du nœud restauré
# (pour récupérer le manifest depuis le réseau)
# La passphrase est demandée interactivement pour déchiffrer le manifest récupéré.
Chiffrement
Clé de session (chunks)
Les chunks sont chiffrés avec AES-256-GCM. La clé de 32 octets est dérivée
depuis la passphrase par Argon2id avec les paramètres stockés dans identity.conf.
La clé réside uniquement en mémoire (structure Zeroizing<[u8;32]>) et est
effacée à la terminaison du processus.
La commande ptopnas mount demande la passphrase au terminal, dérive la clé,
l'envoie via POST /session/key, puis démarre mount_ptopnas.
Clé du manifest (SQLCipher)
La clé du manifest est dérivée de façon déterministe :
blake3::derive_key("ptopnas manifest v1", peer_id_bytes).
Ainsi, elle n'est jamais stockée sur disque et est recalculée à chaque démarrage.
Démarrage et arrêt
Via systemd (recommandé)
sudo systemctl start ptopnas # démarrer
sudo systemctl stop ptopnas # arrêter
systemctl status ptopnas # état
journalctl -u ptopnas -f # logs en temps réel
Via CLI (sans systemd)
# Démarrer le daemon en premier plan
ptopnas start
# Vérifier qu'il tourne
ptopnas status # ou : curl -H "Authorization: Bearer $(cat /var/lib/ptopnas/identity/api_token)" \
# http://127.0.0.1:7475/status
# Arrêter proprement
ptopnas stop
Les logs sont écrits dans /var/log/ptopnas/daemon.log.
Le niveau de log est contrôlable via RUST_LOG
(ex. RUST_LOG=debug dans le drop-in systemd ou en variable d'env).
Gestion des pairs
# Lister les pairs connus
ptopnas peers list
# Ajouter un pair manuellement
ptopnas peers add 192.168.1.10:7474
# Voir l'impact avant de retirer un pair
ptopnas peers impact 192.168.1.10:7474
# Retirer un pair
ptopnas peers remove 192.168.1.10:7474
# Scanner le LAN (mDNS)
ptopnas peers scan
Score de fiabilité
Chaque pair démarre avec un score de 100. À chaque challenge
proof-of-storage échoué, le score est multiplié par (1 - pénalité%/100)
(défaut 5 %). Le score ne peut pas aller en dessous de 0.
Les pairs sont triés lors de la placement de shards par un score combiné :
score = reliability × ln(max(threshold_days, 30) / 30).
Les nœuds plus fiables et annonçant une longue rétention reçoivent plus de shards.
Fichiers (push / pull)
# Uploader un fichier
ptopnas push /chemin/local/fichier.iso photos/vacances/fichier.iso
# Télécharger un fichier
ptopnas pull photos/vacances/fichier.iso /chemin/local/fichier.iso
# Lister les fichiers
ptopnas ls /
ptopnas ls photos/
# Supprimer un fichier
ptopnas rm photos/vacances/fichier.iso
ptopnas mount en amont, ou utilisez le montage FUSE directement.
Montage FUSE
# Monter le filesystem (demande la passphrase)
ptopnas mount
# Démonter
ptopnas umount
# Montage automatique sans intervention — voir section "Montage automatique"
ptopnas save-key # une seule fois ; le daemon monte seul au prochain démarrage
Une fois monté, le répertoire mountpoint (défaut /mnt/ptopnas)
se comporte comme un répertoire local. Les lectures déclenchent un téléchargement transparent,
les écritures ne re-uploardent que les chunks de 4 Mo qui ont changé
(détection par hash BLAKE3 par bloc).
Quota de stockage
# Afficher le quota
ptopnas quota
# Modifier la contribution (en Go)
ptopnas quota resize 100
| Champ API | Description |
|---|---|
contributed_bytes | Espace offert au réseau (en octets) |
used_bytes | Espace actuellement utilisé par des shards |
available_bytes | contributed_bytes − used_bytes |
Garbage Collection
Le daemon évince périodiquement les shards des pairs dont on n'a pas eu de nouvelles
depuis plus de threshold_days jours. Cette durée est annoncée par chaque pair
via les messages P2P AddPeer / Pong — un pair qui réclame une
longue rétention obtient la même courtoisie.
Le GC est implémenté par un cycle qui appelle quota::list_stale_peer_ids()
(requête SQL sur quota.db), puis supprime les fichiers correspondants dans
data/chunks/ et met à jour used_bytes.
Vérification de la synchronisation
La commande ptopnas check calcule quel pourcentage (en espace de stockage)
des fichiers est entièrement récupérable en cas de perte de nœuds.
ptopnas check
Exemple de sortie
════════════════════════════════════════════════════════════
Vérification de la synchronisation
════════════════════════════════════════════════════════════
[████████████████████████████████████░░░░] 92.3%
Fichiers : 11 / 12 récupérables
Données : 18.4 GB / 19.9 GB récupérables (92.3%)
Fichiers pas encore entièrement synchronisés :
FICHIER TAILLE CHUNKS STATUT
─────────────────────────────────────────────────────────
videos/brut/session_2025.mkv 1.5 GB 6/14 4/14 chunks sains
════════════════════════════════════════════════════════════
Logique de récupérabilité
Un fichier est récupérable si et seulement si chacun de ses chunks
possède au moins data_shards shards non marqués lost
dans le manifest. Avec la configuration 10+4 :
- 14 shards distribués par chunk — 10 suffisent pour reconstruire.
- Un chunk avec 9 shards disponibles est considéré à risque.
- Les shards en cours de distribution (
peer_id = 'distributing') ne comptent pas encore.
API sous-jacente
GET /files/check
Authorization: Bearer <token>
→ {
"total_files": 12,
"recoverable_files": 11,
"total_bytes": 21368709120,
"recoverable_bytes": 19748864000,
"sync_pct": 92.3,
"files": [
{
"virtual_path": "videos/brut/session_2025.mkv",
"size_bytes": 1610612736,
"recoverable": false,
"healthy_chunks": 4,
"total_chunks": 14
},
…
]
}
Supervision
TOKEN="$(cat /var/lib/ptopnas/identity/api_token)"
# État du daemon
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:7475/status
# Quota
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:7475/quota
# Liste des pairs avec scores
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:7475/peers
# Forcer une synchronisation immédiate
ptopnas sync
Métriques clés
| Champ | Source | Signification |
|---|---|---|
uptime_secs | /status | Temps de fonctionnement depuis le dernier démarrage |
peers_count | /status | Nombre de pairs actifs dans la peer list |
used_bytes | /quota | Espace disque consommé par des shards |
reliability_score | /peers | Score de fiabilité [0–100] de chaque pair |
API REST — Authentification
Toutes les routes (sauf /status en mode non-auth — voir ci-dessous) exigent le header :
Authorization: Bearer <api_token>
Le token est disponible dans identity/api_token.
GET /status renvoie 401 si aucun token valide n'est fourni.
Il n'existe pas d'endpoint public.
API REST — Status
GET /status
Retourne l'état global du nœud.
{
"version": "0.1.0",
"peer_id": "f5ff57b85bb8c639…",
"peers_count": 3,
"quota_used": 1073741824,
"quota_total": 10737418240,
"uptime_secs": 3600,
"mountpoint": "/mnt/ptopnas",
"mounted": true,
"argon2_salt_hex": "…",
"argon2_memory_kb": 65536,
"argon2_iterations": 3,
"argon2_parallelism": 4,
"passphrase_verifier_hex": "…"
}
API REST — Pairs
GET /peers
Liste tous les pairs connus avec leurs statistiques.
POST /peers/add
{ "addr": "192.168.1.10:7474" }
Enregistre un pair, envoie un message AddPeer, met à jour quota.db.
GET /peers/impact?addr=…
Analyse l'impact du retrait d'un pair : fichiers affectés, récupérabilité.
POST /peers/remove
{ "addr": "192.168.1.10:7474" }
Retire le pair, marque ses shards comme «lost», supprime les enregistrements quota.
GET /discovery/peers
Retourne la liste des pairs découverts via mDNS qui n'ont pas encore été ajoutés.
API REST — Fichiers
GET /files?dir=…
Liste les entrées d'un répertoire virtuel (fichiers et sous-répertoires directs).
GET /files/tree?dir=…
Arborescence récursive JSON.
POST /files/push
{
"local_path": "/tmp/ptopnas_upload.bin",
"virtual_path": "photos/img.jpg"
}
Encode, chiffre, distribue le fichier. local_path doit être sous /tmp/ptopnas_.
GET /files/pull?virtual_path=…&dest_path=…
Télécharge et reconstruit un fichier. dest_path doit être sous /tmp/ptopnas_.
POST /files/patch_chunk NEW
{
"virtual_path": "docs/archive.tar",
"chunk_index": 2,
"local_path": "/tmp/ptopnas_chunk2.bin"
}
Re-chiffre et redistribue un seul chunk (4 Mo) sans retoucher les autres. Utilisé par le pilote FUSE pour l'écriture partielle.
DELETE /files/<virtual_path>
Supprime le fichier du manifest et retire ses shards des pairs distants.
POST /files/rename
{ "old_path": "a/b.txt", "new_path": "c/d.txt" }
GET /files/attr?path=…
Retourne les attributs d'un fichier (taille, date, is_dir) — utilisé par le pilote FUSE.
POST /files/queue
Place un fichier en file d'attente (inbox) pour distribution différée.
POST /dirs/create
{ "virtual_path": "photos/2025" }
Crée un répertoire virtuel dans le manifest.
API REST — Shards
POST /shards/store
Reçoit un shard binaire en corps (Content-Type: application/octet-stream).
Le header JSON de contexte est dans le body JSON StoreShardRequest.
Limite de corps : 32 Mo.
GET /shards/<fragment_id>
Retourne le contenu binaire brut d'un shard stocké localement.
POST /shards/challenge
{
"type": "pos",
"fragment_id": "abc123_3",
"offset": 1024,
"length": 64
}
Challenge proof-of-storage : le pair retourne les octets [offset, offset+length) du shard.
API REST — Session
POST /session/key
{
"key_hex": "…32 octets hex (64 chars)…",
"verifier_hex": "…optionnel, pour valider la passphrase…"
}
Installe la clé de session en mémoire. Appelé automatiquement par ptopnas mount
après la dérivation Argon2id. La clé est zéroïsée à la terminaison ou via DELETE.
DELETE /session/key
Efface la clé de session — équivalent d'un «verrouillage» du NAS.
POST /quota/resize
{ "contributed_gb": 100 }
Modifie la contribution de stockage à chaud.
API REST — Administration
POST /sync NEW
Force une synchronisation immédiate avec tous les pairs (redistribution des shards manquants,
vérification de l'intégrité). Retourne 200 {"status":"ok"} une fois terminé,
ou 202 {"status":"running"} si la sync dépasse 120 secondes.
Utilisé par ptopnas sync.
POST /admin/restore
Restaure un manifest depuis une sauvegarde externe : ouvre le fichier source comme une base SQLCipher et importe le contenu dans le manifest actif. À utiliser après une perte de manifest.db.
POST /shutdown
Demande un arrêt propre du daemon.
Internals — Erasure coding Reed-Solomon 10+4
La crate reed_solomon_erasure (Galois GF(2⁸)) est utilisée pour encoder chaque
ciphertext en 14 shards (10 données + 4 parité).
- Taille d'un shard ≈
ciphertext_size / 10(aligné sur 10 octets). - Pour reconstruire, il suffit de n'importe quels 10 shards sur 14.
- Le daemon tente d'abord les shards de données (indices 0–9) ; si certains manquent,
il appelle
reconstruct()avec les parités disponibles. - Les paramètres
data_shardsetparity_shardssont stockés par chunk dans le manifest pour que les fichiers restent décodables même si la configuration change.
// Encode
let shards = erasure::encode(&ciphertext, 10, 4)?; // → Vec<Vec<u8>>, len = 14
// Reconstruct (some shards may be None)
erasure::reconstruct(&mut shards, 10, 4)?;
let data = shards[..10].concat(); // puis tronqué à ciphertext_size
Internals — DHT Kademlia
Le module daemon/src/kademlia.rs implémente une table de routage Kademlia
standard (XOR distance, 256 k-buckets, k=20 par bucket).
- Les
NodeIdsont les BLAKE3 hex despeer_id(256 bits). - Le bootstrap envoie
FindNode(our_id)à tous les pairs connus ; leurs réponses (FindNodeResponse) peuplent la table de routage transitivement. - Les messages P2P
FindNode/FindNodeResponsesont sérialisés en JSON via le canal TCP P2P. - Eviction simple : quand un bucket est plein (20 entrées), la plus ancienne est éjectée. Une implémentation de production pinguerait l'ancienne entrée avant éjection.
Internals — Détection NAT / STUN
Au démarrage, le daemon envoie une STUN Binding Request (RFC 5389) en UDP
à plusieurs serveurs publics (stun.l.google.com:19302,
stun.cloudflare.com:3478) et extrait l'adresse externe depuis l'attribut
XOR-MAPPED-ADDRESS.
Cette adresse est annoncée aux pairs dans les messages AddPeer / Pong,
permettant aux nœuds derrière NAT d'être joignables par leurs pairs.
Internals — manifest.db chiffré (SQLCipher)
Le manifest est une base SQLite chiffrée via SQLCipher AES-256-CBC.
La clé est fournie à l'ouverture via PRAGMA key = '<hex>' suivi de
PRAGMA kdf_iter = 64000 pour réduire le délai de dérivation PBKDF2.
La clé est dérivée de façon déterministe (sans stockage) :
let key_bytes = blake3::derive_key("ptopnas manifest v1", peer_id.as_bytes());
let key_hex = hex::encode(key_bytes);
manifest::open_db(path, Some(&key_hex))?;
Si un manifest non chiffré existait (migration), open_db détecte l'erreur
à la première lecture et ré-encrypte via sqlcipher_export().
Schéma
files (id, virtual_path UNIQUE, real_size, blake3_hash, upload_date, is_dir, inbox_path)
chunks (id, chunk_id, file_id→files, chunk_offset, original_size,
is_compressed, nonce_hex, ciphertext_size, data_shards, parity_shards)
shards (id, fragment_id UNIQUE, chunk_id, shard_index, peer_id)
Internals — Écriture partielle FUSE
Le pilote FUSE (mount/src/fs.rs) maintient un WriteBuffer par
fichier ouvert en écriture. À l'ouverture, le contenu courant est téléchargé et les
hashes BLAKE3 de chaque bloc de 4 Mo sont mémorisés.
À la fermeture (flush) :
- Les hashes sont recalculés bloc par bloc.
- Seuls les blocs dont le hash a changé déclenchent un appel
POST /files/patch_chunk. - Les blocs inchangés ne génèrent aucun trafic réseau.
Si le fichier n'existait pas encore (nouveau fichier), on bascule sur un
POST /files/push standard.
Internals — Proof-of-Storage
Périodiquement (toutes les challenge_interval_secs), le daemon envoie un
challenge à chaque pair : «retourne-moi les octets [offset, offset+length) du shard X».
Le pair répond avec les octets correspondants.
- En cas de succès :
update_peer_last_seen()— le timer GC est réinitialisé. - En cas d'échec ou timeout :
penalize_peer(addr, 5.0)— le score diminue de 5%.
Internals — Scores de fiabilité & placement
Lors de la distribution d'un nouveau fichier, les pairs sont triés par score de placement :
fn peer_placement_score(reliability: f64, threshold_days: u32) -> f64 {
let t = threshold_days.max(30) as f64;
reliability * (t / 30.0_f64).ln().max(1.0)
}
- Le logarithme croît sous-linéairement : doubler le threshold ne double pas le score.
- Floor à 30 jours pour éviter ln(0) et assurer que les nœuds à court threshold reçoivent tout de même des shards.
- Un nœud avec reliability=100 et threshold=180 jours score ≈ 179.
Le GC réciproque utilise le threshold_days annoncé par chaque pair :
un pair qui réclame 360 jours de rétention est conservé deux fois plus longtemps.
Référence CLI — ptopnas
| Commande | Description |
|---|---|
ptopnas init [IDENTITY] | Génère l'identité et la configuration par défaut. Accepte une identity string pour restaurer. |
ptopnas start | Démarre le daemon, dérive la clé, monte le FUSE |
ptopnas stop | Arrête le daemon proprement |
ptopnas status | Affiche l'état du nœud |
ptopnas status sync | Rapport détaillé de synchronisation (récupérabilité par fichier) |
ptopnas sync | Force une synchronisation immédiate avec tous les pairs |
ptopnas mount [--mountpoint <mp>] | Demande la passphrase et monte le FUSE |
ptopnas umount | Démonte le FUSE |
ptopnas save-key | Dérive et enregistre la clé dans session.key (mode 0400) pour montage automatique |
ptopnas forget-key | Supprime session.key — désactive le montage automatique |
ptopnas push <local> [--path <virtual>] | Uploade un fichier |
ptopnas pull <virtual> [--dest <local>] | Télécharge un fichier |
ptopnas ls [<dir>] | Liste un répertoire virtuel |
ptopnas rm <virtual> | Supprime un fichier |
ptopnas peers list | Liste les pairs avec scores de fiabilité |
ptopnas peers add <addr> | Ajoute un pair manuellement |
ptopnas peers remove <addr> [-f] | Retire un pair (–f pour ignorer l'impact) |
ptopnas peers scan [--add-all] | Scan mDNS LAN ; --add-all pour ajouter automatiquement |
ptopnas quota show | Affiche le quota utilisé / disponible |
ptopnas quota resize <nb>Go | Modifie la contribution de stockage à chaud |
ptopnas id | Affiche l'identity string ptopnas:v1:… à sauvegarder |
ptopnas recover [IDENTITY] [--peer <addr>] | Restaure les shards depuis les pairs après sinistre |
Développement — Tests
| Test | Description |
|---|---|
test_three_node_upload_download | Upload 20 Mo depuis A, distribution sur B+C, kill B, pull depuis A — vérification hash |
test_quota_tracking | Vérifie que used_bytes augmente après un push |
test_recovery_after_partial_loss | Upload 4 Mo, kill B, pull depuis A — reconstruction RS vérifiée |
test_reliability_penalty_via_api | Vérifie que le score de fiabilité démarre à 100 |
test_api_status | Vérifie la structure JSON de /status |
test_api_rejects_unauthenticated | Vérifie que les requêtes sans token retournent 401 |
Développement — Build & Deploy
# Build complet + tests + deploy
cargo build --release
cargo test
sudo ./deploy.sh
Le numéro de build est stocké dans build_number et incrémenté à chaque
exécution de deploy.sh. La documentation doit être mise à jour si les
endpoints API, les paramètres de configuration ou le schéma de la base de données changent.
Structure du dépôt
crates/
core/ — types, chunker, crypto, erasure, manifest, paths
daemon/ — API REST, P2P, Kademlia, quota, sync, GC, challenge
cli/ — commandes ptopnas (init, start, push, pull…)
mount/ — pilote FUSE (fs.rs, client.rs, cache.rs)
integration-tests/
tests/
integration/ — basic_flow.rs (6 tests d'intégration)
docs/
index.html — cette documentation