Issue Date: January 16, 2026 Affected Component: Torrust Tracker Configuration Status: Documented - Issue filed in tracker repository Upstream Issue: torrust/torrust-tracker#1640
The Torrust Tracker has a configuration option [core.net].on_reverse_proxy that controls whether the tracker expects X-Forwarded-For HTTP headers to determine the real client IP address. This setting is global and applies to all HTTP trackers in the deployment.
This creates a limitation: you cannot have some HTTP trackers behind a reverse proxy while others are accessed directly in the same deployment.
While implementing HTTPS support with Caddy as a TLS-terminating reverse proxy in the Torrust Tracker Deployer, we needed to configure the tracker to work behind Caddy.
Our use case:
- Multiple HTTP trackers on different ports (e.g., 7070, 7071, 7072)
- Some trackers exposed via HTTPS through Caddy (TLS termination)
- Some trackers exposed directly via HTTP (no proxy)
Example configuration intent:
{
"http_trackers": [
{
"bind_address": "0.0.0.0:7070",
"domain": "http1.tracker.local",
"use_tls_proxy": true
},
{
"bind_address": "0.0.0.0:7071",
"domain": "http2.tracker.local",
"use_tls_proxy": true
},
{
"bind_address": "0.0.0.0:7072"
}
]
}In this scenario:
- Trackers on ports 7070 and 7071 are behind Caddy (need
on_reverse_proxy = true) - Tracker on port 7072 is direct (needs
on_reverse_proxy = false)
However, the current tracker configuration only allows:
[core.net]
on_reverse_proxy = true # Applies to ALL HTTP trackersThe on_reverse_proxy setting is defined in [core.net] which is a global network configuration section, not per-tracker. Looking at the tracker's network configuration structure:
Reference: Torrust Tracker Network Configuration
pub struct Network {
// ...
pub on_reverse_proxy: bool, // Global setting
// ...
}Each HTTP tracker configuration does not have its own on_reverse_proxy field.
When on_reverse_proxy = true is set globally:
- All HTTP trackers expect
X-Forwarded-Forheaders - Trackers accessed directly (without proxy) will fail to identify client IPs correctly
- The tracker will see the absence of
X-Forwarded-Forand may log warnings or behave unexpectedly
When on_reverse_proxy = false is set globally:
- All HTTP trackers ignore
X-Forwarded-Forheaders - Trackers behind a reverse proxy will see the proxy's IP as the client IP
- All peers from different clients will appear to come from the same IP (the proxy)
- This breaks peer identification in swarms
We enforce a rule in the deployer:
If ANY HTTP tracker uses a TLS proxy, ALL HTTP trackers must use the TLS proxy.
This is documented as a known limitation and validated during environment creation:
Known Limitation (due to tracker's global setting):
If you have multiple HTTP trackers where some use use_tls_proxy and others don't,
the ones without it will still receive the global on_reverse_proxy = true setting
and may fail if they receive direct requests without X-Forwarded-For headers.
Workaround: Ensure all HTTP trackers in a deployment either ALL use the TLS proxy
or NONE use it.
This limitation reduces deployment flexibility and forces users into an all-or-nothing approach.
Add an optional on_reverse_proxy field to each HTTP tracker configuration, allowing per-tracker control:
[core.net]
on_reverse_proxy = false # Default for trackers without explicit setting
[[http_trackers]]
bind_address = "0.0.0.0:7070"
on_reverse_proxy = true # Override: this tracker is behind a proxy
[[http_trackers]]
bind_address = "0.0.0.0:7071"
on_reverse_proxy = true # Override: this tracker is behind a proxy
[[http_trackers]]
bind_address = "0.0.0.0:7072"
# No override: uses global default (false) - direct access- If
on_reverse_proxyis specified on an HTTP tracker, use that value - If not specified, fall back to
[core.net].on_reverse_proxy(backward compatible) - Each HTTP tracker independently decides whether to read
X-Forwarded-For
The HTTP tracker request handler would need to check its own on_reverse_proxy setting when extracting the client IP, rather than checking the global setting.
Pseudocode change:
// Before (global check)
fn get_client_ip(request: &Request, config: &Config) -> IpAddr {
if config.core.net.on_reverse_proxy {
extract_from_x_forwarded_for(request)
} else {
request.peer_addr()
}
}
// After (per-tracker check)
fn get_client_ip(request: &Request, tracker_config: &HttpTrackerConfig) -> IpAddr {
let on_reverse_proxy = tracker_config.on_reverse_proxy
.unwrap_or(config.core.net.on_reverse_proxy);
if on_reverse_proxy {
extract_from_x_forwarded_for(request)
} else {
request.peer_addr()
}
}- Flexible deployments: Mix proxied and direct HTTP trackers in one deployment
- Backward compatible: Global setting remains the default
- Clearer intent: Each tracker explicitly declares its network topology
- Better for edge cases: Internal trackers (localhost) vs external (behind proxy)
- Mixed TLS/non-TLS deployment: Some trackers via HTTPS (Caddy), some via direct HTTP
- Internal monitoring: Direct localhost tracker for Prometheus, proxied trackers for public access
- Gradual migration: Move trackers behind proxy one at a time during migration
- Multi-tenant: Different trackers for different networks with different proxy configurations