Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Network Containment

Network containment is SwarmOtter’s fail-closed data-plane routing model.

It applies to torrent-related traffic:

  • Peer TCP.
  • Peer UDP and uTP.
  • DHT UDP.
  • PEX-discovered peers.
  • UDP tracker announces.
  • HTTP and HTTPS tracker announces and supported scrape.
  • Webseeds.
  • Magnet metadata fetching.
  • DNS used by torrent operations.

The API and Web UI are separate control-plane traffic and use api.bind_address.

Traffic planes

SwarmOtter separates API/Web UI control traffic from torrent data-plane traffic. Network containment applies to the torrent data plane.

flowchart LR
    subgraph control["Control plane"]
        client["Browser or API client"] -->|"api.bind_address"| api["SwarmOtter API / Web UI"]
    end

    subgraph data["Torrent data plane"]
        engine["SwarmOtter torrent engine"] -->|"containment boundary"| boundary["Required interface, source address, or contained namespace"]
        boundary --> torrentNet["Peers, trackers, DHT, PEX peers, webseeds, and torrent DNS"]
    end

For the Docker Compose deployment, the containment boundary is Gluetun:

flowchart TB
    lan["LAN browser or API client"]

    subgraph host["Docker host"]
        published["Published port 9091"]
        subgraph ns["Shared Gluetun network namespace"]
            swarmotter["SwarmOtter service<br/>network_mode: service:vpn"]
            firewall["Gluetun firewall"]
        end
    end

    vpn["VPN tunnel"]

    lan -->|"API / Web UI"| published --> swarmotter
    swarmotter -->|"torrent data plane"| firewall --> vpn

Fail-closed behavior

When strict containment is enabled and the configured path is unavailable, SwarmOtter blocks torrent networking instead of falling back to the default route.

Strict mode is the default. Omitting [network] does not disable containment: it leaves strict mode without an enforceable path and validation fails before the control listener or background tasks start. Use explicit disabled mode only for local development or a separately enforced boundary:

[network]
mode = "disabled"
flowchart TB
    operation["Torrent operation"] --> check{"Required path healthy?"}
    check -->|"yes"| path["Contained network path"]
    check -->|"no"| blocked["Blocked fail closed"]

    api["API / Web UI"] --> status["Status and remediation remain available"]

Common fail-closed conditions:

  • The required interface does not exist.
  • The required interface is down.
  • The required interface has no usable IP address.
  • A configured source address is no longer assigned.
  • IPv6 is required but disabled in network or torrent configuration.
  • The route cannot be validated when route validation is enabled.
  • DNS cannot be validated when DNS validation is enabled.
  • The required network namespace is unavailable.
  • Socket binding fails.

Linux route and DNS path checks use ip route get. The official container and native packages provide the required utility; direct and tarball installs must install iproute2 on Debian/Ubuntu or iproute on Fedora/RHEL-family systems.

The API reports the current state at:

GET /api/v1/network/health

The Web UI displays the same health state.

Live gate, recovery intent, and bind failures

One process-wide gate covers binders, engines, trackers, peer sessions, webseeds, metadata, DHT, uTP, inbound listeners, and seeders. When a required path disappears, the gate blocks before socket-owning tasks are aborted. Every block advances the cancellation generation. A task from an older generation is therefore cancelled even if recovery follows before it next polls, and a connected stream cannot bridge a fail-closed interval. The API/Web UI listener is outside this data-plane gate and remains available for diagnostics and repair.

HTTP(S) tracker announce/scrape and webseed range reads use one contained HTTP/1 codec. Each redirect hop asks the binder to connect to the target host; the ordinary contained binder resolves it on the contained path, while an enabled SOCKS5 binder keeps the hostname for remote target DNS. TLS wraps only that stream. No connector, independent resolver, or pool can open a data-plane socket. HTTPS-to-HTTP redirects are rejected, decoded bodies are bounded, and exact webseed Range/Content-Range semantics are enforced. UDP scrape is unsupported and makes no network call; UDP announce remains contained and supported unless SOCKS5 TCP-only mode is enabled.

Only work demonstrably live at the block edge receives durable recovery intent. After recovery, SwarmOtter consumes that intent and resumes those downloads, metadata fetches, or active seeders. Paused, merely queued, ratio/idle-stopped, completed-without-a-live-seeder, and stale blocked records do not start because the path recovered.

