Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

freenet-unraid

A Docker Compose stack that runs a Freenet node as a pure contributing peer to the Freenet network. It's built and tested for Unraid, but the compose file itself has no Unraid-specific dependencies and should run on any Docker host.

This node only contributes storage and bandwidth to the network — it is never configured as a gateway (is_gateway is never set to true), so it never acts as a bootstrap/entry point for other clients. network_mode: host is required for Freenet's own UDP NAT traversal (hole-punching) to have a chance of working at all.

In practice, hole-punching alone is not always enough. It only works reliably against a "cone" NAT — one that keeps the same external port for a given internal port regardless of destination. Many consumer/prosumer routers (confirmed with OPNsense, likely others) randomize the external port per outbound connection instead, which silently breaks hole-punching: the node's dashboard will show NAT hole punching: 0/N successful and only ever connect to gateways, never to real peers. If that happens, see Troubleshooting: NAT hole-punching fails below — it does require a port forward plus a matching outbound NAT rule, which contradicts the "no port forwarding needed" assumption this project started with.

Note: This is the new Rust-based Freenet (freenet.org). The older, unrelated anonymous network that used to be called "Freenet" is now called Hyphanet — a completely different project.

Prerequisites

  • A Docker host with Docker and the docker compose plugin installed.
  • A host directory (or, on Unraid, a share) for the node's datastore — config, keys, contracts, and database all live here. Make sure it has enough free space for the storage budget you configure (see MAX_HOSTING_STORAGE_GB below); the default is 2500 GB.

Quickstart

  1. Copy the environment template and adjust it for your setup:

    cp .env.example .env

    At minimum, review:

    • ALLOWED_CIDRextra allowed source ranges for the dashboard/API. Freenet already allows every private/RFC1918, loopback and link-local source address regardless of what you put here, so this value does not restrict anything — it only adds ranges beyond that default. A plain LAN value like 10.10.1.0/24 is therefore effectively a no-op, and you cannot lock yourself out by getting it "wrong". It matters only when you reach the dashboard from a range that isn't RFC1918 — most commonly Tailscale's CGNAT range 100.64.0.0/10. Separate multiple ranges with commas: ALLOWED_CIDR=10.10.1.0/24,100.64.0.0/10.
    • DATA_PATH — the host directory/share where the node's data will live.
    • MAX_HOSTING_STORAGE_GB and TOTAL_BANDWIDTH_LIMIT_MBPS — see the table below.
  2. Run the deploy script:

    ./deploy.sh

    This checks that WS_API_PORT (default 7509) isn't already in use on the host, then pulls the prebuilt image for IMAGE_TAG from GitHub Container Registry (ghcr.io/raybomb87/freenet-unraid) and starts the container — no local build, no Rust toolchain required. That image is itself a thin wrapper: it downloads and checksum-verifies the official prebuilt freenet-core binary for the pinned release rather than compiling from source (see docker/Dockerfile).

  3. Confirm the container is running:

    docker logs -f freenet-node

Environment variables (.env)

