ptopnas

Stockage NAS pair-à-pair chiffré, auto-réparant · version 0.1.0 · build 213

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

BinaireRôle
ptopnasCLI de contrôle (init, start, push, pull, peers, quota, mount…)
ptopnas-daemonProcessus de fond : API REST, listener P2P, sync, GC
mount_ptopnasPilote 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

FichierContenuChiffrement
/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

Astuce Le port API (7475) est lié à 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/
Note La feature 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 :

CheminContenu
/usr/bin/ptopnasCLI de contrôle
/usr/sbin/ptopnas-daemonDaemon P2P
/usr/lib/ptopnas/mount_ptopnasPilote FUSE
/usr/lib/systemd/system/ptopnas.serviceUnité systemd
/etc/ptopnas/config.confConfiguration (conffile — préservé lors des mises à jour)
/var/lib/ptopnas/Données persistantes (manifest, chunks, identity)
/var/log/ptopnas/Journaux
Variable PTOPNAS_ROOT En définissant 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
Sans montage automatique Après un redémarrage, le daemon P2P redémarre automatiquement, mais le point de montage FUSE reste inactif jusqu'à ce qu'un utilisateur exécute 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

  1. L'utilisateur exécute ptopnas save-key une seule fois (saisit la passphrase).
  2. La clé dérivée (AES-256) est écrite dans /var/lib/ptopnas/identity/session.key en mode 0400.
  3. Au démarrage suivant du daemon (systemd ou manuel), il détecte le fichier, charge la clé en mémoire et spawne mount_ptopnas automatiquement.
  4. 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
Sécurité du keyfile Le fichier 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éfautDescription
peer_id(généré)Identifiant BLAKE3 hex du nœud (64 chars).
listen_addr0.0.0.0:7474Adresse d'écoute P2P.
api_addr127.0.0.1:7475Adresse d'écoute de l'API REST.

[storage]

CléDéfautDescription
contributed_gb10Espace disque en Go offert au réseau.

[mount]

CléDéfautDescription
mountpoint/mnt/ptopnasPoint de montage FUSE.
cache_mb256Cache LRU en Mo pour les lectures FUSE.
fuse_allow_otherfalseAutoriser d'autres utilisateurs à accéder au point de montage.

[crypto]

CléDéfautDescription
argon2_memory_kb65536Mémoire Argon2id en Ko (min recommandé : 64 Mo).
argon2_iterations3Nombre de passes Argon2id.
argon2_parallelism4Threads parallèles Argon2id.

[erasure]

CléDéfautDescription
data_shards10Shards de données Reed-Solomon.
parity_shards4Shards de parité (tolérance aux pannes).
Paramètres Reed-Solomon Avec 10+4 il faut au minimum 14 nœuds actifs pour distribuer un fichier. En dessous de 14 nœuds, le daemon enregistre les shards en local et tente de redistribuer lors des syncs suivants.

[network]

CléDéfautDescription
chunk_size_bytes4194304Taille des chunks (4 Mo par défaut).
connection_timeout_secs10Timeout des connexions P2P.
check_interval_secs30Intervalle du cycle de sync/challenge (secondes).
challenge_interval_secs3600Intervalle entre deux challenges PoS (secondes).

[discovery]

CléDéfautDescription
scan_modeautoauto | manual | disabled
scan_interval_secs60Intervalle de réécoute mDNS.
auto_addfalseAjouter automatiquement les pairs LAN découverts.

[gc]

CléDéfautDescription
enabledtrueActiver le GC des shards de pairs absents.
threshold_days180Jours d'absence avant éviction. Ce nœud annonce cette valeur aux pairs.

[relay]

CléDéfautDescription
enabledtrueRelayer les connexions TCP pour les nœuds derrière NAT strict.
max_connections10Connexions 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 :

ChampDescription
peer_idBLAKE3 hex d'une graine de 32 octets aléatoires
argon2_salt_hexSel aléatoire 16 octets pour la dérivation de clé
argon2_memory_kbParamètre Argon2 utilisé à l'init
argon2_iterationsParamètre Argon2 utilisé à l'init
argon2_parallelismParamètre Argon2 utilisé à l'init
passphrase_verifier_hexChiffrement 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 :

  1. L'identity string ptopnas:v1:… affichée à l'init.
  2. La passphrase choisie à l'init (mémorisée ou conservée hors ligne).
Ces deux éléments sont irremplaçables Sans l'identity string, le 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.
Ne sauvegardez pas 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
Passphrase requise Les opérations push et pull nécessitent qu'une clé de session soit active. Exécutez 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 APIDescription
contributed_bytesEspace offert au réseau (en octets)
used_bytesEspace actuellement utilisé par des shards
available_bytescontributed_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 :

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

ChampSourceSignification
uptime_secs/statusTemps de fonctionnement depuis le dernier démarrage
peers_count/statusNombre de pairs actifs dans la peer list
used_bytes/quotaEspace disque consommé par des shards
reliability_score/peersScore 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.

Note 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é).

// 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).

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.

Pas de dépendance externe Le client STUN est implémenté manuellement en ~100 lignes de Rust pur (UDP socket, parsing binaire RFC 5389) sans crate tierce.

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) :

  1. Les hashes sont recalculés bloc par bloc.
  2. Seuls les blocs dont le hash a changé déclenchent un appel POST /files/patch_chunk.
  3. 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.

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 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

CommandeDescription
ptopnas init [IDENTITY]Génère l'identité et la configuration par défaut. Accepte une identity string pour restaurer.
ptopnas startDémarre le daemon, dérive la clé, monte le FUSE
ptopnas stopArrête le daemon proprement
ptopnas statusAffiche l'état du nœud
ptopnas status syncRapport détaillé de synchronisation (récupérabilité par fichier)
ptopnas syncForce une synchronisation immédiate avec tous les pairs
ptopnas mount [--mountpoint <mp>]Demande la passphrase et monte le FUSE
ptopnas umountDémonte le FUSE
ptopnas save-keyDérive et enregistre la clé dans session.key (mode 0400) pour montage automatique
ptopnas forget-keySupprime 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 listListe 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 showAffiche le quota utilisé / disponible
ptopnas quota resize <nb>GoModifie la contribution de stockage à chaud
ptopnas idAffiche l'identity string ptopnas:v1:… à sauvegarder
ptopnas recover [IDENTITY] [--peer <addr>]Restaure les shards depuis les pairs après sinistre

Développement — Tests

TestDescription
test_three_node_upload_downloadUpload 20 Mo depuis A, distribution sur B+C, kill B, pull depuis A — vérification hash
test_quota_trackingVérifie que used_bytes augmente après un push
test_recovery_after_partial_lossUpload 4 Mo, kill B, pull depuis A — reconstruction RS vérifiée
test_reliability_penalty_via_apiVérifie que le score de fiabilité démarre à 100
test_api_statusVérifie la structure JSON de /status
test_api_rejects_unauthenticatedVé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