Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
semantic-links:
skill-links:
- create-adr
related-artifacts:
- issue #1640
- issue #1978
- packages/configuration/src/v3_0_0/network.rs
- packages/configuration/src/v3_0_0/http_tracker.rs
- packages/configuration/src/v3_0_0/udp_tracker.rs
- packages/configuration/src/v3_0_0/core.rs
- docs/adrs/20260617093046_reject_wildcard_external_ip.md
- docs/adrs/20260620000000_add_ipv6_v6only_config_option.md
---

# Make Network Configuration Per Tracker Instance

## Description

Schema v2 placed `external_ip` and `on_reverse_proxy` in the global `[core.net]`
section, while `ipv6_v6only` was duplicated as a flat field on each HTTP and UDP
tracker. This model cannot represent trackers with distinct public addresses,
reverse-proxy trust policies, or socket behavior.

## Agreement

Schema v3 places one optional `network: Network` value on each `HttpTracker` and
`UdpTracker`. The corresponding TOML `[*.network]` block contains:

- `external_ip`
- `on_reverse_proxy`
- `ipv6_v6only`

When the block is omitted, it defaults to `external_ip = None`,
`on_reverse_proxy = false`, and `ipv6_v6only = false`.

Schema v3 removes `[core.net]` and the flat tracker `ipv6_v6only` fields. It
does not accept those removed fields, fall back to them, or define precedence
between the old and new layouts. Schema v2 remains separately available for
backward compatibility; application-wide migration to v3 is deferred to EPIC
subissue #1980.

When application consumers migrate to schema v3 in EPIC subissue #1980,
`AnnounceHandler` will receive the applicable instance's external IP as a
parameter instead of owning global network-topology configuration.

## Alternatives Considered

### Keep global `[core.net]`

Rejected because a global setting cannot model independent tracker instances.

### Support old and new fields in schema v3

Rejected because it would make a breaking schema ambiguous, require a precedence
rule, and leave obsolete configuration behavior in production code.

### Keep `ipv6_v6only` flat on each tracker

Rejected because all three values describe the same per-instance network topology
and socket behavior.

## Consequences

- **Positive**: Each listener has an explicit, independently configurable network identity.
- **Positive**: Reverse-proxy trust is correctly scoped to the HTTP listener handling a request.
- **Positive**: The v3 schema has one clear configuration layout with no hidden fallback.
- **Negative**: Operators must migrate v2 configuration files before using schema v3.

## Date

2026-07-21
1 change: 1 addition & 0 deletions docs/adrs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ semantic-links:
| [20260623200526](20260623200526_adopt_contract-first_architecture_for_rest_api.md) | 2026-06-23 | Adopt a contract-first architecture for the REST API | Structure the REST API into four layers: protocol contract, application/use-case, runtime adapter, and transport adapter. Enables a future tracker-agnostic REST API standard. |
| [20260629000000](20260629000000_adopt_independent_package_versioning.md) | 2026-06-29 | Adopt independent package versioning | All workspace packages version independently. Path dependencies guarantee compatibility, so linked versions are unnecessary. Enables per-package publishing and aligns with EPIC #1669 extraction goals. |
| [20260716000000](20260716000000_accept_only_ip_addresses_in_http_announce_ip_param.md) | 2026-07-16 | Accept only IP addresses in HTTP announce `ip` param | The HTTP announce `ip` GET parameter accepts only valid `IpAddr` values; DNS names are silently ignored. Matches de-facto standard of opentracker, chihaya, and all other known trackers. |
| [20260721000000](20260721000000_make_network_configuration_per_tracker_instance.md) | 2026-07-21 | Make network configuration per tracker instance | Schema v3 uses an optional `network` block on each tracker and removes global `core.net` and flat tracker networking fields without fallback. |

## ADR Lifecycle

Expand Down

Large diffs are not rendered by default.