Variable Default Meaning
IMAGE_TAG v0.2.111 Which git tag of freenet-core gets built. See "Updating" below.
DATA_PATH /mnt/user/FreeNET Host path mounted into the container as the node's data directory (config, keys, contracts, database).
MAX_HOSTING_STORAGE_GB 2500 How much disk space (in GB) the node is allowed to use hosting other peers' contract data for the network. Passed as both --max-hosting-storage and --max-hosting-disk (see the note below the table).
ALLOWED_CIDR 10.10.1.0/24 Extra allowed source ranges for the dashboard/API. Private/RFC1918, loopback and link-local addresses are always allowed regardless, so this restricts nothing — it only adds ranges beyond that (e.g. Tailscale's 100.64.0.0/10). Comma-separated.
WS_API_PORT 7509 The port the dashboard/WebSocket API listens on.
TOTAL_BANDWIDTH_LIMIT_MBPS 100 A total bandwidth budget (Mbps) that is divided across active connections, not a hard ceiling on all traffic. See "Bandwidth" below.

These values are passed through to the freenet binary as CLI flags by entrypoint.sh (--max-hosting-storage, --max-hosting-disk, --total-bandwidth-limit, --allowed-source-cidrs, --ws-api-port) — there's no config.toml to edit by hand. The storage and bandwidth values are converted from GB/Mbps to bytes/bytes-per-second by entrypoint.sh before being passed in.

Note on the storage budget: --max-hosting-storage on its own is not enough. The binary applies a second, independent ceiling, --max-hosting-disk, which defaults to just 32 GiB. The effective budget is the lower of the two, so without setting both, a 2500 GB budget would be silently clamped to 32 GiB. entrypoint.sh therefore passes the same byte value to both flags.

Bandwidth — what the limits actually do

The bandwidth limits in freenet-core are weaker than their names suggest, which is worth knowing before you rely on them:

  • total_bandwidth_limit (set via TOTAL_BANDWIDTH_LIMIT_MBPS) is a budget that is divided across active connections — the binary's own help text describes per-connection rates as being computed as total / active_connections. It is not a single hard ceiling on all the traffic the node generates.
  • bandwidth_limit applies only to the send_stream mechanism for large data transfers. The general packet rate limiter in freenet-core is disabled upstream as of v0.2.106 — quoting freenet network --help directly: "The general packet rate limiter is currently disabled due to reliability issues."

The practical consequence is that real-world usage can exceed the nominal limit, particularly with many concurrent connections.

Required verification step: watch your router's (or the host OS's) own traffic graph for a full day after first startup and confirm that actual usage is acceptable. This is not optional — Freenet's own self-reported numbers are not sufficient evidence that the cap is being honoured. Lower TOTAL_BANDWIDTH_LIMIT_MBPS if usage is higher than you want.

Dashboard and API — read this before exposing anything

Freenet's own dashboard/WebSocket API (the port set by WS_API_PORT, default 7509) is described in Freenet's own documentation as a fully privileged API — it should be treated like SSH access. Anything that can connect to this API effectively has full control over the node.

Because of that:

  • The real protection here is that there is no route in to the port from the internet — it is not ALLOWED_CIDR that provides it. Freenet lets every private/RFC1918 address through regardless, so anything on your LAN can reach the API. Treat the whole LAN as trusted, or segment your network if that isn't acceptable.
  • Never port-forward this port, and never expose it to the open internet.
  • If you need remote access, route it over a trusted, authenticated network layer you already control — a VPN, Tailscale, or an SSH tunnel — never by exposing the port directly.

With that in place, you reach the dashboard at:

http://<your-docker-host-LAN-IP>:7509

— either directly from a device on your LAN, or over your VPN/Tailscale/SSH tunnel from elsewhere. Never over the open internet.

Telemetry

freenet-core defaults to sending operational telemetry (timing/topology data, not contract content) to nova.locut.us (telemetry-enabled = true upstream). A home contributing node has no real need for this, so entrypoint.sh sets FREENET_TELEMETRY_ENABLED=false by default.

One quirk worth knowing: --telemetry-enabled is a bare boolean CLI switch that can only turn telemetry on (--telemetry-enabled false errors out, because clap parses false as a different positional argument instead). The only way to turn it back off is the FREENET_TELEMETRY_ENABLED environment variable, which the binary's argument parsing does honor for true/false. If you want to re-enable telemetry, set FREENET_TELEMETRY_ENABLED=true as a container environment variable — there is no corresponding switch in .env at the moment.

Updating

freenet-core ships new releases frequently, so this stack always pins to one specific git tag via IMAGE_TAG in .env (v0.2.111 at the time of writing) — the image is never built against main.

To update to a new version:

./update.sh v0.2.107

This updates IMAGE_TAG in .env to the new tag and re-runs deploy.sh, which pulls the matching image tag and restarts the container.

