From acb77ba9d4909cf188bacfc35f2bbb46eb91b7cd Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 21 Jul 2026 10:55:02 +0100 Subject: [PATCH 1/7] docs(configuration): record per-instance network design --- ...r-http-tracker-on-reverse-proxy-setting.md | 115 +++++++++--------- .../open/1978-configuration-overhaul-epic.md | 31 ++--- 2 files changed, 75 insertions(+), 71 deletions(-) diff --git a/docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md b/docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md index 21ff2017c..9b7b30e19 100644 --- a/docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md +++ b/docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md @@ -7,7 +7,7 @@ github-issue: 1640 spec-path: docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md branch: "1640-move-network-to-per-instance-config" related-pr: null -last-updated-utc: 2026-06-23 18:30 +last-updated-utc: 2026-07-21 00:00 semantic-links: skill-links: - create-issue @@ -84,7 +84,7 @@ ipv6_v6only = true # field directly in UdpTracker [[http_trackers]] bind_address = "0.0.0.0:7070" -[http_trackers.net] +[http_trackers.network] external_ip = "203.0.113.5" on_reverse_proxy = true ipv6_v6only = false @@ -92,7 +92,7 @@ ipv6_v6only = false [[udp_trackers]] bind_address = "0.0.0.0:6969" -[udp_trackers.net] +[udp_trackers.network] external_ip = "2001:db8::1" on_reverse_proxy = false ipv6_v6only = true @@ -105,7 +105,7 @@ The JSON form makes the per-instance structure clearer: "http_trackers": [ { "bind_address": "0.0.0.0:7070", - "net": { + "network": { "external_ip": "203.0.113.5", "on_reverse_proxy": true, "ipv6_v6only": false @@ -115,7 +115,7 @@ The JSON form makes the per-instance structure clearer: "udp_trackers": [ { "bind_address": "0.0.0.0:6969", - "net": { + "network": { "external_ip": "2001:db8::1", "on_reverse_proxy": false, "ipv6_v6only": true @@ -150,8 +150,8 @@ pub struct HttpTracker { pub bind_address: SocketAddr, pub tls_config: Option, pub tracker_usage_statistics: bool, - pub net: Network, // ← replaces individual fields - // ipv6_v6only REMOVED — now inside net + pub network: Network, // ← replaces individual fields + // ipv6_v6only REMOVED — now inside network } // Server-layer config for each UDP tracker @@ -160,17 +160,17 @@ pub struct UdpTracker { pub cookie_lifetime: Duration, pub tracker_usage_statistics: bool, pub max_connection_id_errors_per_ip: u32, - pub net: Network, // ← replaces individual fields - // ipv6_v6only REMOVED — now inside net + pub network: Network, // ← replaces individual fields + // ipv6_v6only REMOVED — now inside network } -// Core — no longer has a net field +// Core — no longer has a network field pub struct Core { pub announce_policy: AnnouncePolicy, pub database: Database, pub inactive_peer_cleanup_interval: u64, pub listed: bool, - // net: Network REMOVED + // network: Network REMOVED pub private: bool, pub private_mode: Option, pub tracker_policy: TrackerPolicy, @@ -178,20 +178,20 @@ pub struct Core { } ``` -### Design Note: `bind_address` stays flat (not inside `net`) +### Design Note: `bind_address` stays flat (not inside `network`) We considered moving `bind_address` into `Network` since it is a networking concern. We decided to keep it flat for two reasons: -1. **Primary key role**: `bind_address` is the HashMap key for tracker instance containers in `AppContainer` (`HashMap>`). Nesting it inside `net` would make lookup more cumbersome without benefit. -2. **TLS asymmetry**: `tls_config` (TLS certificate paths) cannot go into `Network`. Keeping `bind_address` and `tls_config` at the same level while `on_reverse_proxy`, `external_ip`, and `ipv6_v6only` group into `net` creates a cleaner boundary between _socket binding_ (flat) and _socket behaviour / network identity_ (grouped). +1. **Primary key role**: `bind_address` is the HashMap key for tracker instance containers in `AppContainer` (`HashMap>`). Nesting it inside `network` would make lookup more cumbersome without benefit. +2. **TLS asymmetry**: `tls_config` (TLS certificate paths) cannot go into `Network`. Keeping `bind_address` and `tls_config` at the same level while `on_reverse_proxy`, `external_ip`, and `ipv6_v6only` group into `network` creates a cleaner boundary between _socket binding_ (flat) and _socket behaviour / network identity_ (grouped). ### Compatibility with Existing ADRs -| ADR | Impact | Status | -| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -| `20260617093046` (reject wildcard `external_ip`) | `ExternalIp` newtype unchanged. `external_ip` moves location (from `core.net` to `http_trackers[].net`). The `Network` struct with its `ExternalIp` field stays in `network.rs` as a shared definition. | ✅ Compatible. ADR says "no schema change" — needs updating since this issue changes the location. | -| `20260620000000` (add `ipv6_v6only` option) | Field moves from flat `HttpTracker.ipv6_v6only` / `UdpTracker.ipv6_v6only` to `HttpTracker.net.ipv6_v6only` / `UdpTracker.net.ipv6_v6only`. Default (`false`) and behaviour unchanged. | ✅ Compatible. ADR needs updating to reflect new field path. | -| `20260527175600` (keep protocol/domain decoupled) | Not directly related — this issue touches configuration types and service-layer code, not protocol types. | ✅ No impact. | +| ADR | Impact | Status | +| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| `20260617093046` (reject wildcard `external_ip`) | `ExternalIp` newtype unchanged. `external_ip` moves location (from `core.net` to `http_trackers[].network`). The `Network` struct with its `ExternalIp` field stays in `network.rs` as a shared definition. | ✅ Compatible. ADR says "no schema change" — needs updating since this issue changes the location. | +| `20260620000000` (add `ipv6_v6only` option) | Field moves from flat `HttpTracker.ipv6_v6only` / `UdpTracker.ipv6_v6only` to `HttpTracker.network.ipv6_v6only` / `UdpTracker.network.ipv6_v6only`. Default (`false`) and behaviour unchanged. | ✅ Compatible. ADR needs updating to reflect new field path. | +| `20260527175600` (keep protocol/domain decoupled) | Not directly related — this issue touches configuration types and service-layer code, not protocol types. | ✅ No impact. | ### User-Facing Migration Note @@ -217,13 +217,13 @@ ipv6_v6only = false [[http_trackers]] bind_address = "0.0.0.0:7070" -[http_trackers.net] +[http_trackers.network] external_ip = "203.0.113.5" on_reverse_proxy = true ipv6_v6only = false ``` -The old `[core.net]` section is no longer valid. Each tracker instance must have its own `net` block. The `external_ip` and `on_reverse_proxy` values must be moved into each `[[http_trackers]].net` (and/or `[[udp_trackers]].net`) block. Leaving them out means `false` / `None` defaults apply. +The old `[core.net]` section is no longer valid. Each tracker instance has its own `Network` configuration. The TOML `network` block is optional and defaults to `external_ip = None`, `on_reverse_proxy = false`, and `ipv6_v6only = false` when omitted. The `external_ip` and `on_reverse_proxy` values must be moved into each configured `[[http_trackers]].network` (and/or `[[udp_trackers]].network`) block. ### Future Extensions (not implemented in this issue) @@ -270,14 +270,14 @@ These fields (`domain`, `use_tls_proxy`) describe how each tracker instance is e Issue [#1417](https://github.com/torrust/torrust-tracker/issues/1417) adds an optional `public_url: Option` field to each tracker instance (`HttpTracker`, `UdpTracker`) and API service (`HttpApi`, `HealthCheckApi`). This field is **implemented in this EPIC** (not a future extension) but lives as a **flat field** on each config struct — **not inside `Network`**. -**Why flat, not inside `Network`**: The `Network` block groups **network topology** concerns (how the tracker connects: external IP, proxy awareness, socket behaviour). `public_url` is about **public exposure** (how users reach the tracker). It's a different axis — one tracker instance might have both a `net.on_reverse_proxy` setting and a `public_url`, and they are independently configurable. +**Why flat, not inside `Network`**: The `Network` block groups **network topology** concerns (how the tracker connects: external IP, proxy awareness, socket behaviour). `public_url` is about **public exposure** (how users reach the tracker). It's a different axis — one tracker instance might have both a `network.on_reverse_proxy` setting and a `public_url`, and they are independently configurable. ```toml [[http_trackers]] bind_address = "0.0.0.0:7070" public_url = "https://tracker.torrust-demo.com/announce" -[http_trackers.net] +[http_trackers.network] external_ip = "203.0.113.5" on_reverse_proxy = true ipv6_v6only = false @@ -313,7 +313,7 @@ pub struct HttpTracker { pub public_url: Option, // ‡ #1417 — full URL (e.g. "https://tracker1.example.com/announce") // Network topology (grouped) - pub net: Network, // † new + pub network: Network, // † new } /// Server-layer config for each UDP tracker. @@ -327,7 +327,7 @@ pub struct UdpTracker { pub public_url: Option, // ‡ #1417 — full URL (e.g. "udp://tracker1.example.com:6969") // Network topology (grouped) - pub net: Network, // † new + pub network: Network, // † new } /// Core — no longer has any networking config. @@ -336,7 +336,7 @@ pub struct Core { pub database: Database, pub inactive_peer_cleanup_interval: u64, pub listed: bool, - // net: Network REMOVED † + // network: Network REMOVED † pub private: bool, pub private_mode: Option, pub tracker_policy: TrackerPolicy, @@ -348,11 +348,11 @@ pub struct Core { The `Network` block groups **network topology** concerns — how the tracker instance connects to the network (external IP, proxy awareness, socket behaviour). `public_url` is about **public exposure** — how users reach the tracker. These are different axes: -- A tracker behind a reverse proxy might have `net.on_reverse_proxy = true` and `public_url = "https://tracker.example.com/announce"` -- A directly-exposed tracker might have `net.on_reverse_proxy = false` and `public_url = "http://tracker.example.com:7070/announce"` +- A tracker behind a reverse proxy might have `network.on_reverse_proxy = true` and `public_url = "https://tracker.example.com/announce"` +- A directly-exposed tracker might have `network.on_reverse_proxy = false` and `public_url = "http://tracker.example.com:7070/announce"` - Both fields are independently configurable; nesting one inside the other would be misleading -The `AnnounceHandler` in `tracker-core` stops reading from `self.config.net.external_ip` and instead accepts it as a parameter: +The `AnnounceHandler` in `tracker-core` stops reading the global configuration's `external_ip` and instead accepts it as a parameter: ```rust pub async fn handle_announcement( @@ -373,11 +373,11 @@ pub async fn handle_announcement( ### In Scope (all phases) -- Add `Network` (with `external_ip`, `on_reverse_proxy`, `ipv6_v6only`) as per-instance field in both `HttpTracker` and `UdpTracker` +- Add `network: Network` (with `external_ip`, `on_reverse_proxy`, `ipv6_v6only`) as an optional-in-TOML, per-instance field in both `HttpTracker` and `UdpTracker` - Remove `Network` from `Core` (remove `core.net` entirely) - Modify `AnnounceHandler::handle_announcement()` to accept `external_ip` per-call instead of reading from global config - Update all callers of `handle_announcement()` (HTTP services, UDP services, tests) to pass per-instance `external_ip` -- Update all consumers of `ipv6_v6only` to read from `HttpTracker.net` / `UdpTracker.net` instead of flat struct fields +- Update all consumers of `ipv6_v6only` to read from `HttpTracker.network` / `UdpTracker.network` instead of flat struct fields - Remove deprecated flat `ipv6_v6only` fields from `HttpTracker` and `UdpTracker` - Update test helpers, default config TOML files, integration tests, docs, and doc comments - Write ADR for the architecture decision @@ -406,9 +406,9 @@ Write the Architectural Decision Record documenting: - Why `external_ip` becomes a parameter of `handle_announcement()` - Why `ipv6_v6only` joins `Network` -### Phase 1 — Add `net: Network` to `HttpTracker` and `UdpTracker` (parallel add) +### Phase 1 — Add `network: Network` to `HttpTracker` and `UdpTracker` (parallel add) -Add the new `net: Network` field to both tracker config structs. Keep the old fields (`core.net`, flat `ipv6_v6only`) for now. `Network` gains `ipv6_v6only`. +Add the new `network: Network` field to both tracker config structs. Keep the old fields (`core.net`, flat `ipv6_v6only`) for now. `Network` gains `ipv6_v6only`. The TOML block is optional and deserializes to the safe defaults below when omitted. Default for `Network`: @@ -424,7 +424,7 @@ Network { ### Phase 2 — Modify `AnnounceHandler::handle_announcement()` to accept `external_ip` -Add `tracker_external_ip: Option` parameter to `handle_announcement()`. The callers temporarily pass `self.config.net.external_ip.map(Into::into)` (still reading from the old global for now). +Add `tracker_external_ip: Option` parameter to `handle_announcement()`. The callers temporarily pass the old global `external_ip` value (still reading from `core.net` for now). **Verification**: All `handle_announcement()` call sites compile. No behaviour change. @@ -434,7 +434,7 @@ This is the largest phase, split into sub-tasks (each committed and CI-verified #### 3a. `on_reverse_proxy` -- `test-helpers`: Set per-tracker `on_reverse_proxy` in `HttpTracker` instead of `core.net` +- `test-helpers`: Set per-tracker `on_reverse_proxy` in `HttpTracker.network` instead of `core.net` - `http-core/services/announce.rs` + `scrape.rs`: Read from per-instance `ReverseProxyMode` (Approach B) - `HttpTrackerCoreServices` + `HttpTrackerCoreContainer`: Create per-instance services - `src/container.rs`: Flow per-instance mode through `AppContainer` @@ -442,13 +442,13 @@ This is the largest phase, split into sub-tasks (each committed and CI-verified #### 3b. `ipv6_v6only` -- `HttpTracker` consumers (`server.rs`, `environment.rs`, `bootstrap/jobs/http_tracker.rs`, contract tests): Read from `http_tracker_config.net.ipv6_v6only` -- `UdpTracker` consumers (`launcher.rs`, contract tests): Read from `udp_tracker_config.net.ipv6_v6only` +- `HttpTracker` consumers (`server.rs`, `environment.rs`, `bootstrap/jobs/http_tracker.rs`, contract tests): Read from `http_tracker_config.network.ipv6_v6only` +- `UdpTracker` consumers (`launcher.rs`, contract tests): Read from `udp_tracker_config.network.ipv6_v6only` #### 3c. `external_ip` -- `udp-server` tests: Pass per-tracker `external_ip` to `handle_announcement()` (now available from `udp_tracker_config.net.external_ip`) -- `http-core` tests: Pass per-tracker `external_ip` to `handle_announcement()` (now available from `http_tracker_config.net.external_ip`) +- `udp-server` tests: Pass per-tracker `external_ip` to `handle_announcement()` (now available from `udp_tracker_config.network.external_ip`) +- `http-core` tests: Pass per-tracker `external_ip` to `handle_announcement()` (now available from `http_tracker_config.network.external_ip`) - `axum-http-server` contract tests: Same ### Phase 4 — Remove deprecated fields @@ -471,18 +471,18 @@ This is the largest phase, split into sub-tasks (each committed and CI-verified **Chosen approach**: **Approach B** (per-instance services with `reverse_proxy_mode` field) for `on_reverse_proxy` threading. -| ID | Phase | Status | Task | Notes | -| --- | ----- | ------ | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -| T0 | 0 | TODO | Write ADR | Record: move `Network` to per-instance, parameterize `external_ip`, join `ipv6_v6only` into `Network` | -| T1 | 1 | TODO | Add `net: Network` (with `ipv6_v6only`) to `HttpTracker` and `UdpTracker` | Parallel add — old fields kept. Default `ipv6_v6only = false` | -| T2 | 2 | TODO | Add `tracker_external_ip` param to `handle_announcement()` | Callers pass old global value temporarily | -| T3a | 3a | TODO | Switch `on_reverse_proxy` consumers to per-instance | Approach B: per-instance services with `ReverseProxyMode` | -| T3b | 3b | TODO | Switch `ipv6_v6only` consumers to `net.ipv6_v6only` | HTTP + UDP server launchers, tests | -| T3c | 3c | TODO | Switch `external_ip` consumers | All callers of `handle_announcement()` pass per-instance value | -| T4 | 4 | TODO | Remove deprecated fields | `core.net`, flat `ipv6_v6only`, `get_ext_ip()` | -| T5 | 4 | TODO | Update default config TOML files | 6 files in `share/default/config/` | -| T6 | 4 | TODO | Update docs and doc comments | `mod.rs`, `lib.rs`, `containers.md`, `tracker-core/lib.rs`, `extractors/client_ip_sources.rs` | -| T7 | 5 | TODO | Run `linter all` and full test suite | | +| ID | Phase | Status | Task | Notes | +| --- | ----- | ------ | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| T0 | 0 | TODO | Write ADR | Record: move `Network` to per-instance, parameterize `external_ip`, join `ipv6_v6only` into `Network` | +| T1 | 1 | TODO | Add `network: Network` (with `ipv6_v6only`) to `HttpTracker` and `UdpTracker` | Parallel add — old fields kept. TOML block defaults safely when omitted | +| T2 | 2 | TODO | Add `tracker_external_ip` param to `handle_announcement()` | Callers pass old global value temporarily | +| T3a | 3a | TODO | Switch `on_reverse_proxy` consumers to per-instance | Approach B: per-instance services with `ReverseProxyMode` | +| T3b | 3b | TODO | Switch `ipv6_v6only` consumers to `network.ipv6_v6only` | HTTP + UDP server launchers, tests | +| T3c | 3c | TODO | Switch `external_ip` consumers | All callers of `handle_announcement()` pass per-instance value | +| T4 | 4 | TODO | Remove deprecated fields | `core.net`, flat `ipv6_v6only`, `get_ext_ip()` | +| T5 | 4 | TODO | Update default config TOML files | 6 files in `share/default/config/` | +| T6 | 4 | TODO | Update docs and doc comments | `mod.rs`, `lib.rs`, `containers.md`, `tracker-core/lib.rs`, `extractors/client_ip_sources.rs` | +| T7 | 5 | TODO | Run `linter all` and full test suite | | ## Progress Tracking @@ -491,10 +491,10 @@ This is the largest phase, split into sub-tasks (each committed and CI-verified - [ ] Spec drafted in `docs/issues/open/` - [ ] Spec reviewed and approved by user/maintainer - [ ] Phase 0: ADR created -- [ ] Phase 1: `net: Network` added to `HttpTracker` and `UdpTracker` (parallel with old fields) +- [ ] Phase 1: `network: Network` added to `HttpTracker` and `UdpTracker` (parallel with old fields) - [ ] Phase 2: `handle_announcement()` accepts `tracker_external_ip` param - [ ] Phase 3a: `on_reverse_proxy` consumers switched to per-instance -- [ ] Phase 3b: `ipv6_v6only` consumers switched to `net.ipv6_v6only` +- [ ] Phase 3b: `ipv6_v6only` consumers switched to `network.ipv6_v6only` - [ ] Phase 3c: `external_ip` consumers switched to per-instance - [ ] Phase 4: Deprecated fields removed (`core.net`, flat `ipv6_v6only`) - [ ] Phase 5: Final verification completed (`linter all`, full test suite) @@ -515,16 +515,17 @@ Append one line per meaningful update. - 2026-06-23 17:45 UTC - Copilot - Added design note on `bind_address` staying flat, future extensions section (`domain`, `use_tls_proxy`, `public_url`) referencing deployer and issue #1417. - 2026-06-23 18:30 UTC - Copilot - Completed deep review against ADRs 20260617093046, 20260620000000, 20260527175600 and issues #1417, #1671. Added compatibility table and migration note. - 2026-07-14 00:00 UTC - josecelano - Resolved #1417 relationship: `public_url` is in this EPIC (not future), stays flat (not inside `Network`). Replaced "Future Extensions" section with "Related Issue: #1417" section. Updated config types to show `public_url` as `‡` field. Added versioning note (app 4.0.0, config schema 3.0.0). +- 2026-07-21 00:00 UTC - josecelano - Confirmed `network` as the per-instance field name, aligned with the `Network` type. Confirmed the TOML `[*.network]` block is optional and defaults to `external_ip = None`, `on_reverse_proxy = false`, and `ipv6_v6only = false`. ## Acceptance Criteria -- [ ] AC1: `on_reverse_proxy` is removed from `[core.net]` and placed per-instance in `HttpTracker.net.on_reverse_proxy` (and `UdpTracker.net.on_reverse_proxy` for future UDP proxy use) -- [ ] AC2: `external_ip` is removed from `[core.net]` and placed per-instance in `HttpTracker.net.external_ip` and `UdpTracker.net.external_ip` -- [ ] AC3: `ipv6_v6only` is moved from flat `HttpTracker.ipv6_v6only` and `UdpTracker.ipv6_v6only` into `HttpTracker.net` / `UdpTracker.net` +- [ ] AC1: `on_reverse_proxy` is removed from `[core.net]` and placed per-instance in `HttpTracker.network.on_reverse_proxy` (and `UdpTracker.network.on_reverse_proxy` for future UDP proxy use) +- [ ] AC2: `external_ip` is removed from `[core.net]` and placed per-instance in `HttpTracker.network.external_ip` and `UdpTracker.network.external_ip` +- [ ] AC3: `ipv6_v6only` is moved from flat `HttpTracker.ipv6_v6only` and `UdpTracker.ipv6_v6only` into `HttpTracker.network` / `UdpTracker.network` - [ ] AC4: `Core.net` (the `Network` struct) is removed from `Core` - [ ] AC5: `AnnounceHandler::handle_announcement()` accepts `tracker_external_ip` per-call instead of reading from global config - [ ] AC6: Two HTTP trackers with different `on_reverse_proxy` settings behave independently: - Tracker A (`on_reverse_proxy = true`) reads `X-Forwarded-For` headers - Tracker B (`on_reverse_proxy = false` or unset) reads connection info IP -- [ ] AC7: Example `http_only_public_tracker.rs` builds with the new `HttpTracker.net.on_reverse_proxy` field +- [ ] AC7: Example `http_only_public_tracker.rs` builds with the new `HttpTracker.network.on_reverse_proxy` field - [ ] AC8: All default config files and docs use the new format - [ ] `linter all` exits with code `0` - [ ] Relevant tests pass diff --git a/docs/issues/open/1978-configuration-overhaul-epic.md b/docs/issues/open/1978-configuration-overhaul-epic.md index 056ee3f09..b096b1d95 100644 --- a/docs/issues/open/1978-configuration-overhaul-epic.md +++ b/docs/issues/open/1978-configuration-overhaul-epic.md @@ -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 @@ -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 @@ -202,6 +202,9 @@ 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`. ## Acceptance Criteria From e238ff0c5ae783e86d482449efab68e7a0569ff4 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 21 Jul 2026 11:19:07 +0100 Subject: [PATCH 2/7] docs(issues): clarify v3 network schema boundary --- ...r-http-tracker-on-reverse-proxy-setting.md | 73 +++++++++++-------- .../open/1978-configuration-overhaul-epic.md | 5 +- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md b/docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md index 9b7b30e19..4182b597d 100644 --- a/docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md +++ b/docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md @@ -54,6 +54,18 @@ Give each tracker instance (`HttpTracker` and `UdpTracker`) its own `Network` co **End state**: Every tracker instance has its own networking config — socket behaviour, proxy awareness, and peer-IP replacement are all per-instance concerns. The shared `Core` only holds truly cross-cutting settings (database, policy, private mode). +### Schema Compatibility Boundary + +This issue changes **only schema `v3.0.0`**. Schema `v2.0.0` remains unchanged in its +separate module for compatibility, but `v3_0_0` must exclusively use the per-instance +`network` fields. It must not deserialize, fall back to, or define precedence for the +removed `[core.net]` section or the removed flat `ipv6_v6only` fields. + +The application-wide migration from v2 configuration types to v3 configuration types is +the responsibility of EPIC subissue #1980. Once that migration is complete, production +code will use only the v3 per-instance `network` values. No runtime compatibility bridge +between the v2 and v3 field layouts is required or permitted. + ## Background The issue was originally opened to allow per-HTTP-tracker `on_reverse_proxy` settings. During analysis we discovered a broader architectural problem: the entire `Network` struct (`external_ip`, `on_reverse_proxy`, `ipv6_v6only`) lived in `[core.net]` as a **global singleton** shared by all tracker instances. This caused three separate issues: @@ -379,24 +391,20 @@ pub async fn handle_announcement( - Update all callers of `handle_announcement()` (HTTP services, UDP services, tests) to pass per-instance `external_ip` - Update all consumers of `ipv6_v6only` to read from `HttpTracker.network` / `UdpTracker.network` instead of flat struct fields - Remove deprecated flat `ipv6_v6only` fields from `HttpTracker` and `UdpTracker` -- Update test helpers, default config TOML files, integration tests, docs, and doc comments +- Update v3 configuration tests, docs, and doc comments - Write ADR for the architecture decision ### Out of Scope - TOML config migration tooling +- Migrating application consumers, test helpers, or default configuration files from schema v2 to v3 (subissue #1980) +- Supporting removed v2 fields in schema v3 or defining old-versus-new field precedence ## Approach B — Per-instance services (chosen) For the `on_reverse_proxy` threading, we use **Approach B** (as analysed earlier): each `HttpTrackerCoreContainer` creates per-instance `AnnounceService` and `ScrapeService` storing their own `ReverseProxyMode`. This avoids extending Axum state tuples and keeps handler signatures stable. The full analysis is preserved below in the appendix. -## Implementation Strategy: Baby Steps + Parallel Changes + Draft PR - -**Key principles**: - -1. **Baby steps**: Each commit is a small, verifiable change -2. **Parallel changes**: Introduce new code paths alongside old ones before deleting the old ones -3. **Draft PR**: Open early and keep updated after each commit, running CI checks continuously +## Implementation Strategy ### Phase 0 — ADR @@ -406,9 +414,11 @@ Write the Architectural Decision Record documenting: - Why `external_ip` becomes a parameter of `handle_announcement()` - Why `ipv6_v6only` joins `Network` -### Phase 1 — Add `network: Network` to `HttpTracker` and `UdpTracker` (parallel add) +### Phase 1 — Define the v3 per-instance `Network` -Add the new `network: Network` field to both tracker config structs. Keep the old fields (`core.net`, flat `ipv6_v6only`) for now. `Network` gains `ipv6_v6only`. The TOML block is optional and deserializes to the safe defaults below when omitted. +Add the new `network: Network` field to both tracker config structs. Remove `core.net` and the +flat `ipv6_v6only` fields from v3 at the same time. `Network` gains `ipv6_v6only`. The TOML +block is optional and deserializes to the safe defaults below when omitted. Default for `Network`: @@ -420,11 +430,13 @@ Network { } ``` -**Verification**: Config deserialization still works with both old and new formats. All existing tests pass unchanged. +**Verification**: V3 configuration deserializes with an omitted `network` block and rejects the +removed v2 field layout. Schema v2 tests remain unchanged. ### Phase 2 — Modify `AnnounceHandler::handle_announcement()` to accept `external_ip` -Add `tracker_external_ip: Option` parameter to `handle_announcement()`. The callers temporarily pass the old global `external_ip` value (still reading from `core.net` for now). +Add `tracker_external_ip: Option` parameter to `handle_announcement()`. V3 consumers +pass their instance's `network.external_ip`; no caller reads `core.net`. **Verification**: All `handle_announcement()` call sites compile. No behaviour change. @@ -451,14 +463,12 @@ This is the largest phase, split into sub-tasks (each committed and CI-verified - `http-core` tests: Pass per-tracker `external_ip` to `handle_announcement()` (now available from `http_tracker_config.network.external_ip`) - `axum-http-server` contract tests: Same -### Phase 4 — Remove deprecated fields +### Phase 4 — Complete the v3 schema boundary - Delete `core.net` from `Core` struct. Keep `network.rs` with both `Network` and `ExternalIp` — both `HttpTracker` and `UdpTracker` import `Network` from there (single definition, no duplication). - Delete flat `ipv6_v6only` fields from `HttpTracker` and `UdpTracker` - Delete `get_ext_ip()` from `Configuration` (no longer needed — each instance has its own `external_ip`) -- Update default TOML files to use the new format -- Update all doc comments and crate-level docs -- Update `docs/containers.md` +- Update v3 doc comments and crate-level docs ### Phase 5 — Final verification @@ -471,18 +481,17 @@ This is the largest phase, split into sub-tasks (each committed and CI-verified **Chosen approach**: **Approach B** (per-instance services with `reverse_proxy_mode` field) for `on_reverse_proxy` threading. -| ID | Phase | Status | Task | Notes | -| --- | ----- | ------ | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -| T0 | 0 | TODO | Write ADR | Record: move `Network` to per-instance, parameterize `external_ip`, join `ipv6_v6only` into `Network` | -| T1 | 1 | TODO | Add `network: Network` (with `ipv6_v6only`) to `HttpTracker` and `UdpTracker` | Parallel add — old fields kept. TOML block defaults safely when omitted | -| T2 | 2 | TODO | Add `tracker_external_ip` param to `handle_announcement()` | Callers pass old global value temporarily | -| T3a | 3a | TODO | Switch `on_reverse_proxy` consumers to per-instance | Approach B: per-instance services with `ReverseProxyMode` | -| T3b | 3b | TODO | Switch `ipv6_v6only` consumers to `network.ipv6_v6only` | HTTP + UDP server launchers, tests | -| T3c | 3c | TODO | Switch `external_ip` consumers | All callers of `handle_announcement()` pass per-instance value | -| T4 | 4 | TODO | Remove deprecated fields | `core.net`, flat `ipv6_v6only`, `get_ext_ip()` | -| T5 | 4 | TODO | Update default config TOML files | 6 files in `share/default/config/` | -| T6 | 4 | TODO | Update docs and doc comments | `mod.rs`, `lib.rs`, `containers.md`, `tracker-core/lib.rs`, `extractors/client_ip_sources.rs` | -| T7 | 5 | TODO | Run `linter all` and full test suite | | +| ID | Phase | Status | Task | Notes | +| --- | ----- | ------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| T0 | 0 | TODO | Write ADR | Record: move `Network` to per-instance, parameterize `external_ip`, join `ipv6_v6only` into `Network` | +| T1 | 1 | TODO | Define v3 `network: Network` (with `ipv6_v6only`) in `HttpTracker` and `UdpTracker` | Removed v2 fields are not accepted in v3; TOML block defaults safely when omitted | +| T2 | 2 | TODO | Add `tracker_external_ip` param to `handle_announcement()` | Callers pass the instance's `network.external_ip` | +| T3a | 3a | TODO | Switch `on_reverse_proxy` consumers to per-instance | Approach B: per-instance services with `ReverseProxyMode` | +| T3b | 3b | TODO | Switch `ipv6_v6only` consumers to `network.ipv6_v6only` | HTTP + UDP server launchers, tests | +| T3c | 3c | TODO | Switch `external_ip` consumers | All callers of `handle_announcement()` pass per-instance value | +| T4 | 4 | TODO | Remove deprecated fields | `core.net`, flat `ipv6_v6only`, `get_ext_ip()` | +| T5 | 4 | TODO | Update v3 documentation and doc comments | V3 configuration module and relevant crate docs | +| T7 | 5 | TODO | Run `linter all` and full test suite | | ## Progress Tracking @@ -491,12 +500,12 @@ This is the largest phase, split into sub-tasks (each committed and CI-verified - [ ] Spec drafted in `docs/issues/open/` - [ ] Spec reviewed and approved by user/maintainer - [ ] Phase 0: ADR created -- [ ] Phase 1: `network: Network` added to `HttpTracker` and `UdpTracker` (parallel with old fields) +- [ ] Phase 1: v3 `network: Network` replaces `core.net` and flat `ipv6_v6only` - [ ] Phase 2: `handle_announcement()` accepts `tracker_external_ip` param - [ ] Phase 3a: `on_reverse_proxy` consumers switched to per-instance - [ ] Phase 3b: `ipv6_v6only` consumers switched to `network.ipv6_v6only` - [ ] Phase 3c: `external_ip` consumers switched to per-instance -- [ ] Phase 4: Deprecated fields removed (`core.net`, flat `ipv6_v6only`) +- [ ] Phase 4: V3 schema boundary complete (`core.net`, flat `ipv6_v6only`, `get_ext_ip()` removed) - [ ] Phase 5: Final verification completed (`linter all`, full test suite) - [ ] Manual verification scenarios executed and recorded (status + evidence) - [ ] Acceptance criteria reviewed after implementation and updated with evidence @@ -516,6 +525,7 @@ Append one line per meaningful update. - 2026-06-23 18:30 UTC - Copilot - Completed deep review against ADRs 20260617093046, 20260620000000, 20260527175600 and issues #1417, #1671. Added compatibility table and migration note. - 2026-07-14 00:00 UTC - josecelano - Resolved #1417 relationship: `public_url` is in this EPIC (not future), stays flat (not inside `Network`). Replaced "Future Extensions" section with "Related Issue: #1417" section. Updated config types to show `public_url` as `‡` field. Added versioning note (app 4.0.0, config schema 3.0.0). - 2026-07-21 00:00 UTC - josecelano - Confirmed `network` as the per-instance field name, aligned with the `Network` type. Confirmed the TOML `[*.network]` 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 the schema compatibility boundary: v3 accepts only per-instance `network` fields and has no fallback or precedence for removed v2 fields. Application migration to v3 remains subissue #1980. ## Acceptance Criteria @@ -526,7 +536,8 @@ Append one line per meaningful update. - [ ] AC5: `AnnounceHandler::handle_announcement()` accepts `tracker_external_ip` per-call instead of reading from global config - [ ] AC6: Two HTTP trackers with different `on_reverse_proxy` settings behave independently: - Tracker A (`on_reverse_proxy = true`) reads `X-Forwarded-For` headers - Tracker B (`on_reverse_proxy = false` or unset) reads connection info IP - [ ] AC7: Example `http_only_public_tracker.rs` builds with the new `HttpTracker.network.on_reverse_proxy` field -- [ ] AC8: All default config files and docs use the new format +- [ ] AC8: V3 configuration documentation uses the new format; active application default configuration migration is deferred to #1980 +- [ ] AC9: Schema v3 rejects `[core.net]` and flat tracker `ipv6_v6only` fields; it does not define old-versus-new precedence - [ ] `linter all` exits with code `0` - [ ] Relevant tests pass - [ ] Manual verification scenarios are executed and documented (status + evidence) diff --git a/docs/issues/open/1978-configuration-overhaul-epic.md b/docs/issues/open/1978-configuration-overhaul-epic.md index b096b1d95..6130e8d66 100644 --- a/docs/issues/open/1978-configuration-overhaul-epic.md +++ b/docs/issues/open/1978-configuration-overhaul-epic.md @@ -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. @@ -205,6 +205,9 @@ For each subissue implementation in this EPIC, the default completion policy is: - 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 From 91262f2f8d55d58d4d19cbadbb50f7a720911b03 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 21 Jul 2026 11:43:23 +0100 Subject: [PATCH 3/7] feat(configuration): move v3 network settings per tracker --- ...work_configuration_per_tracker_instance.md | 74 ++++++++++ docs/adrs/index.md | 1 + ...r-http-tracker-on-reverse-proxy-setting.md | 41 +++--- packages/configuration/src/v3_0_0/core.rs | 11 +- .../configuration/src/v3_0_0/http_tracker.rs | 22 ++- packages/configuration/src/v3_0_0/mod.rs | 135 +++++++++++++----- packages/configuration/src/v3_0_0/network.rs | 22 +++ .../configuration/src/v3_0_0/udp_tracker.rs | 29 ++-- 8 files changed, 242 insertions(+), 93 deletions(-) create mode 100644 docs/adrs/20260721000000_make_network_configuration_per_tracker_instance.md diff --git a/docs/adrs/20260721000000_make_network_configuration_per_tracker_instance.md b/docs/adrs/20260721000000_make_network_configuration_per_tracker_instance.md new file mode 100644 index 000000000..3b9e29f9b --- /dev/null +++ b/docs/adrs/20260721000000_make_network_configuration_per_tracker_instance.md @@ -0,0 +1,74 @@ +--- +semantic-links: + related-artifacts: + - docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md + - 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 +--- + +# 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 + +## References + +- Issue [#1640](https://github.com/torrust/torrust-tracker/issues/1640) +- EPIC [#1978](https://github.com/torrust/torrust-tracker/issues/1978) +- [Reject wildcard IPs as invalid `external_ip` values](20260617093046_reject_wildcard_external_ip.md) +- [Add `ipv6_v6only` config option for separate sockets](20260620000000_add_ipv6_v6only_config_option.md) diff --git a/docs/adrs/index.md b/docs/adrs/index.md index 9723c324a..50e434321 100644 --- a/docs/adrs/index.md +++ b/docs/adrs/index.md @@ -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 diff --git a/docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md b/docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md index 4182b597d..77e39013f 100644 --- a/docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md +++ b/docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md @@ -481,17 +481,17 @@ This is the largest phase, split into sub-tasks (each committed and CI-verified **Chosen approach**: **Approach B** (per-instance services with `reverse_proxy_mode` field) for `on_reverse_proxy` threading. -| ID | Phase | Status | Task | Notes | -| --- | ----- | ------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -| T0 | 0 | TODO | Write ADR | Record: move `Network` to per-instance, parameterize `external_ip`, join `ipv6_v6only` into `Network` | -| T1 | 1 | TODO | Define v3 `network: Network` (with `ipv6_v6only`) in `HttpTracker` and `UdpTracker` | Removed v2 fields are not accepted in v3; TOML block defaults safely when omitted | -| T2 | 2 | TODO | Add `tracker_external_ip` param to `handle_announcement()` | Callers pass the instance's `network.external_ip` | -| T3a | 3a | TODO | Switch `on_reverse_proxy` consumers to per-instance | Approach B: per-instance services with `ReverseProxyMode` | -| T3b | 3b | TODO | Switch `ipv6_v6only` consumers to `network.ipv6_v6only` | HTTP + UDP server launchers, tests | -| T3c | 3c | TODO | Switch `external_ip` consumers | All callers of `handle_announcement()` pass per-instance value | -| T4 | 4 | TODO | Remove deprecated fields | `core.net`, flat `ipv6_v6only`, `get_ext_ip()` | -| T5 | 4 | TODO | Update v3 documentation and doc comments | V3 configuration module and relevant crate docs | -| T7 | 5 | TODO | Run `linter all` and full test suite | | +| ID | Phase | Status | Task | Notes | +| --- | ----- | -------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| T0 | 0 | DONE | Write ADR | `20260721000000_make_network_configuration_per_tracker_instance.md` | +| T1 | 1 | DONE | Define v3 `network: Network` (with `ipv6_v6only`) in `HttpTracker` and `UdpTracker` | Removed v2 fields are rejected in v3; TOML block defaults safely when omitted | +| T2 | 2 | DEFERRED | Add `tracker_external_ip` param to `handle_announcement()` | Requires active runtime consumers to migrate to v3 in #1980 | +| T3a | 3a | DEFERRED | Switch `on_reverse_proxy` consumers to per-instance | Requires active runtime consumers to migrate to v3 in #1980 | +| T3b | 3b | DEFERRED | Switch `ipv6_v6only` consumers to `network.ipv6_v6only` | Requires active runtime consumers to migrate to v3 in #1980 | +| T3c | 3c | DEFERRED | Switch `external_ip` consumers | Requires active runtime consumers to migrate to v3 in #1980 | +| T4 | 4 | DONE | Remove deprecated fields from v3 | Removed `core.net`, flat `ipv6_v6only`, and `get_ext_ip()` | +| T5 | 4 | DONE | Update v3 documentation and doc comments | V3 configuration module, ADR, and issue specification | +| T7 | 5 | PARTIAL | Run `linter all` and full test suite | `linter all` and `cargo test -p torrust-tracker-configuration` pass; full suite deferred to #1980 | ## Progress Tracking @@ -499,13 +499,13 @@ This is the largest phase, split into sub-tasks (each committed and CI-verified - [ ] Spec drafted in `docs/issues/open/` - [ ] Spec reviewed and approved by user/maintainer -- [ ] Phase 0: ADR created -- [ ] Phase 1: v3 `network: Network` replaces `core.net` and flat `ipv6_v6only` +- [x] Phase 0: ADR created +- [x] Phase 1: v3 `network: Network` replaces `core.net` and flat `ipv6_v6only` - [ ] Phase 2: `handle_announcement()` accepts `tracker_external_ip` param - [ ] Phase 3a: `on_reverse_proxy` consumers switched to per-instance - [ ] Phase 3b: `ipv6_v6only` consumers switched to `network.ipv6_v6only` - [ ] Phase 3c: `external_ip` consumers switched to per-instance -- [ ] Phase 4: V3 schema boundary complete (`core.net`, flat `ipv6_v6only`, `get_ext_ip()` removed) +- [x] Phase 4: V3 schema boundary complete (`core.net`, flat `ipv6_v6only`, `get_ext_ip()` removed) - [ ] Phase 5: Final verification completed (`linter all`, full test suite) - [ ] Manual verification scenarios executed and recorded (status + evidence) - [ ] Acceptance criteria reviewed after implementation and updated with evidence @@ -526,18 +526,19 @@ Append one line per meaningful update. - 2026-07-14 00:00 UTC - josecelano - Resolved #1417 relationship: `public_url` is in this EPIC (not future), stays flat (not inside `Network`). Replaced "Future Extensions" section with "Related Issue: #1417" section. Updated config types to show `public_url` as `‡` field. Added versioning note (app 4.0.0, config schema 3.0.0). - 2026-07-21 00:00 UTC - josecelano - Confirmed `network` as the per-instance field name, aligned with the `Network` type. Confirmed the TOML `[*.network]` 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 the schema compatibility boundary: v3 accepts only per-instance `network` fields and has no fallback or precedence for removed v2 fields. Application migration to v3 remains subissue #1980. +- 2026-07-21 00:00 UTC - agent - Implemented the v3 schema slice: per-tracker `network` defaults, removed v3 global and flat fields, strict old-layout rejection tests, and ADR. Active runtime consumers remain on v2 and are deferred to #1980. ## Acceptance Criteria -- [ ] AC1: `on_reverse_proxy` is removed from `[core.net]` and placed per-instance in `HttpTracker.network.on_reverse_proxy` (and `UdpTracker.network.on_reverse_proxy` for future UDP proxy use) -- [ ] AC2: `external_ip` is removed from `[core.net]` and placed per-instance in `HttpTracker.network.external_ip` and `UdpTracker.network.external_ip` -- [ ] AC3: `ipv6_v6only` is moved from flat `HttpTracker.ipv6_v6only` and `UdpTracker.ipv6_v6only` into `HttpTracker.network` / `UdpTracker.network` -- [ ] AC4: `Core.net` (the `Network` struct) is removed from `Core` +- [x] AC1: `on_reverse_proxy` is removed from `[core.net]` and placed per-instance in `HttpTracker.network.on_reverse_proxy` (and `UdpTracker.network.on_reverse_proxy` for future UDP proxy use) +- [x] AC2: `external_ip` is removed from `[core.net]` and placed per-instance in `HttpTracker.network.external_ip` and `UdpTracker.network.external_ip` +- [x] AC3: `ipv6_v6only` is moved from flat `HttpTracker.ipv6_v6only` and `UdpTracker.ipv6_v6only` into `HttpTracker.network` / `UdpTracker.network` +- [x] AC4: `Core.net` (the `Network` struct) is removed from `Core` - [ ] AC5: `AnnounceHandler::handle_announcement()` accepts `tracker_external_ip` per-call instead of reading from global config - [ ] AC6: Two HTTP trackers with different `on_reverse_proxy` settings behave independently: - Tracker A (`on_reverse_proxy = true`) reads `X-Forwarded-For` headers - Tracker B (`on_reverse_proxy = false` or unset) reads connection info IP - [ ] AC7: Example `http_only_public_tracker.rs` builds with the new `HttpTracker.network.on_reverse_proxy` field -- [ ] AC8: V3 configuration documentation uses the new format; active application default configuration migration is deferred to #1980 -- [ ] AC9: Schema v3 rejects `[core.net]` and flat tracker `ipv6_v6only` fields; it does not define old-versus-new precedence +- [x] AC8: V3 configuration documentation uses the new format; active application default configuration migration is deferred to #1980 +- [x] AC9: Schema v3 rejects `[core.net]` and flat tracker `ipv6_v6only` fields; it does not define old-versus-new precedence - [ ] `linter all` exits with code `0` - [ ] Relevant tests pass - [ ] Manual verification scenarios are executed and documented (status + evidence) diff --git a/packages/configuration/src/v3_0_0/core.rs b/packages/configuration/src/v3_0_0/core.rs index a87ce6ac4..eb7fa38f0 100644 --- a/packages/configuration/src/v3_0_0/core.rs +++ b/packages/configuration/src/v3_0_0/core.rs @@ -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")] @@ -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, @@ -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(), @@ -84,10 +79,6 @@ impl Core { false } - fn default_network() -> Network { - Network::default() - } - fn default_private() -> bool { false } diff --git a/packages/configuration/src/v3_0_0/http_tracker.rs b/packages/configuration/src/v3_0_0/http_tracker.rs index 5061e1f7b..574ac6737 100644 --- a/packages/configuration/src/v3_0_0/http_tracker.rs +++ b/packages/configuration/src/v3_0_0/http_tracker.rs @@ -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 @@ -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:`) 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 { @@ -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(), } } } @@ -61,8 +55,8 @@ impl HttpTracker { false } - fn default_ipv6_v6only() -> bool { - false + fn default_network() -> Network { + Network::default() } } diff --git a/packages/configuration/src/v3_0_0/mod.rs b/packages/configuration/src/v3_0_0/mod.rs index 6d2f7f632..affc065f6 100644 --- a/packages/configuration/src/v3_0_0/mod.rs +++ b/packages/configuration/src/v3_0_0/mod.rs @@ -79,8 +79,9 @@ //! //! Alternatively, you could setup a reverse proxy like Nginx or Apache to //! handle the SSL/TLS part and forward the requests to the tracker. If you do -//! that, you should set [`on_reverse_proxy`](crate::v3_0_0::network::Network::on_reverse_proxy) -//! to `true` in the configuration file. It's out of scope for this +//! that, you should set +//! [`http_trackers.network.on_reverse_proxy`](crate::v3_0_0::network::Network::on_reverse_proxy) +//! to `true` for that tracker in the configuration file. It's out of scope for this //! documentation to explain in detail how to setup a reverse proxy, but the //! configuration file should be something like this: //! @@ -214,9 +215,6 @@ //! driver = "sqlite3" //! path = "./storage/tracker/lib/database/sqlite3.db" //! -//! [core.net] -//! on_reverse_proxy = false -//! //! [core.tracker_policy] //! max_peer_timeout = 900 //! persistent_torrent_completed_stat = false @@ -241,7 +239,6 @@ pub mod tracker_api; pub mod udp_tracker; use std::fs; -use std::net::IpAddr; use figment::Figment; use figment::providers::{Env, Format, Serialized, Toml}; @@ -309,13 +306,6 @@ impl Default for Configuration { } impl Configuration { - /// Returns the tracker public IP address id defined in the configuration, - /// and `None` otherwise. - #[must_use] - pub fn get_ext_ip(&self) -> Option { - self.core.net.external_ip.map(Into::into) - } - /// Saves the default configuration at the given path. /// /// # Errors @@ -452,7 +442,9 @@ mod tests { use crate::Info; use crate::v3_0_0::Configuration; + use crate::v3_0_0::http_tracker::HttpTracker; use crate::v3_0_0::network::ExternalIp; + use crate::v3_0_0::udp_tracker::UdpTracker; #[cfg(test)] fn default_config_toml() -> String { @@ -479,9 +471,6 @@ mod tests { driver = "sqlite3" path = "./storage/tracker/lib/database/sqlite3.db" - [core.net] - on_reverse_proxy = false - [core.tracker_policy] max_peer_timeout = 900 persistent_torrent_completed_stat = false @@ -507,9 +496,8 @@ mod tests { #[test] fn configuration_should_not_contain_an_external_ip_by_default() { - let configuration = Configuration::default(); - - assert_eq!(configuration.core.net.external_ip, None); + assert_eq!(HttpTracker::default().network.external_ip, None); + assert_eq!(UdpTracker::default().network.external_ip, None); } #[test] @@ -728,7 +716,7 @@ mod tests { #[allow(clippy::result_large_err)] #[test] - fn should_deserialize_valid_external_ip_from_toml() { + fn it_should_deserialize_network_settings_from_a_http_tracker_network_block() { Jail::expect_with(|jail| { jail.create_file( "tracker.toml", @@ -743,9 +731,13 @@ mod tests { listed = false private = false - [core.net] + [[http_trackers]] + bind_address = "127.0.0.1:7070" + + [http_trackers.network] external_ip = "203.0.113.5" - on_reverse_proxy = false + on_reverse_proxy = true + ipv6_v6only = true "#, )?; @@ -756,7 +748,9 @@ mod tests { let config = Configuration::load(&info).expect("Should load config"); assert_eq!( - config.core.net.external_ip, + config.http_trackers.expect("HTTP tracker should be configured")[0] + .network + .external_ip, Some(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5)).try_into().expect("valid IP")) ); @@ -766,7 +760,50 @@ mod tests { #[allow(clippy::result_large_err)] #[test] - fn should_reject_unspecified_ipv4_external_ip_in_toml() { + fn it_should_use_safe_network_defaults_when_the_network_block_is_omitted() { + Jail::expect_with(|jail| { + jail.create_file( + "tracker.toml", + r#" + [metadata] + schema_version = "3.0.0" + + [logging] + threshold = "info" + + [core] + listed = false + private = false + + [[http_trackers]] + bind_address = "127.0.0.1:7070" + + [[udp_trackers]] + bind_address = "127.0.0.1:6969" + "#, + )?; + + let info = Info { + config_toml: None, + config_toml_path: "tracker.toml".to_string(), + }; + + let configuration = Configuration::load(&info).expect("configuration should load"); + let http_network = &configuration.http_trackers.expect("HTTP tracker should be configured")[0].network; + let udp_network = &configuration.udp_trackers.expect("UDP tracker should be configured")[0].network; + + assert_eq!(http_network.external_ip, None); + assert!(!http_network.on_reverse_proxy); + assert!(!http_network.ipv6_v6only); + assert_eq!(udp_network, http_network); + + Ok(()) + }); + } + + #[allow(clippy::result_large_err)] + #[test] + fn it_should_reject_the_removed_core_network_layout() { Jail::expect_with(|jail| { jail.create_file( "tracker.toml", @@ -782,8 +819,7 @@ mod tests { private = false [core.net] - external_ip = "0.0.0.0" - on_reverse_proxy = false + external_ip = "203.0.113.5" "#, )?; @@ -793,7 +829,7 @@ mod tests { }; let result = Configuration::load(&info); - assert!(result.is_err()); + assert!(result.is_err(), "v3 must reject the removed core.net layout"); Ok(()) }); @@ -801,7 +837,7 @@ mod tests { #[allow(clippy::result_large_err)] #[test] - fn should_reject_unspecified_ipv6_external_ip_in_toml() { + fn it_should_reject_the_removed_flat_tracker_ipv6_v6only_field() { Jail::expect_with(|jail| { jail.create_file( "tracker.toml", @@ -816,9 +852,44 @@ mod tests { listed = false private = false - [core.net] - external_ip = "::" - on_reverse_proxy = false + [[http_trackers]] + bind_address = "127.0.0.1:7070" + ipv6_v6only = true + "#, + )?; + + let info = Info { + config_toml: None, + config_toml_path: "tracker.toml".to_string(), + }; + + let result = Configuration::load(&info); + assert!(result.is_err(), "v3 must reject the removed flat ipv6_v6only field"); + + Ok(()) + }); + } + + #[allow(clippy::result_large_err)] + #[test] + fn it_should_reject_the_removed_flat_udp_tracker_ipv6_v6only_field() { + Jail::expect_with(|jail| { + jail.create_file( + "tracker.toml", + r#" + [metadata] + schema_version = "3.0.0" + + [logging] + threshold = "info" + + [core] + listed = false + private = false + + [[udp_trackers]] + bind_address = "127.0.0.1:6969" + ipv6_v6only = true "#, )?; @@ -828,7 +899,7 @@ mod tests { }; let result = Configuration::load(&info); - assert!(result.is_err()); + assert!(result.is_err(), "v3 must reject the removed flat ipv6_v6only field"); Ok(()) }); diff --git a/packages/configuration/src/v3_0_0/network.rs b/packages/configuration/src/v3_0_0/network.rs index 75ae69a45..75652272e 100644 --- a/packages/configuration/src/v3_0_0/network.rs +++ b/packages/configuration/src/v3_0_0/network.rs @@ -1,3 +1,7 @@ +//! Per-tracker network topology configuration for schema v3. +//! +//! See `docs/adrs/20260721000000_make_network_configuration_per_tracker_instance.md`. + use std::convert::TryFrom; use std::fmt; use std::net::IpAddr; @@ -6,6 +10,7 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +#[serde(deny_unknown_fields)] pub struct Network { /// The external IP address of the tracker. If the client is using a /// loopback IP address, this IP address will be used instead. If the peer @@ -20,6 +25,18 @@ pub struct Network { /// sent from the proxy will be used to get the client's IP address. #[serde(default = "Network::default_on_reverse_proxy")] pub on_reverse_proxy: bool, + + /// Whether to set `IPV6_V6ONLY=1` on IPv6 sockets. + /// + /// When `true` (IPv6-only), the tracker must also bind an IPv4 socket + /// (for example, `0.0.0.0:`) to accept IPv4 connections. When + /// `false` (the default), the socket option is not overridden and the OS + /// default applies. + /// + /// On OpenBSD, `IPV6_V6ONLY` is always `1` and cannot be disabled; setting + /// this to `false` is a no-op. + #[serde(default = "Network::default_ipv6_v6only")] + pub ipv6_v6only: bool, } impl Default for Network { @@ -27,6 +44,7 @@ impl Default for Network { Self { external_ip: Self::default_external_ip(), on_reverse_proxy: Self::default_on_reverse_proxy(), + ipv6_v6only: Self::default_ipv6_v6only(), } } } @@ -39,6 +57,10 @@ impl Network { fn default_on_reverse_proxy() -> bool { false } + + fn default_ipv6_v6only() -> bool { + false + } } /// A validated external IP address that is guaranteed not to be a wildcard /// address (`0.0.0.0` or `::`). diff --git a/packages/configuration/src/v3_0_0/udp_tracker.rs b/packages/configuration/src/v3_0_0/udp_tracker.rs index bd8973932..aae4125ce 100644 --- a/packages/configuration/src/v3_0_0/udp_tracker.rs +++ b/packages/configuration/src/v3_0_0/udp_tracker.rs @@ -3,7 +3,10 @@ use std::time::Duration; use serde::{Deserialize, Serialize}; +use crate::v3_0_0::network::Network; + #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +#[serde(deny_unknown_fields)] pub struct UdpTracker { /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to @@ -21,22 +24,14 @@ pub struct UdpTracker { #[serde(default = "UdpTracker::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:`) 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 = "UdpTracker::default_ipv6_v6only")] - pub ipv6_v6only: bool, - /// The maximum number of connection ID errors per IP before the client is /// banned. Default is `10`. #[serde(default = "UdpTracker::default_max_connection_id_errors_per_ip")] pub max_connection_id_errors_per_ip: u32, + + /// Per-instance network topology and socket behavior. + #[serde(default = "UdpTracker::default_network")] + pub network: Network, } impl Default for UdpTracker { fn default() -> Self { @@ -44,8 +39,8 @@ impl Default for UdpTracker { bind_address: Self::default_bind_address(), cookie_lifetime: Self::default_cookie_lifetime(), tracker_usage_statistics: Self::default_tracker_usage_statistics(), - ipv6_v6only: Self::default_ipv6_v6only(), max_connection_id_errors_per_ip: Self::default_max_connection_id_errors_per_ip(), + network: Self::default_network(), } } } @@ -63,11 +58,11 @@ impl UdpTracker { false } - fn default_ipv6_v6only() -> bool { - false - } - fn default_max_connection_id_errors_per_ip() -> u32 { 10 } + + fn default_network() -> Network { + Network::default() + } } From 4cc6651c09a08a15e0cdbbf7013c22fe3af87230 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 21 Jul 2026 11:57:09 +0100 Subject: [PATCH 4/7] docs(adrs): apply semantic-link convention to network ADR --- ...e_network_configuration_per_tracker_instance.md | 14 ++++++-------- packages/configuration/src/v3_0_0/network.rs | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/adrs/20260721000000_make_network_configuration_per_tracker_instance.md b/docs/adrs/20260721000000_make_network_configuration_per_tracker_instance.md index 3b9e29f9b..3b8a62359 100644 --- a/docs/adrs/20260721000000_make_network_configuration_per_tracker_instance.md +++ b/docs/adrs/20260721000000_make_network_configuration_per_tracker_instance.md @@ -1,11 +1,16 @@ --- semantic-links: + skill-links: + - create-adr related-artifacts: - - docs/issues/open/1640-1978-per-http-tracker-on-reverse-proxy-setting.md + - 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 @@ -65,10 +70,3 @@ and socket behavior. ## Date 2026-07-21 - -## References - -- Issue [#1640](https://github.com/torrust/torrust-tracker/issues/1640) -- EPIC [#1978](https://github.com/torrust/torrust-tracker/issues/1978) -- [Reject wildcard IPs as invalid `external_ip` values](20260617093046_reject_wildcard_external_ip.md) -- [Add `ipv6_v6only` config option for separate sockets](20260620000000_add_ipv6_v6only_config_option.md) diff --git a/packages/configuration/src/v3_0_0/network.rs b/packages/configuration/src/v3_0_0/network.rs index 75652272e..ba82aabe8 100644 --- a/packages/configuration/src/v3_0_0/network.rs +++ b/packages/configuration/src/v3_0_0/network.rs @@ -1,6 +1,6 @@ //! Per-tracker network topology configuration for schema v3. //! -//! See `docs/adrs/20260721000000_make_network_configuration_per_tracker_instance.md`. +//! adr: `docs/adrs/20260721000000_make_network_configuration_per_tracker_instance.md` use std::convert::TryFrom; use std::fmt; From ec85d61a790947eb4c29b05cebaa47aa0210d4f5 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 21 Jul 2026 12:13:24 +0100 Subject: [PATCH 5/7] test(configuration): assert all three network fields in deserialization test --- packages/configuration/src/v3_0_0/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/configuration/src/v3_0_0/mod.rs b/packages/configuration/src/v3_0_0/mod.rs index affc065f6..c955b5d0c 100644 --- a/packages/configuration/src/v3_0_0/mod.rs +++ b/packages/configuration/src/v3_0_0/mod.rs @@ -747,12 +747,13 @@ mod tests { }; let config = Configuration::load(&info).expect("Should load config"); + let network = &config.http_trackers.expect("HTTP tracker should be configured")[0].network; assert_eq!( - config.http_trackers.expect("HTTP tracker should be configured")[0] - .network - .external_ip, + network.external_ip, Some(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5)).try_into().expect("valid IP")) ); + assert!(network.on_reverse_proxy, "on_reverse_proxy should be true"); + assert!(network.ipv6_v6only, "ipv6_v6only should be true"); Ok(()) }); From ce08f1d5503e6e20739b111a7589be17572206e4 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 21 Jul 2026 12:29:59 +0100 Subject: [PATCH 6/7] docs(configuration): fix grammar and misleading test name per review --- packages/configuration/src/v3_0_0/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/configuration/src/v3_0_0/mod.rs b/packages/configuration/src/v3_0_0/mod.rs index c955b5d0c..103c4377f 100644 --- a/packages/configuration/src/v3_0_0/mod.rs +++ b/packages/configuration/src/v3_0_0/mod.rs @@ -77,12 +77,12 @@ //! //! where the application stores all the persistent data. //! -//! Alternatively, you could setup a reverse proxy like Nginx or Apache to +//! Alternatively, you could set up a reverse proxy like Nginx or Apache to //! handle the SSL/TLS part and forward the requests to the tracker. If you do //! that, you should set //! [`http_trackers.network.on_reverse_proxy`](crate::v3_0_0::network::Network::on_reverse_proxy) //! to `true` for that tracker in the configuration file. It's out of scope for this -//! documentation to explain in detail how to setup a reverse proxy, but the +//! documentation to explain in detail how to set up a reverse proxy, but the //! configuration file should be something like this: //! //! For [NGINX](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/): @@ -495,7 +495,7 @@ mod tests { } #[test] - fn configuration_should_not_contain_an_external_ip_by_default() { + fn tracker_defaults_should_not_contain_an_external_ip() { assert_eq!(HttpTracker::default().network.external_ip, None); assert_eq!(UdpTracker::default().network.external_ip, None); } From f797ea7004f132f1ecd4b839f12b49b3691d34b1 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 21 Jul 2026 12:46:38 +0100 Subject: [PATCH 7/7] test(configuration): add UDP network block deserialization test --- packages/configuration/src/v3_0_0/mod.rs | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/configuration/src/v3_0_0/mod.rs b/packages/configuration/src/v3_0_0/mod.rs index 103c4377f..803ec1281 100644 --- a/packages/configuration/src/v3_0_0/mod.rs +++ b/packages/configuration/src/v3_0_0/mod.rs @@ -759,6 +759,51 @@ mod tests { }); } + #[allow(clippy::result_large_err)] + #[test] + fn it_should_deserialize_network_settings_from_a_udp_tracker_network_block() { + Jail::expect_with(|jail| { + jail.create_file( + "tracker.toml", + r#" + [metadata] + schema_version = "3.0.0" + + [logging] + threshold = "info" + + [core] + listed = false + private = false + + [[udp_trackers]] + bind_address = "127.0.0.1:6969" + + [udp_trackers.network] + external_ip = "203.0.113.5" + on_reverse_proxy = true + ipv6_v6only = true + "#, + )?; + + let info = Info { + config_toml: None, + config_toml_path: "tracker.toml".to_string(), + }; + + let config = Configuration::load(&info).expect("Should load config"); + let network = &config.udp_trackers.expect("UDP tracker should be configured")[0].network; + assert_eq!( + network.external_ip, + Some(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5)).try_into().expect("valid IP")) + ); + assert!(network.on_reverse_proxy, "on_reverse_proxy should be true"); + assert!(network.ipv6_v6only, "ipv6_v6only should be true"); + + Ok(()) + }); + } + #[allow(clippy::result_large_err)] #[test] fn it_should_use_safe_network_defaults_when_the_network_block_is_omitted() {