36 changes: 21 additions & 15 deletions docs/issues/open/1978-configuration-overhaul-epic.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ status: open
github-issue: 1978
spec-path: docs/issues/open/1978-configuration-overhaul-epic.md
epic-owner: josecelano
last-updated-utc: 2026-07-20 15:25
last-updated-utc: 2026-07-21 00:00
semantic-links:
skill-links:
- create-issue
Expand Down Expand Up @@ -81,19 +81,19 @@ version from `2.0.0` to `3.0.0`.

Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`.

| Order | Issue | Local Spec | Status | Notes |
| ----- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------- |
| 1 | [#1979](../../issues/1979) — Copy `v2_0_0` → `v3_0_0` as baseline | `docs/issues/open/1979-1978-copy-configuration-schema-v2-to-v3-baseline.md` | DONE | Merged in PR #1999; v3 baseline and smoke tests are in `develop` |
| 2 | [#1981](../../issues/1981) — Fix `tsl_config` → `tls_config` typo | `docs/issues/open/1981-1978-fix-tsl-config-tls-config-typo.md` | DONE | Implemented for v3; v2 compatibility retained until final migration |
| 3 | [#1640](../../issues/1640) — Support per-HTTP-tracker `on_reverse_proxy` setting | `docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md` | TODO | Heaviest change (~30 files); establishes per-instance `Network` block |
| 4 | [#1417](../../issues/1417) — Include public service URL in configuration | `docs/issues/open/1417-1978-add-public-service-url-to-configuration.md` | TODO | Depends on #3 for `Network` placement decision; adds flat `public_url` field |
| 5 | [#1415](../../issues/1415) — Use `ServiceBinding` instead of bare `SocketAddr` for service identity | `docs/issues/open/1415-1978-use-service-binding-instead-of-socket-addr.md` | TODO | Independent; no config changes; can be parallel with #6, #7, #8, #9 |
| 6 | [#1453](../../issues/1453) — IP bans reset interval configurable + fix duplicate cleanup | `docs/issues/open/1453-1978-ip-bans-reset-interval-configurable.md` | TODO | Independent global UDP policy; implement before #7 to establish its boundary |
| 7 | [#1136](../../issues/1136) — Add configurable UDP connection ID validation policy | `docs/issues/open/1136-1978-configurable-udp-connection-id-validation-policy.md` | TODO | Independent per-listener policy; ordered after related global ban cleanup in #6 |
| 8 | [#1490](../../issues/1490) — Decompose database config and overhaul secrets with `secrecy` crate | `docs/issues/open/1490-1978-decompose-database-config-and-overhaul-secrets.md` | TODO | After #3 (both touch `Core`); can be parallel with #5, #6, #7, #9 |
| 9 | [#889](../../issues/889) — New config option for logging style | `docs/issues/open/889-1978-new-config-option-for-logging-style.md` | TODO | Independent; can be parallel with #5, #6, #7, #8 |
| 10 | [#1987](../../issues/1987) — Use peer IP from the HTTP announce `ip` parameter when configured | `docs/issues/open/1987-add-config-option-to-use-ip-from-announce-query-string/ISSUE.md` | TODO | After #3 and external prerequisite #1985; per-HTTP-tracker opt-in policy |
| 11 | [#1980](../../issues/1980) — Final cleanup: remove global re-exports, migrate consumers to explicit v3 imports | `docs/issues/open/1980-1978-configuration-overhaul-final-cleanup.md` | TODO | Must be last; depends on ALL other subissues |
| Order | Issue | Local Spec | Status | Notes |
| ----- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------- |
| 1 | [#1979](../../issues/1979) — Copy `v2_0_0` → `v3_0_0` as baseline | `docs/issues/open/1979-1978-copy-configuration-schema-v2-to-v3-baseline.md` | DONE | Merged in PR #1999; v3 baseline and smoke tests are in `develop` |
| 2 | [#1981](../../issues/1981) — Fix `tsl_config` → `tls_config` typo | `docs/issues/open/1981-1978-fix-tsl-config-tls-config-typo.md` | DONE | Implemented for v3; v2 compatibility retained until final migration |
| 3 | [#1640](../../issues/1640) — Support per-HTTP-tracker `on_reverse_proxy` setting | `docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md` | IN_PROGRESS | Heaviest change (~30 files); establishes per-instance `Network` block |
| 4 | [#1417](../../issues/1417) — Include public service URL in configuration | `docs/issues/open/1417-1978-add-public-service-url-to-configuration.md` | TODO | Depends on #3 for `Network` placement decision; adds flat `public_url` field |
| 5 | [#1415](../../issues/1415) — Use `ServiceBinding` instead of bare `SocketAddr` for service identity | `docs/issues/open/1415-1978-use-service-binding-instead-of-socket-addr.md` | TODO | Independent; no config changes; can be parallel with #6, #7, #8, #9 |
| 6 | [#1453](../../issues/1453) — IP bans reset interval configurable + fix duplicate cleanup | `docs/issues/open/1453-1978-ip-bans-reset-interval-configurable.md` | TODO | Independent global UDP policy; implement before #7 to establish its boundary |
| 7 | [#1136](../../issues/1136) — Add configurable UDP connection ID validation policy | `docs/issues/open/1136-1978-configurable-udp-connection-id-validation-policy.md` | TODO | Independent per-listener policy; ordered after related global ban cleanup in #6 |
| 8 | [#1490](../../issues/1490) — Decompose database config and overhaul secrets with `secrecy` crate | `docs/issues/open/1490-1978-decompose-database-config-and-overhaul-secrets.md` | TODO | After #3 (both touch `Core`); can be parallel with #5, #6, #7, #9 |
| 9 | [#889](../../issues/889) — New config option for logging style | `docs/issues/open/889-1978-new-config-option-for-logging-style.md` | TODO | Independent; can be parallel with #5, #6, #7, #8 |
| 10 | [#1987](../../issues/1987) — Use peer IP from the HTTP announce `ip` parameter when configured | `docs/issues/open/1987-add-config-option-to-use-ip-from-announce-query-string/ISSUE.md` | TODO | After #3 and external prerequisite #1985; per-HTTP-tracker opt-in policy |
| 11 | [#1980](../../issues/1980) — Final cleanup: remove global re-exports, migrate consumers to explicit v3 imports | `docs/issues/open/1980-1978-configuration-overhaul-final-cleanup.md` | TODO | Must be last; depends on ALL other subissues |

## Delivery Strategy

Expand Down Expand Up @@ -146,7 +146,7 @@ Subissues #5, #6, #7, #9 are independent and can run in parallel with the critic

### Phase 1: Structural changes (sequential)

- **Subissue #3** (#1640) — Per-instance `Network` block. Heaviest change (~30 files). Establishes the `Network` struct that #4 references.
- **Subissue #3** (#1640) — Per-instance `Network` block in schema v3.0.0. Establishes the `Network` struct that #4 references; v3 does not support removed v2 field names.
- **Subissue #8** (#1490) — Database enum decomposition + `secrecy` crate. After #3 (both touch `Core`). ~35 files.
- **Subissue #4** (#1417) — `public_url` flat field. After #3 (depends on `Network` placement decision). ~6 files.
- **Subissue #10** (#1987) — Opt-in use of the HTTP announce `ip` parameter. After #3 and external prerequisite #1985.
Expand Down Expand Up @@ -202,6 +202,12 @@ For each subissue implementation in this EPIC, the default completion policy is:
started #1981 as the next subissue; identified its schema compatibility boundary for maintainer review.
- 2026-07-20 15:25 UTC - agent - Completed #1981 with v3-corrected TLS names and
schema-neutral module naming; preserved v2 compatibility and verified the full workspace. #1640 is next.
- 2026-07-21 00:00 UTC - agent - Started #1640 as the next sequential EPIC subissue.
Maintainer confirmed the per-instance field as `network: Network`; its TOML block is optional
and defaults to `external_ip = None`, `on_reverse_proxy = false`, and `ipv6_v6only = false`.
- 2026-07-21 00:00 UTC - josecelano - Confirmed schema compatibility boundary for #1640:
v3 uses only the new per-instance `network` fields with no fallback or precedence for removed
v2 fields. The application-wide v2-to-v3 consumer and default-config migration remains #1980.

## Acceptance Criteria

Expand Down
11 changes: 1 addition & 10 deletions packages/configuration/src/v3_0_0/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ use serde::{Deserialize, Serialize};
use torrust_tracker_primitives::announce::AnnouncePolicy;
use torrust_tracker_primitives::{PrivateMode, TrackerPolicy};

use super::network::Network;
use crate::v3_0_0::database::Database;
use crate::validator::{SemanticValidationError, Validator};

#[allow(clippy::struct_excessive_bools)]
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct Core {
/// Announce policy configuration.
#[serde(default = "Core::default_announce_policy")]
Expand All @@ -26,10 +26,6 @@ pub struct Core {
#[serde(default = "Core::default_listed")]
pub listed: bool,

/// Network configuration.
#[serde(default = "Core::default_network")]
pub net: Network,

/// When `true` clients require a key to connect and use the tracker.
#[serde(default = "Core::default_private")]
pub private: bool,
Expand Down Expand Up @@ -58,7 +54,6 @@ impl Default for Core {
database: Self::default_database(),
inactive_peer_cleanup_interval: Self::default_inactive_peer_cleanup_interval(),
listed: Self::default_listed(),
net: Self::default_network(),
private: Self::default_private(),
private_mode: Self::default_private_mode(),
tracker_policy: Self::default_tracker_policy(),
Expand All @@ -84,10 +79,6 @@ impl Core {
false
}

fn default_network() -> Network {
Network::default()
}

fn default_private() -> bool {
false
}
Expand Down
22 changes: 8 additions & 14 deletions packages/configuration/src/v3_0_0/http_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use serde::{Deserialize, Serialize};
use serde_with::serde_as;

use crate::v3_0_0::network::Network;
use crate::v3_0_0::tls::TlsConfig;

/// Configuration for each HTTP tracker.
#[serde_as]
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct HttpTracker {
/// The address the tracker will bind to.
/// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to
Expand All @@ -24,17 +26,9 @@ pub struct HttpTracker {
#[serde(default = "HttpTracker::default_tracker_usage_statistics")]
pub tracker_usage_statistics: bool,

/// Whether to set `IPV6_V6ONLY=1` on IPv6 sockets.
///
/// When `true` (IPv6-only), the tracker must also bind an IPv4 socket
/// (e.g. `0.0.0.0:<port>`) to accept IPv4 connections.
/// When `false` (default), the socket option is not overridden and the
/// OS default applies (dual-stack on Linux, IPv6-only on other platforms).
///
/// > **Platform note**: On OpenBSD, `IPV6_V6ONLY` is always `1` and cannot
/// > be disabled; setting this to `false` is a no-op.
#[serde(default = "HttpTracker::default_ipv6_v6only")]
pub ipv6_v6only: bool,
/// Per-instance network topology and socket behavior.
#[serde(default = "HttpTracker::default_network")]
pub network: Network,
}

impl Default for HttpTracker {
Expand All @@ -43,7 +37,7 @@ impl Default for HttpTracker {
bind_address: Self::default_bind_address(),
tls_config: Self::default_tls_config(),
tracker_usage_statistics: Self::default_tracker_usage_statistics(),
ipv6_v6only: Self::default_ipv6_v6only(),
network: Self::default_network(),
}
}
}
Expand All @@ -61,8 +55,8 @@ impl HttpTracker {
false
}

fn default_ipv6_v6only() -> bool {
false
fn default_network() -> Network {
Network::default()
}
}

Expand Down
Loading
Loading