A source, interface, UDP, or peer-listener bind failure blocks immediately and reports socket_bind_failed; a generic strict-policy denial reports blocked_fail_closed. These failures remain latched even if the interface probe later reports healthy. To recover, submit an explicit full configuration with PUT /api/v1/settings. SwarmOtter validates the peer-listener bind and, unless SOCKS5 TCP-only mode is enabled, a contained ephemeral UDP bind before clearing the latch. If validation or persistence fails, the old configuration remains active and traffic stays blocked. A partial settings patch, health tick, or torrent resume does not clear the latch.

SOCKS5 TCP proxy

[network.socks5] is an opt-in TCP CONNECT layer, not a replacement for the configured containment path. The daemon uses the contained binder to resolve and connect to the proxy itself. TCP peer IP addresses use SOCKS IP-address requests; HTTP(S) tracker, scrape, and webseed hostnames use SOCKS domain requests so target DNS happens at the proxy. If a proxy connection or handshake fails, the target is not retried directly.

SOCKS5 no-authentication and RFC 1929 username/password authentication are supported. The password is redacted from Settings reads and update results. A blank password in a full Settings save retains the stored value only when the username is unchanged.

The supported proxy mode is deliberately TCP-only. Enabling it requires:

[network.socks5]
enabled = true
host = "proxy.example"

[torrent]
utp_enabled = false

[dht]
enabled = false

The proxy binder blocks UDP sockets and direct target resolution, so UDP tracker, DHT, and uTP traffic cannot silently escape outside the proxy. SOCKS5 UDP ASSOCIATE and proxy-provided inbound forwarding are not implemented. Peer listeners remain bound to the configured contained path. NAT-PMP/UPnP mapping uses the same containment boundary directly for local router traffic; it is not a proxy or torrent-egress fallback. Network diagnostics expose only the socks5_enabled and socks5_udp_blocked state, never proxy host or credentials.

Dynamic interface binding

For DHCP or SLAAC addresses, bind to an interface instead of an address:

[network]
mode = "strict"
required_interface = "br0"
allow_ipv6 = true
fail_closed = true
validate_route = true
validate_dns = true

[torrent]
allow_ipv6 = true

On Linux, SwarmOtter enforces sockets with device-bound sockets. IPv4 and IPv6 connections are both allowed when the interface has usable addresses and both network.allow_ipv6 and torrent.allow_ipv6 are true. Hostname resolution is allowed only when DNS is also proven constrained to the configured path, such as systemd-resolved link DNS reported by resolvectl dns br0.

DNS policy

DNS is part of torrent traffic. In strict containment, hostname resolution must not escape through an unconstrained resolver.

Use one of these patterns:

  • Bind to an interface whose DNS is visible to the Linux probe, such as systemd-resolved link DNS from resolvectl dns br0.
  • Use a contained network namespace or container network where DNS is part of the contained path.
  • Use IP-literal peers, trackers, and bootstrap nodes when DNS containment is not available.
  • Set validate_dns = true when you want network health to report dns_not_constrained proactively instead of discovering it at tracker/DHT resolution time.

Health states

StateMeaning
healthyTorrent networking can use the configured contained path.
disabledNetwork containment is disabled.
interface_missingThe configured interface name is not visible to the daemon.
interface_downThe configured interface exists but is down.
no_interface_addressThe interface has no usable IPv4 or allowed IPv6 address.
source_address_missingA configured source address is not assigned.
route_invalidRoute validation failed.
socket_bind_failedThe daemon could not bind a socket to the configured path.
dns_not_constrainedDNS validation was requested but could not be proven safe.
network_namespace_unavailableThe daemon is not in the required namespace.
blocked_fail_closedStrict containment blocked traffic.

Privileged local acceptance test

Build and invoke the harness as your normal user. It requests sudo internally only for ip namespace/link operations:

cargo build --locked -p swarmotterd
scripts/test-network-containment-transition.sh \
  "$PWD/target/debug/swarmotterd"

The harness uses no external network or default route. It creates two PID-qualified namespaces, generates a lawful payload and torrent, and runs a compact HTTP tracker plus throttled TCP BitTorrent seed in the peer namespace. The raw torrent is registered through the real API and must show partial tracker-discovered peer-wire progress. The harness then deletes the daemon veth and requires interface_missing, network_blocked, empty data-plane scheduler diagnostics, stable verified bytes, and a responsive /health route. The tracker, seed, generator, and API clients have no capabilities; SwarmOtter gets only CAP_NET_RAW for SO_BINDTODEVICE. Cleanup removes both namespaces and fixture processes.