Running a Tor bridge at home without exposing the LAN
Blog · privacy

Running a Tor bridge at home without exposing the LAN

Bridge versus exit, container isolation with DOCKER-USER, resource limits and the version that aged out

In July 2026 the Tor Project’s Relay Search labelled my bridge “Not Recommended”. Nothing was broken. The container was running, connections were flowing, my monitoring was green. The Tor version inside it had simply aged out: a latest image pulled 5 weeks earlier, still on Tor 0.4.8.22, had fallen off the directory authorities’ list of recommended versions.

I run an obfs4 Tor bridge and a Snowflake proxy at home, on the same machine as everything else in my lab. They donate bandwidth to people in places where Tor is blocked. This post is about doing that responsibly: what to run (and what not to), how to keep the relay away from the private network it shares a machine with, and the small operational traps, like version aging, that quietly make a volunteer relay less useful.

Contents · 10 sections

TL;DR #

  • Run a bridge or Snowflake proxy, not an exit, from a home connection. Bridges are unlisted entry points; they do not send traffic to the open internet on users’ behalf.
  • Put the relay on its own container network and drop everything from it to your LAN, VPN and cluster ranges. Verify from inside the container.
  • Set bandwidth, CPU and memory limits so a busy day for Tor users is not an outage for you.
  • latest is not a version policy. Watch for the obsolete / Not Recommended flag and update.
  • Keep the identity keys in a persistent volume; they are the bridge’s identity.
  • Monitor the right thing: the bridge heartbeat and Snowflake statistics, not a Tor client that happens to run on the same host.

Bridge, relay, exit: pick the right job #

Tor has several roles a volunteer can play:

  • Exit relays connect to the destination website on behalf of users. Abuse complaints go to the exit’s IP address. That is a poor fit for a residential connection that also carries your own life.
  • Middle and guard relays are listed publicly in the Tor consensus, which makes their addresses easy to block.
  • Bridges are relays that are not published in the main directory. Users get bridge addresses through other channels, so censors cannot simply download a list. obfs4 makes the traffic look like random bytes, so it is harder to fingerprint.
  • Snowflake proxies are lighter still: they relay WebRTC connections from censored users to a Snowflake bridge, and need no open ports.

I run one obfs4 bridge and one Snowflake proxy. The bridge configuration states explicitly that it is not an exit:

services:
  tor-bridge:
    image: thetorproject/obfs4-bridge:<version>
    environment:
      NICKNAME: <nickname>
      EMAIL: <contact address>
      OBFS4_ENABLE_ADDITIONAL_VARIABLES: "1"
      OBFS4V_ExitRelay: "0"
      OBFS4V_IPv6Exit: "0"
      OBFS4V_RelayBandwidthRate: 50 MB
      OBFS4V_RelayBandwidthBurst: 100 MB
    volumes:
      - ./bridge-keys:/var/lib/tor        # identity keys: keep forever
    networks: [tor-relay]
    mem_limit: 4g
    cpus: 4
    logging: {driver: json-file, options: {max-size: 10m, max-file: "3"}}

  snowflake:
    image: thetorproject/snowflake-proxy:<version>
    networks: [tor-relay]
    mem_limit: 1g
    cpus: 2

A contact address matters: the Tor Project uses it to reach operators about problems.

Isolation: the relay may reach the internet, nothing else #

A Tor bridge accepts connections from strangers by design. If the process were ever compromised, the first thing an attacker would look at is what else it can reach. On a home server, that is everything: the router, the NAS, the Kubernetes cluster, other containers.

So the relay containers sit on their own Docker network, and a small script adds firewall rules in Docker’s DOCKER-USER chain that drop any traffic from that network’s bridge interface to private destinations:

#!/bin/bash
set -euo pipefail
NET=tor-relay
ID=$(docker network inspect -f '{{.Id}}' "$NET")
IF="br-${ID:0:12}"
for DST in <cluster-pod-cidr> <cluster-service-cidr> <lan-cidr> <vpn-cidr>; do
  iptables -C DOCKER-USER -i "$IF" -d "$DST" -j DROP 2>/dev/null \
    || iptables -I DOCKER-USER -i "$IF" -d "$DST" -j DROP
done

Three details make it robust:

  • DOCKER-USER is the chain Docker reserves for your own rules. Docker does not rewrite it, and it is evaluated before Docker’s own forwarding rules.
  • The script is idempotent (-C before -I) and runs as ExecStartPost of the systemd unit that starts the containers, so the rules exist whenever the relay does.
  • The interface name is derived from the network ID, so it survives container re-creation.

