From 57b9b44d8a0c6cfc1eeb03eb16944accba19903a Mon Sep 17 00:00:00 2001 From: Ben_Kosytorz Date: Thu, 24 Sep 2026 20:12:51 +0200 Subject: [PATCH] Add Docker and Nix packaging for the node server/docker/: multi-stage image (rust:1-slim build, trixie runtime) with all four binaries plus the pinned yggdrasil 0.5.14 .deb (sha256-verified, fetched from the official release), a compose example per household (host network, NET_ADMIN, /dev/net/tun, init: true), a German-commented node.toml.example (safe default: leech mode) and a README covering privileges, two-household wiring, leech mode and the kill switch. Image build and smoke tests verified locally with docker 29.8.1. flake.nix: builds the workspace as one spidernet package via rustPlatform.buildRustPackage (cargoLock against the committed Cargo.lock) plus an experimental NixOS module; written without a local Nix and marked accordingly. --- flake.nix | 193 +++++++++++++++++++++++++++++++ server/.dockerignore | 9 ++ server/docker/Dockerfile | 67 +++++++++++ server/docker/README.md | 125 ++++++++++++++++++++ server/docker/docker-compose.yml | 74 ++++++++++++ server/docker/node.toml.example | 95 +++++++++++++++ 6 files changed, 563 insertions(+) create mode 100644 flake.nix create mode 100644 server/.dockerignore create mode 100644 server/docker/Dockerfile create mode 100644 server/docker/README.md create mode 100644 server/docker/docker-compose.yml create mode 100644 server/docker/node.toml.example diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..b457eb7 --- /dev/null +++ b/flake.nix @@ -0,0 +1,193 @@ +# SpiderNet Nix flake — builds the Rust workspace in server/ and exposes +# one `spidernet` package containing all four binaries (sn-node, sn-exit, +# sn-fetch, sn-fair) in $out/bin, plus an experimental NixOS module. +# +# NO WARRANTY — SpiderNet is provided "as is" without warranty of any kind +# (AGPL-3.0, sections 15/16). Use is entirely at the operator's own +# responsibility. +# +# !!! NOT VERIFIED ON THIS MACHINE !!! +# Nix is not installed in the environment where this flake was written. +# `nix flake check` and `nix build` have NOT been run — treat everything +# below as best-effort and report build failures as issues. +# +# Usage (once Nix >= 2.19 is installed): +# nix build . # -> result/bin/{sn-node,sn-exit,...} +# nix shell .# -c sn-fair --capacity 100 --participant 1=contributing +# +# The nixpkgs input is pinned to the current stable NixOS release branch +# (nixos-26.05, released 2026-05-30; nixos-25.11 is already deprecated). + +{ + description = + "SpiderNet: pooled-bandwidth neighborhood network node software"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; + }; + + outputs = + { self, nixpkgs }: + let + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + forAllSystems = nixpkgs.lib.genAttrs systems; + in + { + packages = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + inherit (pkgs) lib; + + # Only the files cargo needs; keeps target/, docs and any other + # clutter out of the Nix store hash. + src = lib.cleanSourceWith { + src = lib.cleanSource ./server; + filter = + path: type: + # Prune cargo build dirs entirely. + !(type == "directory" && lib.hasSuffix "/target" path) + && ( + type == "directory" + || lib.hasSuffix ".rs" path + || lib.hasSuffix ".toml" path + || lib.hasSuffix ".lock" path + ); + }; + + spidernet = pkgs.rustPlatform.buildRustPackage { + pname = "spidernet"; + version = "0.1.0"; + + inherit src; + cargoLock.lockFile = ./server/Cargo.lock; + + # Integration tests talk to real servers; skip them when + # packaging. Run `cargo test` inside server/ for development. + doCheck = false; + + meta = { + description = + "SpiderNet node software (sn-node, sn-exit, sn-fetch, sn-fair)"; + longDescription = '' + SpiderNet pools the bandwidth of household internet + connections over a neighborhood mesh (Yggdrasil overlay). + NO WARRANTY — AGPL-3.0, sections 15/16. + ''; + license = lib.licenses.agpl3Only; + mainProgram = "sn-node"; + platforms = lib.platforms.unix; + }; + }; + in + { + inherit spidernet; + default = spidernet; + } + ); + + # ------------------------------------------------------------------ + # EXPERIMENTAL NixOS module — never built or activated anywhere. + # Runs `sn-node daemon` (which manages the yggdrasil sidecar) as a + # hardened systemd service. + # + # Usage example (in a NixOS configuration that imports this flake): + # + # imports = [ spidernet.nixosModules.default ]; + # + # services.spidernet = { + # enable = true; + # configFile = "/etc/spidernet/node.toml"; + # # stateDir defaults to "spidernet" -> /var/lib/spidernet, + # # which the example config uses as state_dir. + # }; + # + # The config file must set state_dir = "/var/lib/spidernet" (or + # leave the default and rely on WorkingDirectory below). + # ------------------------------------------------------------------ + nixosModules.default = + { + config, + lib, + pkgs, + ... + }: + let + cfg = config.services.spidernet; + in + { + options.services.spidernet = { + enable = lib.mkEnableOption "SpiderNet node daemon (sn-node)"; + + package = lib.mkOption { + type = lib.types.package; + # The flake package of the evaluating system; can be + # overridden with any build of spidernet. + default = self.packages.${pkgs.system}.default; + defaultText = lib.literalExpression "spidernet.packages.\${pkgs.system}.default"; + description = "SpiderNet package to run (contains sn-node etc.)."; + }; + + configFile = lib.mkOption { + type = lib.types.path; + description = '' + Path to the node.toml config file read by + `sn-node daemon --config`. + ''; + }; + + stateDir = lib.mkOption { + type = lib.types.str; + default = "spidernet"; + description = '' + State directory (systemd StateDirectory), holding node.key + and the generated yggdrasil.conf. Created as + /var/lib/. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.spidernet = { + description = "SpiderNet node (Yggdrasil overlay, optional exit)"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + + # The daemon spawns yggdrasil (manage = true by default). + path = [ pkgs.yggdrasil ]; + + serviceConfig = { + ExecStart = + "${lib.getExe cfg.package} daemon --config ${cfg.configFile}"; + StateDirectory = cfg.stateDir; + WorkingDirectory = "/var/lib/${cfg.stateDir}"; + + # TUN setup requires CAP_NET_ADMIN; nothing more. + AmbientCapabilities = [ "CAP_NET_ADMIN" ]; + CapabilityBoundingSet = [ "CAP_NET_ADMIN" ]; + DeviceAllow = [ + "/dev/net/tun rw" + # Yggdrasil TUN devices are created at runtime. + "char-tun rwm" + ]; + + Restart = "on-failure"; + RestartSec = 5; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + # State dir must stay writable. + ReadWritePaths = [ "/var/lib/${cfg.stateDir}" ]; + }; + }; + }; + }; + }; +} diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..271527c --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,9 @@ +# Keep the build context small; everything needed is Cargo.toml, +# Cargo.lock and crates/ (see server/docker/Dockerfile). +target/ +.git/ +docker/node.toml.example +docker/node.toml +docker/README.md +docker/docker-compose.yml +spidernet-state/ diff --git a/server/docker/Dockerfile b/server/docker/Dockerfile new file mode 100644 index 0000000..6962935 --- /dev/null +++ b/server/docker/Dockerfile @@ -0,0 +1,67 @@ +# syntax=docker/dockerfile:1 +# +# SpiderNet node container image (multi-stage). +# +# NO WARRANTY — SpiderNet is provided "as is", without warranty of any kind +# (AGPL-3.0, sections 15/16). Running a node is entirely at the operator's +# own responsibility, including all traffic sent and received with it. +# +# Build context is the `server/` directory (see docker-compose.yml): +# docker build -t spidernet-node -f docker/Dockerfile .. + +# --- Stage 1: build the Rust workspace -------------------------------------- + +FROM rust:1-slim AS build + +WORKDIR /server + +COPY Cargo.toml Cargo.lock ./ +COPY crates ./crates + +# --locked: the build must match the committed Cargo.lock exactly. +RUN cargo build --release --locked --workspace + +# --- Stage 2: runtime -------------------------------------------------------- + +# trixie, not bookworm: rust:1-slim is trixie-based, and binaries built +# against its glibc (2.41) need a matching runtime (bookworm only has 2.36). +FROM debian:trixie-slim + +# Yggdrasil sidecar: current stable release of yggdrasil-go. Release v0.5.14 +# ships official .deb packages (no raw tarballs). Binary inside: /usr/bin/yggdrasil. +# Digests from https://api.github.com/repos/yggdrasil-network/yggdrasil-go/releases/latest +ARG YGGDRASIL_VERSION=0.5.14 +ARG TARGETARCH + +# ca-certificates: TLS for sn-exit/sn-fetch (rustls) and tls:// peers. +# curl is only needed to fetch the yggdrasil .deb and is removed again. +# TARGETARCH is only set by BuildKit; fall back to the host arch otherwise +# (both use the same amd64/arm64 naming as the yggdrasil .deb assets). +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl \ + && ARCH="${TARGETARCH:-$(dpkg --print-architecture)}" \ + && case "$ARCH" in \ + amd64) YGGDRASIL_SHA256=72745496b09baa1c3f5a6741e1257a704cfdb9d00cdf3ddbef025987b239699d ;; \ + arm64) YGGDRASIL_SHA256=ac8c9a9a76c54b878fd251aef5a2bbd20375c6109b28b32435683441462360c9 ;; \ + *) echo "unsupported architecture: $ARCH" >&2; exit 1 ;; \ + esac \ + && curl -fsSL -o /tmp/yggdrasil.deb \ + "https://github.com/yggdrasil-network/yggdrasil-go/releases/download/v${YGGDRASIL_VERSION}/yggdrasil-${YGGDRASIL_VERSION}-${ARCH}.deb" \ + && echo "${YGGDRASIL_SHA256} /tmp/yggdrasil.deb" | sha256sum -c - \ + && dpkg-deb -x /tmp/yggdrasil.deb /tmp/yggdrasil-root \ + && cp /tmp/yggdrasil-root/usr/bin/yggdrasil /usr/local/bin/yggdrasil \ + && rm -rf /tmp/yggdrasil.deb /tmp/yggdrasil-root \ + && apt-get purge -y curl \ + && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /server/target/release/sn-node /usr/local/bin/sn-node +COPY --from=build /server/target/release/sn-exit /usr/local/bin/sn-exit +COPY --from=build /server/target/release/sn-fetch /usr/local/bin/sn-fetch +COPY --from=build /server/target/release/sn-fair /usr/local/bin/sn-fair + +# WORKDIR matches the state volume in docker-compose.yml; a config that +# leaves `state_dir` at its default then resolves to the volume as well. +WORKDIR /var/lib/spidernet + +ENTRYPOINT ["sn-node", "daemon", "--config", "/etc/spidernet/node.toml"] diff --git a/server/docker/README.md b/server/docker/README.md new file mode 100644 index 0000000..8a0e1d0 --- /dev/null +++ b/server/docker/README.md @@ -0,0 +1,125 @@ +# SpiderNet-Node mit Docker + +Dieser Ordner enthält alles, um den SpiderNet-Node eines Haushalts als +Container zu betreiben: Multi-Stage-Dockerfile, ein Compose-Beispiel für +einen Host und eine kommentierte Beispiel-Konfiguration. + +## Haftungsausschluss + +> **KEINE GARANTIE.** Diese Software wird "as is" ohne jegliche Garantie +> bereitgestellt (AGPL §§ 15/16). Die Betreiber und Contributor:innen dieses +> Projekts übernehmen **keinerlei Verantwortung oder Haftung** dafür, wozu +> Nutzer:innen die Software verwenden oder welche Inhalte über sie +> übertragen werden. Die Verantwortung für gesendete und empfangene Inhalte +> liegt vollständig bei den Nutzer:innen. + +## Quickstart + +```sh +# 1. Konfiguration anlegen und anpassen +cp node.toml.example node.toml +$EDITOR node.toml + +# 2. Image bauen und starten (Build-Kontext ist server/, daher "context: ..") +docker compose up --build -d + +# 3. Beobachten +docker compose logs -f + +# 4. Overlay-Status ansehen (Admin-Socket lauscht auf localhost:9001) +docker compose exec node sn-node status + +# 5. Einen großen Download über die gepoolte Bandbreite laden +docker compose exec node sn-fetch "https://…/große-datei.iso" \ + -o /tmp/out.iso -e "1=2,2=1@http://[]:8080" +``` + +Image direkt bauen (ohne Compose), Build-Kontext `server/`: + +```sh +docker build -t spidernet-node -f docker/Dockerfile .. +``` + +## Was die Container-Privilegien bedeuten + +| Einstellung | Warum | +|---|---| +| `network_mode: host` | Yggdrasil muss auf dem realen LAN peeren und auf der Overlay-Seite erreichbar sein. Mit Bridge-NAT würden Peer-Verbindungen und der Exit-Port nicht stimmen. | +| `cap_add: [NET_ADMIN]` | Yggdrasil legt das TUN-Interface (`ifname`) an und konfiguriert es — das braucht `CAP_NET_ADMIN`. | +| `/dev/net/tun` | Das Kernel-TUN-Gerät, das Yggdrasil öffnet. | +| `init: true` | Sauberer PID 1: Reaping + SIGTERM erreicht den Daemon, damit `docker compose stop` als sauberer Kill-Switch funktioniert. | + +Der Container läuft als `root` — für TUN im Container üblich. `NET_ADMIN` +ist bewusst nur als Capability gesetzt, ohne `privileged`. + +## Zwei Haushalte verbinden + +Beide Hosts laufen mit demselben Image; die Rolle (Exit oder Leech) steht +nur in der jeweiligen `node.toml`. + +- **Die Mesh-Verbindung liegt außerhalb des Containers:** Glasfaser, + Switch oder WiFi zwischen den Haushalten. Der Container peeret über das + Host-Netzwerk. +- **Auto-Peering:** Im selben LAN (Multicast aktiv) finden sich + Yggdrasil-Nodes selbst — `peers = []` reicht. +- **Explizit:** Falls Multicast nicht geht (z. B. VLANs), trägt man in + `node.toml` bei `peers` URIs wie `tcp://:1337` ein. +- **ACL:** Die 64-Hex-Public-Keys der erlaubten Nachbarn gehören in + `allow_keys`, sonst kann jeder Node im Overlay peeren. + +Die `node.toml` jedes Haushalts enthält persönliche Daten der Nachbarschaft +(Public Keys, Adressen) und später den Node-Private-Key im State-Verzeichnis: +**niemals committen**, außerhalb des Repos aufbewahren. + +## Leech-Modus (Empfang ohne Exit) + +Die Beispiel-Konfiguration startet sicher im **Leech-Modus** — ein +Haushalt, der nur empfangen will, muss nichts ändern: + +```toml +[exit] +enabled = false +``` + +Das ist ausdrücklich legitim — Leeches werden bei Sättigung zuerst +gedrosselt, aber nie ausgeschlossen. Es gibt kein separates Leech-Image; +das Compose-Beispiel funktioniert unverändert. + +Wer seinen Uplink als Exit anbieten will (opt-in), setzt: + +```toml +[exit] +enabled = true +allow = [""] +``` + +`enabled = true` mit leerer `allow`-Liste ist ungültig — der Daemon +verweigert dann den Start (Ausnahme: Dev-Modus mit Loopback-bind). + +## Kill-Switch + +```sh +docker compose stop +``` + +Stoppen des Exits bzw. des Daemons ist der Kill-Switch: Sobald der Daemon +nicht läuft, stellt der Haushalt keinen Uplink mehr bereit. Der eigene Node +fällt auf den eigenen Internetanschluss zurück. Wichtig zum Verständnis: +SpiderNet v0 leitet **normalen Verkehr nicht um** — kein Routing, kein +NAT-Trick. Nur Downloads, die `sn-fetch` explizit über das Netz verteilt, +nutzen die gepoolte Bandbreite; alles andere geht wie gewohnt über den +eigenen Anschluss. + +## Zustand + +`node.key` und die generierte `yggdrasil.conf` liegen im State-Verzeichnis +(`/var/lib/spidernet`, Named Volume `spidernet-state` im Compose-Beispiel). +Der Node behält damit seine Identität über Container-Neustarts. Volume +löschen = neue Node-Identität. + +## Noch offen / Hinweise + +- Der `daemon`-Subcommand ist implementiert und die Beispiel-Konfiguration + wurde damit verifiziert (`sn-node daemon --check` → `config ok`). +- Images werden lokal gebaut, nicht in ein Registry gepusht (das Projekt + kennt keine zentrale Infrastruktur). diff --git a/server/docker/docker-compose.yml b/server/docker/docker-compose.yml new file mode 100644 index 0000000..df0a82c --- /dev/null +++ b/server/docker/docker-compose.yml @@ -0,0 +1,74 @@ +# SpiderNet node — single-household example (one node per host). +# +# NO WARRANTY — SpiderNet is provided "as is" without warranty of any kind +# (AGPL-3.0, sections 15/16). Running a node is entirely at the operator's +# own responsibility. +# +# The same image serves both roles: +# - exit household: [exit] enabled = true in node.toml (plus neighbor +# addresses in [exit].allow — enabled with an empty allowlist is +# rejected by the daemon) +# - leech household: [exit] enabled = false in node.toml (the example +# config default — safe, receive-only) +# There is no separate leech image; only the config differs. +# +# Quickstart: +# cp node.toml.example node.toml # then edit node.toml +# docker compose up --build -d + +services: + node: + build: + # Build context is server/, matching the Dockerfile paths. + context: .. + dockerfile: docker/Dockerfile + image: spidernet-node + container_name: spidernet-node + # Proper PID 1: reaps zombies and makes sure SIGTERM reaches sn-node + # so `docker compose stop` behaves as the documented kill switch. + init: true + # Yggdrasil must bind its TUN interface and peer on the real LAN; + # both require the host network stack (no NAT translation). + network_mode: host + cap_add: + # Needed by yggdrasil to create/configure the TUN interface. + - NET_ADMIN + devices: + # The kernel TUN device used for the overlay interface. + - /dev/net/tun:/dev/net/tun + restart: unless-stopped + volumes: + # Config is read-only; state (node.key, yggdrasil.conf) goes to a + # named volume so the node identity survives container upgrades. + - ./node.toml:/etc/spidernet/node.toml:ro + - spidernet-state:/var/lib/spidernet + +# --------------------------------------------------------------------------- +# EXIT VARIANT (offering the household's uplink as exit): +# +# The example config defaults to leech mode ([exit] enabled = false). To +# become an exit household set enabled = true in node.toml and add the +# neighbor overlay addresses to [exit].allow. This compose file works +# unchanged for both roles (kill switch: `docker compose stop`). +# +# If you prefer two service definitions (e.g. to toggle via profiles), note +# that `network_mode: host` allows only ONE node per host — stop the other +# service first: +# +# node-leech: +# build: +# context: .. +# dockerfile: docker/Dockerfile +# image: spidernet-node +# container_name: spidernet-node-leech +# init: true +# network_mode: host +# cap_add: [NET_ADMIN] +# devices: ["/dev/net/tun:/dev/net/tun"] +# restart: unless-stopped +# volumes: +# - ./node-leech.toml:/etc/spidernet/node.toml:ro +# - spidernet-state-leech:/var/lib/spidernet + +volumes: + spidernet-state: diff --git a/server/docker/node.toml.example b/server/docker/node.toml.example new file mode 100644 index 0000000..bb0377e --- /dev/null +++ b/server/docker/node.toml.example @@ -0,0 +1,95 @@ +# Beispielkonfiguration für den SpiderNet-Node-Daemon (`sn-node daemon`). +# +# Vorlage: diese Datei nach `node.toml` kopieren und anpassen. Die +# Compose-Datei in diesem Ordner bindet `./node.toml` ein. +# +# KEINE GARANTIE — SpiderNet wird "as is" ohne jegliche Garantie +# bereitgestellt (AGPL §§ 15/16). Nutzung auf eigene Verantwortung; +# die Verantwortung für gesendete und empfangene Inhalte liegt bei den +# Nutzer:innen. +# +# WICHTIG: Die echte node.toml enthält persönliche Daten der Nachbarschaft +# (Public Keys, Adressen) und später den Node-Private-Key im state_dir. +# node.toml NIEMALS ins Repository committen. + +[node] +# Admin-Socket des Yggdrasil-Knotens (tcp://host:port oder unix:///pfad). +# Wird auch als Yggdrasil-AdminListen verwendet, wenn der Daemon das +# Yggdrasil-Sidecar selbst verwaltet (siehe [yggdrasil].manage). +admin_endpoint = "tcp://localhost:9001" + +# Verzeichnis für den Zustand des Nodes (node.key, generierte +# yggdrasil.conf). Optional, Standard "spidernet-state" — relativ zum +# Arbeitsverzeichnis des Daemons. Im Container besser absolut setzen, +# damit es auf das State-Volume fällt: +state_dir = "/var/lib/spidernet" + +[yggdrasil] +# true = der Daemon startet und überwacht das Yggdrasil-Sidecar selbst. +# false = es wird ein bereits laufender Yggdrasil-Daemon verwendet. +manage = true + +# Pfad/Name des Yggdrasil-Binary; nur relevant, wenn manage = true. +# Im Container ist yggdrasil unter /usr/local/bin/yggdrasil installiert. +binary = "yggdrasil" + +# Peer-URIs (tcp://, tls://, quic://, socks://) für manuelles Peering. +# Über das Nachbarschafts-LAN peert Yggdrasil per Multicast automatisch; +# explizite Peers braucht man z. B. über WLAN-Brücken hinweg. +peers = [] + +# Listener-URIs für eingehende Peerings (tcp://, tls://, quic://). +listen = [] + +# Access-Control: 64-stellige Hex-Public-Keys der Nachbarn, die mit uns +# peeren dürfen (AllowedPublicKeys). Leere Liste = jeder darf peeren. +allow_keys = [] + +# true = Multicast-Auto-Peering im LAN deaktivieren. +no_multicast = false + +# TUN-Interface: "auto" (Standard), "none" (headless, nur Router-Modus) +# oder ein fester Interface-Name. +ifname = "auto" + +[exit] +# true = dieser Haushalt stellt seinen Uplink als Exit bereit (opt-in). +# false = Leech-Modus (Standard hier): der Node empfängt nur, bietet +# keinen Exit an — ausdrücklich legitim, niemand wird gedrosselt, nur +# fair priorisiert. +# +# Wer Exit anbieten will: enabled = true setzen UND in `allow` die +# Overlay-Adressen der Nachbarn eintragen — enabled = true mit leerer +# `allow`-Liste ist ungültig (der Daemon verweigert den Start; Ausnahme: +# Dev-Modus mit Loopback-bind). +enabled = false + +# Explizite Bind-Adresse. Leer (Standard) = "[]:port", +# d. h. der Exit lauscht auf der eigenen Overlay-Adresse. +# UNGÜLTIG: allow = [] zusammen mit einem nicht-Loopback-bind — der Exit +# wäre sonst für beliebig viele Quellen offen (Schutz: ohne Allowlist nur +# Loopback, das ist der Dev-Modus). +bind = "" + +# TCP-Port des Exit-Dienstes (u16, Standard 8080). +port = 8080 + +# Overlay-Adressen der Nachbarn, die diesen Exit nutzen dürfen. +# Leere Liste = nur Loopback (Dev-Modus); bei nicht-Loopback-bind Pflicht. +# Beispiel: +# allow = ["200:abcd:ef01::2", "200:abcd:ef01::3"] +allow = [] + +# Kontingent pro Nachbar: Bytes pro Zeitfenster (u64, Standard 1024 MiB). +# 0 = unbegrenzt (kein Kontingent). +quota_mib = 1024 + +# Länge des Kontingent-Zeitfensters in Stunden (fixed window). +window_hours = 24 + +# KILL-SWITCH: Wer nichts mehr teilen will, stoppt einfach den Exit bzw. +# den Daemon (Container: `docker compose stop`). Damit endet die +# Bereitstellung des eigenen Uplinks sofort — der eigene Node fällt auf +# den eigenen Internetanschluss zurück. SpiderNet v0 leitet normalen +# Verkehr nicht um: nur Downloads, die sn-fetch explizit über das Netz +# verteilt, nutzen die gepoolte Bandbreite.