Note for forks / other maintainers: ghcr.io/raybomb87/freenet-unraid images are published by running the repo's "Build and publish Docker image" GitHub Action (.github/workflows/docker-publish.yml) by hand for a given freenet-core tag — they are not built automatically on every upstream release. If you need a version that hasn't been published yet, either wait for the maintainer to run it or run the workflow yourself in your own fork (it will publish to ghcr.io/<your-username>/freenet-unraid instead — update the image: line in docker-compose.yml to match).

Before you update far past the pinned tag

entrypoint.sh drives the node entirely through CLI flags (--max-hosting-storage, --max-hosting-disk, --total-bandwidth-limit, --allowed-source-cidrs, --ws-api-port) rather than a templated config.toml. Those flag names, their defaults, and the config schema they write out were verified against a real running v0.2.106 binary rather than taken from documentation — the results are recorded in docker/CONFIG-KEYS-VERIFIED.md.

Two things are worth knowing if you bump IMAGE_TAG:

  • The binary's own config keys are an inconsistent mix of snake_case and kebab-case (is_gateway with underscores, max-hosting-storage with hyphens). That's upstream behaviour, not a quirk of this repo.
  • Because freenet-core releases frequently, flag names and their defaults can change between releases. Check freenet network --help against the new tag before moving much past v0.2.106, and update both entrypoint.sh and docker/CONFIG-KEYS-VERIFIED.md if anything has shifted. A silently renamed or re-defaulted flag is the most likely way this stack breaks on an upgrade — --max-hosting-disk is a live example of a default (32 GiB) that will quietly cap your storage budget if it stops being passed.

Troubleshooting: NAT hole-punching fails

Check the dashboard (http://<host-LAN-IP>:7509) after the node has been up for a few minutes. If it shows something like:

Only connected to gateways — no peer-to-peer connections yet
NAT hole punching: 0/48 successful

...your router's NAT is very likely not a "cone" NAT — it assigns a different external port per outbound connection (sometimes called "symmetric" or address/port-dependent mapping) instead of keeping one stable external port for the node's internal port. Hole-punching depends on the external port your node advertises to the network staying the same for every peer, so this silently breaks it: the node only ever reaches the gateways it connects outbound to, never real peers.

There is no software fix for this — it has to be corrected on the router. Verified working on OPNsense 26.1 (the same fix applies conceptually to any router with similar automatic outbound NAT/PAT):

  1. Inbound port forward (OPNsense: Firewall → NAT → Destination NAT — older pfSense-derived UIs call this "Port Forward"): WAN, UDP, destination port = your node's network-port (find it in the node's node-config/config.toml, or watch what the dashboard's "Firewall likely blocking..." warning names) → redirect to <host-LAN-IP> on the same port.
  2. Outbound NAT rule with Static Port: WAN, source = your LAN subnet, source port = the same node port → translate to the interface address, with Static Port enabled. Without this, the port forward alone isn't enough — the node's outbound packets to new peers would still get a randomized external port that doesn't match what it advertised, so genuine peers still can't punch back through.

On OPNsense specifically, if outbound NAT mode is "Hybrid," a manual rule for the same interface can still lose to the broader automatic rule: pf evaluates NAT rules top-to-bottom and the last matching rule wins, and automatic Hybrid-mode rules are compiled in after manual ones for the same interface regardless of the order shown in the "Manual rules" GUI table. If your manual static-port rule doesn't seem to take effect (pfctl -sn -vv shows Evaluations but States: 0 for it), switching Outbound NAT to full "Manual" mode — which lets you place your rule after the general one — is the reliable fix; there isn't a lighter-weight option within Hybrid mode.

After both rules are in place, kill any existing state for the node's own host so new connections pick up the fresh mapping (pfctl -k <your-host-LAN-IP> on OPNsense/pfSense), or just restart the container. Give it a few minutes — the dashboard's hole-punching ratio and peer list fill in gradually as new connections are attempted.

Credits

This stack is a thin Docker wrapper around freenet/freenet-core — all the actual node logic is theirs. Because that project ships frequent releases, this repo builds from a pinned upstream tag (IMAGE_TAG) rather than tracking main, so upgrades are a deliberate, tested step rather than an automatic moving target.

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages