How to Run a UDP Tracker Behind a Floating IP on Ubuntu
A practical guide to running a UDP BitTorrent tracker behind floating IPs (also known as static, reserved, or elastic IPs) on Ubuntu, including policy routing, Docker IPv6 networking, and SNAT for correct reply paths.
Introduction
In this article, we document how we configured the server for the Torrust Tracker Demo to run a UDP tracker behind a floating IP on Ubuntu.
The same approach applies to other cloud providers where floating IPs are called static IPs, reserved IPs, or elastic IPs. The naming changes, but the network behavior is the same.
Why This Matters
Using floating IPs is a common strategy to isolate infrastructure from public endpoint addresses. It lets you replace, resize, or rebuild the internal server while keeping the same public DNS records and tracker announce URLs.
For HTTP services this is usually straightforward. For UDP trackers, there is an extra requirement: the response must come back from the same public IP that received the request. If replies leave via another source IP, many clients treat it as a timeout.
Tested Environment
- Cloud provider: Hetzner Cloud
- OS: Ubuntu 24.04 LTS
- Tracker endpoint:
udp://udp1.torrust-tracker-demo.com:6969/announce - Floating IPv4:
116.202.177.184 - Floating IPv6:
2a01:4f8:1c0c:828e::1 - Container stack: Docker + Docker Compose
Architecture Summary
| Layer | What must happen |
|---|---|
| DNS | UDP tracker domain resolves to dedicated floating IPs |
| Firewall | UDP port 6969 allowed on IPv4 and IPv6 |
| Kernel routing | Source policy routing for each floating IP |
| Docker IPv6 | ip6tables enabled and bridge network has IPv6 subnet |
| NAT (IPv6) | SNAT replies to the floating IPv6 for UDP/6969 |
Step 1: Configure Policy Routing for Floating IPs
For every floating IP, add a source-based routing policy so replies use the matching
public address. On our server we persist this in /etc/netplan/60-floating-ip.yaml.
network:
version: 2
renderer: networkd
ethernets:
eth0:
addresses:
- 116.202.177.184/32
- 2a01:4f8:1c0c:828e::1/64
routing-policy:
- from: 116.202.177.184
table: 100
- from: 2a01:4f8:1c0c:828e::1
table: 200
routes:
- to: default
via: 172.31.1.1
table: 100
- to: default
via: fe80::1
table: 200sudo netplan apply
ip rule list
ip route show table 100
ip -6 rule list
ip -6 route show table 20060-floating-ip.yaml)
rather than editing 50-cloud-init.yaml.Step 2: Open UDP Port 6969 in the Firewall
In our investigation, one blocker was firewall path behavior on IPv6. The server had ufw in default deny mode, and UDP 6969 was not explicitly allowed.
Important nuance: with Docker, published ports on IPv4 are often reachable even when ufw looks restrictive, because Docker installs its own NAT and forwarding
rules. That does not guarantee equivalent behavior for IPv6 in every setup.
For this reason, verify IPv4 and IPv6 paths separately instead of assuming both families behave
the same way.
sudo ufw allow 6969/udp
sudo ufw status verboseExpected result includes both 6969/udp and 6969/udp (v6) as ALLOW IN. Treat this as one control in a layered setup, not as the only
explanation for reachability.
Step 3: Enable Docker ip6tables Management
Docker frequently handles IPv4 iptables automatically, but IPv6 behavior depends on daemon settings and network topology. To keep IPv6 UDP handling predictable across restarts, enable ip6tables in Docker.
{
"ip6tables": true
}Add it to /etc/docker/daemon.json, then restart Docker:
sudo systemctl restart docker
sudo ip6tables -L ufw6-user-input -nStep 4: Enable IPv6 on the Docker Bridge Network
If the bridge network has no IPv6 subnet, containers only get IPv4 addresses. In that case, native IPv6 UDP forwarding can fail. We solved this by enabling IPv6 in the Docker network.
proxy_network:
driver: bridge
enable_ipv6: true
ipam:
config:
- subnet: "fd01:db8:1::/64"cd /opt/torrust
docker compose down
docker compose up -d
docker inspect tracker --format '{{range .NetworkSettings.Networks}}{{.GlobalIPv6Address}} {{end}}'
sudo ip6tables -t nat -L DOCKER -n -v | grep 6969Step 5: Add SNAT for IPv6 UDP Replies
After enabling IPv6 inside Docker, replies can still leave with the primary IPv6 because of MASQUERADE behavior. For floating IPv6 UDP endpoints, add an explicit SNAT rule.
# /etc/ufw/before6.rules
# NAT: rewrite source of Docker UDP tracker IPv6 replies to the floating IP
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s fd01:db8:1::/64 -o eth0 -p udp --sport 6969 \
-j SNAT --to-source 2a01:4f8:1c0c:828e::1
COMMITsudo ufw reload
sudo ip6tables -t nat -L POSTROUTING -n -v | grep 6969Verification Checklist
- Domain points to the correct floating IPs:
dig Aanddig AAAA. - Firewall allows UDP 6969 on both families.
- Policy rules and custom tables are active after reboot.
- Container has a non-empty IPv6 on the bridge network.
- DNAT and SNAT counters increase when probes run.
- Tracker accepts announces and replies from the expected source IP.
sudo tcpdump -i eth0 -n udp port 6969 -vFor external validation, we used newTrackon and the raw status page at newtrackon.com/raw.
Cloud Provider Naming Equivalents
The same server-side setup is useful across providers, even if naming differs.
| Provider | Typical name |
|---|---|
| Hetzner | Floating IP |
| DigitalOcean | Reserved IP |
| AWS | Elastic IP |
| Linode/Akamai | Static IP |
Conclusion
To run a UDP tracker reliably behind floating IPs, you need more than DNS and a port mapping. You need symmetric routing, correct IPv6 firewall behavior, container IPv6 networking, and explicit SNAT when floating IPv6 is involved.
This is exactly how we fixed the Torrust Tracker Demo deployment on Hetzner Ubuntu. In a follow-up update, we can extend this article with packet-flow diagrams and provider-specific adaptations for DigitalOcean, AWS, and Linode.
Related Reading
If you want broader context around this setup, these articles cover the full deployment story, newTrackon requirements, and the demo infrastructure decisions.
- Deploying the Torrust Tracker Demo with the Torrust Tracker Deployer
- Submitting Trackers to newTrackon
- The New Torrust Tracker Demo Is Live
- Introducing the Torrust Tracker Deployer
- Visualize Tracker Metrics with Prometheus and Grafana
Source Links and References
The following links were used during investigation and documentation. Local files are listed with absolute paths exactly as provided.
- https://github.com/torrust/torrust-tracker-deployer/blob/main/docs/deployments/hetzner-demo-tracker/post-provision/ipv6-udp-tracker-issue.md
- https://github.com/torrust/torrust-tracker-deployer/blob/main/docs/user-guide/providers/hetzner/post-deployment.md
- https://github.com/torrust/torrust-tracker-deployer/issues/407
- https://github.com/torrust/torrust-tracker-deployer/issues/414
- https://github.com/torrust/torrust-tracker-demo/blob/main/docs/issues/ISSUE-2-udp-tracker-down-on-newtrackon.md
- https://github.com/torrust/torrust-tracker-demo/blob/main/docs/post-mortems/2026-03-09-udp-ipv6-docker.md
- https://github.com/torrust/torrust-tracker-demo/blob/main/docs/docker-ipv6.md
- https://github.com/torrust/torrust-tracker-demo/issues/2