Then test from the relay’s side. At the July upgrade I checked from inside the re-created bridge container that the router and the NAS were unreachable, and that the rules had survived the re-creation. Do it after every change. A firewall rule you have only read is a hypothesis.

Resource limits are a courtesy to yourself #

The bridge is capped at a sustained 50 MB/s with bursts to 100 MB/s, plus CPU and memory limits on both containers and rotated logs. Tor usage spikes when a country blocks something. Without limits, a spike for them would mean an outage for every other service on the machine. With limits, it means the bridge is busy.

The version that aged out #

Back to the July flag. The Relay Search page showed the bridge as “Not Recommended”, and the Onionoo API, which serves the data behind it, reported version_status: obsolete for Tor 0.4.8.22.

The cause was the image tag. The compose file pinned latest, which means “whatever was latest when I last pulled”. My last pull was five weeks old. Tor’s directory authorities publish a list of recommended versions, and older ones drop off it.

The fix was a pull and a re-create:

docker compose pull && docker compose up -d
docker logs --tail 50 tor-bridge | grep -E 'Bootstrapped|version'

The bridge came back on Tor 0.4.9.9, bootstrapped to 100%, and kept the same fingerprint, because its identity keys live in a persistent volume, not in the container. Losing those keys turns your bridge into a new, unknown one, and users who had its address lose it.

Two lessons from that day:

  • Know which directory is live. There were two copies of the compose file: the live one that the systemd unit starts, and a copy in my manifests repository. Updating the copy does nothing.
  • Pin a version, and schedule its update. Either pin a real version and bump it deliberately, or keep latest and run a monthly pull. What does not work is latest with no pull, which is a pinned version you did not choose.

Recovering a relay from a running container #

In October an accident deleted my home folder, including the relay’s compose file and the isolation script. The containers kept running. I reconstructed both from the live system: the compose file from docker inspect of the running containers, and the firewall script from the live DOCKER-USER rules. The bridge’s identity keys were owned by the container user, outside my user’s reach, and survived. The files now live in Git, with a header saying how they were rebuilt.

Monitoring the right thing #

My local AI agent runs scheduled health checks. Until October its “Tor” check looked at the wrong thing: a Tor client proxy used by another service on the same host, reported as “relay status”. It also wandered off browsing files and timed out.

The check was rewritten to look only at the relay:

  • the bridge’s heartbeat messages, which Tor writes every six hours with connection counts;
  • Snowflake’s hourly statistics of connections served;
  • a final line of ALL_CLEAR, or explicit WARN: / CRIT: lines.

One practical gotcha: the Snowflake proxy image is built from scratch. There is no shell inside, so docker exec fails. Read it through docker logs.

The other check that matters for a home connection is the public address. My bridge advertises a dynamic DNS name. Every 5 minutes a watchdog compares the resolved address with the one Tor reports, restarts the bridge when it changes, and confirms the “reachable from the outside” message afterwards.

Checklist #

  • Run a bridge or Snowflake, not an exit, on a home connection; set ExitRelay 0.
  • Give a real contact address.
  • Own container network; DOCKER-USER drops to every private range; run the rules from the service unit.
  • Test isolation from inside the container after every change.
  • Set bandwidth, CPU, memory and log limits.
  • Keep identity keys in a persistent, backed-up volume.
  • Pin and update the Tor version; watch Relay Search or Onionoo for “obsolete”.
  • Know which compose directory is live; keep it in Git.
  • Monitor heartbeat and Snowflake stats, and the public address on dynamic connections.

Frequently asked questions #

Is it safe to run a Tor bridge at home? #

A bridge does not connect to websites on users’ behalf, so abuse complaints about destination traffic go to exit relays, not to you. The main risks are resource use and the bridge process itself, which you manage with limits and network isolation from your private networks.

What is the difference between a Tor bridge and a Snowflake proxy? #

A bridge is a full, unlisted Tor relay that censored users connect to, usually with an obfuscating transport like obfs4. A Snowflake proxy is a lightweight helper that forwards WebRTC connections from users to a Snowflake bridge run by the Tor Project, and needs no open ports.

Your Tor version is no longer on the directory authorities’ recommended list. Update the Tor package or image. If you run it in Docker with a latest tag, pull and re-create the container.

Will updating my bridge change its fingerprint? #

Not if the identity keys persist. Keep the Tor data directory in a volume that survives re-creation, and back it up.

The lesson #

Volunteering bandwidth is easy; doing it without becoming a risk to yourself or a weak link for its users takes a little discipline. Isolate the relay as if it might be hostile, limit it as if it might be popular, and keep it current as if someone depends on it, because someone does.