Skip to content

feat(udp-server): add configurable connection ID validation policy (#1136) - #2032

Merged
josecelano merged 5 commits into
torrust:developfrom
josecelano:1136-connection-id-validation-policy
Jul 28, 2026
Merged

feat(udp-server): add configurable connection ID validation policy (#1136)#2032
josecelano merged 5 commits into
torrust:developfrom
josecelano:1136-connection-id-validation-policy

Conversation

@josecelano

@josecelano josecelano commented Jul 27, 2026

Copy link
Copy Markdown
Member

Description

Implements issue #1136 — add a global connection_id_validation option to the schema v3 UDP tracker server configuration, allowing operators to disable connection ID validation for compatibility with non-compliant BEP 15 clients.

Changes

Configuration (T1–T2)

  • ConnectionIdValidationPolicy enum with strict (default) and disabled variants in v3_0_0/udp_tracker_server.rs (global setting, not per-instance — the BanService is shared across all UDP listeners)
  • 6 serialization/deserialization tests

Runtime type (T3)

  • Mirror enum in udp-core to avoid configuration crate dependency
  • validate_cookie: bool parameter on AnnounceService::handle_announce and ScrapeService::handle_scrape

Policy propagation (T4–T5)

  • Threaded from launcher through processor to handlers
  • CookieValidationContext struct to stay within clippy's too_many_arguments limit

Observability and banning (T6)

  • Disabled mode still validates the cookie for observability — emits UdpError { ConnectionCookie } event (objective fact)
  • Ban listener always counts invalid IDs; enforcement is gated in the main loop (is_banned check only when Strict)
  • ADR-20260727000000 "Events are objective facts" documents the design principle

Startup warning (T7)

  • WARN-level log at startup when policy is Disabled

Integration tests (T8)

  • 4 contract tests: connect issues valid ID, announce/scrape succeed with arbitrary ID, and ban enforcement is skipped in disabled mode

Documentation & Code Quality

  • ADR-20260727180000: Shared services across tracker instances (peer repository, ban service, event buses)
  • All Copilot review suggestions processed and resolved with documented rationale
  • 100% linter compliance (markdown, YAML, TOML, Clippy, Rustfmt, ShellCheck)

Commits

Commit Description
cea84cff Spec updates after Q&A
ca35387f Full implementation (T1-T8, ADRs, global config pivot)
9ce32b50 Fix doc comments and test assertions per Copilot review
c04d75ea Add banning semantics test for disabled-policy mode
24071657 Fix unnecessary cast (clippy)

Related

Notes

The per-instance/global configuration pivot was made after discovering that the global BanService would be polluted by per-instance policies. All Copilot suggestions were addressed with documented replies and the branch is linter-clean and test-passing.

…on policy

- Clarify that cookie-error metrics are emitted in disabled mode so
  operators can observe non-compliant clients; IP-ban counters are
  not incremented when validation is disabled
- Connect action continues to issue valid connection IDs in both modes;
  BEP 15-compliant clients are unaffected in disabled mode
- Expand startup warning to WARN level, identifying the service binding
  and the security implication
- Rewrite test strategy: disabled policy is a distinct scenario group
  (connect, announce, scrape) analogous to private/public scenarios
- Add AC12 covering metrics/connect/BEP-15 client behaviour
- Add M3 manual scenario for connect on a disabled listener; renumber
  M4/M5 accordingly
- Document feature motivation: operator flexibility for real-world
  non-compliant clients; encourage strict BEP 15 compliance
- Mark EPIC subissue 7 as IN_PROGRESS
Copilot AI review requested due to automatic review settings July 27, 2026 17:59
josecelano added a commit to josecelano/torrust-tracker that referenced this pull request Jul 27, 2026
All 12 ACs met; manual verification deferred to torrust#1980.
@josecelano josecelano self-assigned this Jul 27, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a per-listener UDP connection ID validation policy (schema v3) with a runtime propagation path that allows bypassing connection ID validation for announce/scrape (while keeping connect issuing IDs), plus an ADR documenting the event-design principle used to preserve observability.

Changes:

  • Add ConnectionIdValidationPolicy { strict, disabled } to config schema v3 and mirror it in udp-core, then thread the policy through UDP server startup → processing → handlers.
  • Update announce/scrape handling to optionally skip cookie validation, while still emitting cookie-error observations/events in disabled mode.
  • Add contract tests for disabled-policy behavior and document the “events are objective facts” principle via a new ADR and event module docs.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/bootstrap/jobs/udp_tracker.rs Passes a (currently hardcoded) Strict policy through the bootstrap UDP job until v3 config is wired in.
project-words.txt Adds unstarted for spell-checking.
packages/udp-server/tests/server/contract.rs Adds contract tests covering disabled-policy connect/announce/scrape success paths.
packages/udp-server/src/testing/environment.rs Adds connection_id_validation to the test environment plus an Unstarted alias and builder method.
packages/udp-server/src/server/states.rs Extends server startup signature to accept the policy.
packages/udp-server/src/server/spawner.rs Threads the policy through the spawner → launcher.
packages/udp-server/src/server/processor.rs Stores the policy in the processor and passes it down to packet handling.
packages/udp-server/src/server/mod.rs Updates server tests to pass Strict explicitly.
packages/udp-server/src/server/launcher.rs Adds startup warning for disabled mode and gates ban enforcement on policy.
packages/udp-server/src/handlers/scrape.rs Adds policy-aware cookie validation behavior for scrape, including disabled-mode observation emission.
packages/udp-server/src/handlers/mod.rs Introduces CookieValidationContext to carry valid range + policy through handler calls.
packages/udp-server/src/handlers/announce.rs Adds policy-aware cookie validation behavior for announce, including disabled-mode observation emission.
packages/udp-server/src/event.rs Adds module-level documentation linking event design to the new ADR.
packages/udp-core/src/services/scrape.rs Adds validate_cookie: bool to allow callers to bypass cookie validation.
packages/udp-core/src/services/announce.rs Adds validate_cookie: bool to allow callers to bypass cookie validation.
packages/udp-core/src/lib.rs Adds ConnectionIdValidationPolicy enum mirrored from configuration v3.
packages/udp-core/src/event.rs Adds module-level documentation referencing the ADR principle.
packages/swarm-coordination-registry/src/event.rs Adds module-level documentation referencing the ADR principle.
packages/http-core/src/event.rs Adds module-level documentation referencing the ADR principle.
packages/configuration/src/v3_0_0/udp_tracker.rs Adds the v3 schema enum + per-listener field with serde defaults and round-trip tests.
docs/issues/open/1980-1978-configuration-overhaul-final-cleanup.md Adds a deferred cleanup task to remove the test-environment hardcoded policy after v3 migration.
docs/issues/open/1978-configuration-overhaul-epic.md Updates epic progress/status entries for this subissue.
docs/issues/open/1136-1978-configurable-udp-connection-id-validation-policy.md Updates the implementation spec, tasks, and verification notes for the new policy.
docs/adrs/index.md Registers the new ADR in the index table.
docs/adrs/20260727000000_events_are_objective_facts.md New ADR documenting the “events are objective facts” design contract.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/udp-server/src/handlers/announce.rs
Comment thread packages/udp-server/src/handlers/scrape.rs
Comment thread packages/udp-server/src/server/launcher.rs
Comment thread packages/udp-server/tests/server/contract.rs
Comment thread packages/udp-server/src/event.rs
Copilot AI review requested due to automatic review settings July 27, 2026 18:06
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.59459% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.46%. Comparing base (314a9e6) to head (2407165).
⚠️ Report is 2 commits behind head on develop.

Files with missing lines Patch % Lines
packages/udp-server/src/testing/environment.rs 66.66% 4 Missing ⚠️
packages/udp-core/src/services/scrape.rs 75.00% 0 Missing and 1 partial ⚠️
packages/udp-server/src/handlers/scrape.rs 88.88% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #2032      +/-   ##
===========================================
- Coverage    81.46%   81.46%   -0.01%     
===========================================
  Files          344      344              
  Lines        24527    24601      +74     
  Branches     24527    24601      +74     
===========================================
+ Hits         19981    20041      +60     
- Misses        4246     4252       +6     
- Partials       300      308       +8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (2)

packages/udp-server/src/server/launcher.rs:218

  • Disabled mode currently skips ban enforcement (is_banned), but cookie errors are still emitted as Event::UdpError { error: ConnectionCookie(..) } from the handlers. Because the banning event handler increments BanService counters on every ConnectionCookie error, a disabled listener will still accumulate ban state and increase the UDP_TRACKER_SERVER_IPS_BANNED_TOTAL gauge, which contradicts the docs/spec text that IP-ban counters are not incremented in disabled mode and can cause unnecessary memory growth.
                // When connection ID validation is disabled, the tracker is
                // intentionally accepting requests with invalid or arbitrary
                // connection IDs. Enforcing IP bans in that mode is
                // contradictory — the banning listener still counts invalid
                // cookies for observability, but the ban is not acted upon.
                let ban_enforcement_active = connection_id_validation == ConnectionIdValidationPolicy::Strict;

                if ban_enforcement_active && udp_tracker_core_container.ban_service.read().await.is_banned(&req.from.ip()) {

packages/udp-server/tests/server/contract.rs:556

  • Same issue as the announce test: asserting only response.is_ok() doesn’t prove the scrape succeeded (an Error response is still Ok(Response)). The test should assert the decoded response is a Scrape response (and not Error).
        client.send(scrape_request.into()).await.unwrap();

        let response = client.receive().await;

        assert!(
            response.is_ok(),
            "scrape should succeed even with an invalid connection ID when validation is disabled"
        );

Comment thread packages/udp-core/src/lib.rs
Comment thread packages/configuration/src/v3_0_0/udp_tracker.rs Outdated
Comment thread packages/udp-server/tests/server/contract.rs Outdated
Comment thread packages/udp-server/tests/server/contract.rs
…orrust#1136)

Implement issue torrust#1136 which adds a `connection_id_validation` option
to the schema v3 UDP tracker server configuration.

**Configuration (T1–T2)**
- Add `ConnectionIdValidationPolicy { Strict, Disabled }` enum to
  `packages/configuration/src/v3_0_0/udp_tracker_server.rs` with
  `#[serde(rename_all = "kebab-case")]` and `Strict` as the default.
- Add the `connection_id_validation` field to `UdpTrackerServer` (global
  setting, not per-instance — the BanService is shared across all listeners).
- Add 6 serialization/deserialization tests.

**Runtime type (T3)**
- Mirror the enum in `packages/udp-core/src/lib.rs` so the service layer
  does not need to depend on the configuration crate.
- Add `validate_cookie: bool` parameter to `AnnounceService::handle_announce`
  and `ScrapeService::handle_scrape`; when `false` the cookie check is skipped.

**Policy propagation (T4–T5)**
- Thread the policy from the launcher through the processor to handlers.
- Bootstrap job hardcodes `Strict` (v2 config compat).
- Test environment defaults to `Strict` with builder override.
- `CookieValidationContext` struct groups `valid_range` + `connection_id_validation`
  to stay within clippy's `too_many_arguments` limit.

**Observability and banning (T6)**
- Disabled mode still validates the cookie and emits `UdpError { ConnectionCookie }`
  (objective fact) so the ban listener always counts invalid IDs.
- Ban enforcement is gated in the main loop: `is_banned` is checked only
  when `connection_id_validation == Strict`.

**Startup warning (T7)**
- `WARN`-level log at startup when the policy is `Disabled`.

**Integration tests (T8)**
- 3 contract tests: connect still issues valid ID, announce/scrape succeed
  with arbitrary ID in disabled mode.

**ADR-20260727000000** — Events are objective facts (design principle).
**ADR-20260727180000** — Shared services across tracker instances.
Copilot AI review requested due to automatic review settings July 28, 2026 09:47
@josecelano
josecelano force-pushed the 1136-connection-id-validation-policy branch from 8a46ff4 to ca35387 Compare July 28, 2026 09:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (5)

packages/udp-core/src/lib.rs:40

  • The Disabled policy docs say IP-ban counters are not incremented, but the udp-server banning listener still increments the shared BanService counter on Event::UdpError { ConnectionCookie } even when validation is disabled; only enforcement is skipped in the main loop. Please update the docs to match the actual behavior (or adjust the implementation if the intent is truly "no counter increments").
    /// Skip connection ID validation for announce and scrape requests.
    /// Cookie-error metrics are still emitted; IP-ban counters are not incremented.
    Disabled,

packages/configuration/src/v3_0_0/udp_tracker_server.rs:58

  • This field doc claims that in disabled mode IP-ban counters are not incremented, but the current server-side banning listener still increments the ban counter (it just doesn’t enforce bans when validation is disabled). Update the comment to reflect enforcement gating rather than counter suppression.
    /// `strict` (default) preserves all existing validation.
    /// `disabled` skips validation so non-compliant clients that reuse
    /// expired or arbitrary connection IDs can still connect. Cookie-error
    /// metrics are still emitted in disabled mode; IP-ban counters are not
    /// incremented.

packages/configuration/src/v3_0_0/udp_tracker_server.rs:37

  • ConnectionIdValidationPolicy::Disabled is documented as not incrementing IP-ban counters, but the current udp-server implementation still increments the shared ban counter on UdpError { ConnectionCookie } and only skips enforcement. The docstring should be updated to avoid misleading operators.

This issue also appears on line 54 of the same file.

    /// Skip connection ID validation for announce and scrape requests.
    /// The connect action continues to issue valid connection IDs.
    /// Cookie-error metrics are still emitted; IP-ban counters are not
    /// incremented.
    Disabled,

docs/issues/open/1136-1978-configurable-udp-connection-id-validation-policy.md:359

  • AC2 evidence references a connection_id_validation field on v3_0_0::UdpTracker, but the implementation and earlier design decision in this spec place the field on v3_0_0::udp_tracker_server::UdpTrackerServer. The evidence line should be corrected to match the code.
| AC2   | DONE                   | Field `connection_id_validation` on `v3_0_0::UdpTracker` struct                                                                         |

docs/issues/open/1136-1978-configurable-udp-connection-id-validation-policy.md:366

  • AC9 evidence currently describes per-processor/per-listener isolation, but this PR’s design pivot (and the config placement on UdpTrackerServer) makes the policy global due to the shared BanService. Update the evidence to reflect that mixed per-listener policies are intentionally unsupported and enforcement is gated when disabled.
| AC9   | DONE                   | Policy is per-processor-instance; tests pass per-listener isolation; M4 scenario verified inline                                        |

…t#1136)

Update Disabled variant docs to say 'IP-ban enforcement is skipped' instead of 'IP-ban counters are not incremented' — the ban counter still counts invalid IDs for observability, only enforcement is gated.\n\nFix announce and scrape contract tests to decode and assert the response variant instead of just checking response.is_ok().
Copilot AI review requested due to automatic review settings July 28, 2026 10:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (7)

docs/issues/open/1136-1978-configurable-udp-connection-id-validation-policy.md:367

  • AC9 evidence says “per-listener isolation”, but the spec text above now states the setting is global (BanService is shared) and strict/disabled cannot coexist on different listeners. The evidence should be updated to reflect the intended global/uniform policy (or the spec should re-justify mixed-policy support).
| AC8   | DONE                   | `Launcher::run_with_graceful_shutdown` emits `WARN` log on `Disabled`; `Unstarted` type alias                                           |
| AC9   | DONE                   | Policy is per-processor-instance; tests pass per-listener isolation; M4 scenario verified inline                                        |
| AC10  | DONE                   | Only `v3_0_0/` touched; bootstrap hardcodes `Strict` for v2 compat                                                                      |

packages/udp-core/src/lib.rs:41

  • udp-core’s ConnectionIdValidationPolicy::Disabled doc comment claims cookie-error metrics and ban-counter semantics, but udp-core does not emit metrics or manage banning; it only provides the policy/type used by callers. This doc currently over-promises behavior that’s implemented in udp-server (handlers/launcher/banning listener).
    /// Skip connection ID validation for announce and scrape requests.
    /// Cookie-error metrics are still emitted and the ban counter still counts
    /// invalid IDs for observability, but IP-ban enforcement is skipped.
    Disabled,

packages/udp-server/src/handlers/announce.rs:65

  • In Disabled mode, this handler still emits Event::UdpError { error: ConnectionCookie(..) } when check() fails. The banning listener increments the ban counter on every UdpError::ConnectionCookie event (packages/udp-server/src/banning/event/handler.rs:19-30), so disabled mode still increments the IP-ban counter/metrics—contradicting issue #1136’s acceptance criteria (“Disabled mode produces no connection-cookie errors, connection-ID metric increments, or IP-ban increments for the bypassed check”).
        // When validation is disabled, still perform the cookie check so the
        // banning listener can count invalid IDs for observability. Emit the
        // same UdpError event (objective fact: a cookie error occurred), but
        // do not return an error — the request is allowed to proceed.
        // Ban enforcement is skipped in the main loop when validation is
        // disabled (see launcher.rs), so the client is never actually blocked.
        let validate_cookie = match cookie_validation.connection_id_validation {
            ConnectionIdValidationPolicy::Strict => true,
            ConnectionIdValidationPolicy::Disabled => {

packages/udp-server/src/handlers/scrape.rs:58

  • Same issue as announce: in Disabled mode this handler emits Event::UdpError { ConnectionCookie(..) } on failed check(), which drives the banning listener counter/metrics (packages/udp-server/src/banning/event/handler.rs:19-30). That means disabled mode still increments the IP-ban counter, conflicting with issue #1136’s stated acceptance criteria for disabled mode.
        let validate_cookie = match cookie_validation.connection_id_validation {
            ConnectionIdValidationPolicy::Strict => true,
            ConnectionIdValidationPolicy::Disabled => {
                if let Err(cookie_error) = check(
                    &request.connection_id,
                    gen_remote_fingerprint(&client_socket_addr),
                    cookie_validation.valid_range.clone(),
                ) {
                    tracing::debug!(

docs/issues/open/1136-1978-configurable-udp-connection-id-validation-policy.md:360

  • The AC2 evidence line references v3_0_0::UdpTracker, but this PR moved connection_id_validation onto v3_0_0::udp_tracker_server::UdpTrackerServer (see packages/configuration/src/v3_0_0/udp_tracker_server.rs:44-68). This looks like a leftover from the earlier per-listener design and makes the verification table misleading.

This issue also appears on line 365 of the same file.

| AC1   | DONE                   | Enum `ConnectionIdValidationPolicy` with `strict`/`disabled` serde values in `v3_0_0/udp_tracker_server.rs`                             |
| AC2   | DONE                   | Field `connection_id_validation` on `v3_0_0::UdpTracker` struct                                                                         |
| AC3   | DONE                   | `#[serde(default)]` + test `it_should_default_connection_id_validation_to_strict`                                                       |

packages/udp-server/src/testing/environment.rs:69

  • with_connection_id_validation is used by the new contract tests (packages/udp-server/tests/server/contract.rs), so #[allow(dead_code)] is unnecessary here and acts as an avoidable lint suppression.
    /// Sets the connection ID validation policy for this test environment.
    #[must_use]
    #[allow(dead_code)]
    pub fn with_connection_id_validation(mut self, policy: ConnectionIdValidationPolicy) -> Self {
        self.connection_id_validation = policy;

packages/configuration/src/v3_0_0/udp_tracker_server.rs:67

  • This PR makes connection_id_validation a global policy on UdpTrackerServer, but issue #1136 (as linked in the PR metadata) explicitly calls for a per-listener connection_id_validation so strict and disabled listeners can run simultaneously and independently. If the global-only design is intentional (per ADR-20260727180000), the GitHub issue’s Design/Acceptance Criteria likely needs updating before closing it.
    /// Connection ID validation policy for all UDP tracker listeners.
    ///
    /// This is a global setting because the ban service is shared across all
    /// UDP instances. A per-instance policy would allow one listener's traffic
    /// to pollute the shared ban counter that another listener enforces against.
    ///
    /// `strict` (default) preserves all existing validation.
    /// `disabled` skips validation so non-compliant clients that reuse
    /// expired or arbitrary connection IDs can still connect. Cookie-error
    /// metrics are still emitted and the ban counter still counts invalid
    /// IDs for observability, but IP-ban enforcement is skipped.
    ///
    /// **Security**: only use `disabled` on deployments where all listeners are
    /// isolated through external network controls. Always prefer `strict` in
    /// public deployments.
    ///
    /// See ADR-20260727180000 for the rationale behind shared services.
    #[serde(default)]
    pub connection_id_validation: ConnectionIdValidationPolicy,

@josecelano josecelano left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Response to Copilot review suggestions:

Addressed in commit 9ce32b5:

  1. Doc comments on Disabled variant — Updated packages/udp-core/src/lib.rs and packages/configuration/src/v3_0_0/udp_tracker_server.rs to say "IP-ban enforcement is skipped, but the ban counter still counts invalid IDs for observability" instead of "IP-ban counters are not incremented". This matches the actual behaviour: the banning listener always increments the counter, only the is_banned check is gated on Strict.

  2. Test assertions — Fixed announce_succeeds_with_an_arbitrary_connection_id and scrape_succeeds_with_an_arbitrary_connection_id to decode the response and assert the correct variant (is_ipv4_announce_response / is_scrape_response) instead of just response.is_ok().

  3. Launcher comment — The comment at launcher.rs:218 is intentionally correct: "the banning listener still counts invalid cookies for observability, but the ban is not acted upon". This matches the design documented in ADR-20260727000000.

…rrust#1136)

Added test `many_invalid_connection_ids_do_not_cause_ban_in_disabled_mode` to verify that ban enforcement is skipped in disabled mode. Requests that exceed the ban threshold still succeed without timeout when validation is disabled. This addresses Copilot's concern about banning semantics in disabled-policy mode.
Copilot AI review requested due to automatic review settings July 28, 2026 12:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (5)

packages/udp-server/src/testing/environment.rs:81

  • start is a pub method and is called from integration/contract tests, so #[allow(dead_code)] is unnecessary and makes it easier for unused APIs to slip in unnoticed.
    /// Starts the test environment and return a running environment.
    ///
    /// # Panics
    ///
    /// Will panic if it cannot start the server.
    #[allow(dead_code)]
    pub async fn start(self) -> Environment<Running> {
        let cookie_lifetime = self.container.udp_tracker_core_container.udp_tracker_config.cookie_lifetime;

packages/udp-server/src/server/launcher.rs:218

  • In Disabled mode, ban enforcement is skipped here, but the ban state still grows because handlers continue emitting Event::UdpError { error: ConnectionCookie(..) } and the banning listener unconditionally calls ban_service.increase_counter(...) (see packages/udp-server/src/banning/event/handler.rs:19-30). Under invalid-ID traffic this can fill BanService::accurate_error_counter with many unique IPs until the next reset interval, even though the policy is intended as a compatibility bypass.

If Disabled mode is meant to avoid ban-state churn (per issue #1136 acceptance criteria and earlier review discussions), the banning listener needs to avoid calling increase_counter when policy is Disabled, while still allowing separate observability metrics.

                // When connection ID validation is disabled, the tracker is
                // intentionally accepting requests with invalid or arbitrary
                // connection IDs. Enforcing IP bans in that mode is
                // contradictory — the banning listener still counts invalid
                // cookies for observability, but the ban is not acted upon.
                let ban_enforcement_active = connection_id_validation == ConnectionIdValidationPolicy::Strict;

                if ban_enforcement_active && udp_tracker_core_container.ban_service.read().await.is_banned(&req.from.ip()) {

docs/issues/open/1136-1978-configurable-udp-connection-id-validation-policy.md:10

  • The issue spec frontmatter still points at related-pr: 2002, but this change set appears to be PR #2032 (see docs/pr-reviews/pr-2032-copilot-suggestions.md and the PR metadata). Keeping an outdated PR reference makes it harder to trace implementation history.
spec-path: docs/issues/open/1136-1978-configurable-udp-connection-id-validation-policy.md
branch: "1136-connection-id-validation-policy"
related-pr: 2002
last-updated-utc: 2026-07-27 12:36

docs/issues/open/1136-1978-configurable-udp-connection-id-validation-policy.md:361

  • Acceptance Verification evidence for AC2 references v3_0_0::UdpTracker, but the implementation moved connection_id_validation to v3_0_0::udp_tracker_server::UdpTrackerServer (global setting). This table entry should match the actual type to avoid confusion for reviewers.
| AC1   | DONE                   | Enum `ConnectionIdValidationPolicy` with `strict`/`disabled` serde values in `v3_0_0/udp_tracker_server.rs`                             |
| AC2   | DONE                   | Field `connection_id_validation` on `v3_0_0::UdpTracker` struct                                                                         |
| AC3   | DONE                   | `#[serde(default)]` + test `it_should_default_connection_id_validation_to_strict`                                                       |
| AC4   | DONE                   | Strict mode rejects via `AnnounceService`/`ScrapeService` with `validate_cookie = true`; unit tests in `udp-core`                       |

packages/udp-server/src/testing/environment.rs:72

  • with_connection_id_validation is a pub method and is exercised by the UDP server contract tests, so #[allow(dead_code)] is redundant here (and can mask genuinely dead code in the future).

This issue also appears on line 74 of the same file.

    /// Sets the connection ID validation policy for this test environment.
    #[must_use]
    #[allow(dead_code)]
    pub fn with_connection_id_validation(mut self, policy: ConnectionIdValidationPolicy) -> Self {
        self.connection_id_validation = policy;
        self
    }

Clippy pointed out that 'x as i32' was unnecessary when x is already i32. Changed loop to use 'for x in 0i32..=15' and removed the cast.
Copilot AI review requested due to automatic review settings July 28, 2026 12:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (8)

docs/issues/open/1136-1978-configurable-udp-connection-id-validation-policy.md:336

  • The manual verification scenarios still expect “no ban increment” in Disabled mode (M2) and include “Mixed policies remain isolated” (M4). That conflicts with (a) the current implementation where Event::UdpError{ConnectionCookie} is emitted and the banning listener increments counters, and (b) the design pivot to a global policy (so strict+disabled can’t run simultaneously). The scenarios should be revised so future manual verification matches the actual deployable behavior.
| ID  | Scenario                                | Command/Steps                                                                                                    | Expected Result                                                                                                              | Status | Evidence                   |
| --- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------ | -------------------------- |
| M1  | Strict listener rejects an invalid ID   | Start a local strict UDP listener; send announce and scrape requests using an expired or zero connection ID      | Requests receive the existing connection-ID error; error metrics and ban counters increase                                   | TODO   |                            |
| M2  | Disabled listener accepts an invalid ID | Start a local disabled UDP listener; repeat the same announce and scrape requests with arbitrary connection IDs  | Requests pass cookie validation and continue through normal request handling; cookie-error metrics emitted; no ban increment | TODO   |                            |
| M3  | Connect works on a disabled listener    | Send a connect request to a disabled listener; then use the returned connection ID in an announce/scrape request | Connect returns a valid connection ID; subsequent announce/scrape succeeds                                                   | TODO   |                            |
| M4  | Mixed policies remain isolated          | Start strict and disabled listeners in one process; send the same invalid requests to both                       | Strict listener rejects them; disabled listener accepts them; neither listener changes the other                             | TODO   |                            |

docs/issues/open/1136-1978-configurable-udp-connection-id-validation-policy.md:366

  • The Acceptance Verification evidence table has inconsistencies with the updated design: AC2 evidence mentions v3_0_0::UdpTracker, but the field is on UdpTrackerServer; AC9 evidence claims per-listener isolation, which conflicts with the global-policy decision documented earlier in this spec.
| AC1   | DONE                   | Enum `ConnectionIdValidationPolicy` with `strict`/`disabled` serde values in `v3_0_0/udp_tracker_server.rs`                             |
| AC2   | DONE                   | Field `connection_id_validation` on `v3_0_0::UdpTracker` struct                                                                         |
| AC3   | DONE                   | `#[serde(default)]` + test `it_should_default_connection_id_validation_to_strict`                                                       |
| AC4   | DONE                   | Strict mode rejects via `AnnounceService`/`ScrapeService` with `validate_cookie = true`; unit tests in `udp-core`                       |
| AC5   | DONE                   | Handlers call `check()` for observation but pass `validate_cookie = false` to service                                                   |
| AC6   | DONE                   | Connect handler unchanged; test `connect_still_issues_a_valid_connection_id` passes                                                     |
| AC7   | DONE                   | Handlers emit `UdpError { ConnectionCookie }` regardless of mode; ban listener always counts; main loop skips `is_banned` when disabled |
| AC8   | DONE                   | `Launcher::run_with_graceful_shutdown` emits `WARN` log on `Disabled`; `Unstarted` type alias                                           |
| AC9   | DONE                   | Policy is per-processor-instance; tests pass per-listener isolation; M4 scenario verified inline                                        |

packages/udp-server/src/handlers/announce.rs:86

  • In Disabled mode this still sends Event::UdpError { error: ConnectionCookie(..) } into the UDP server event bus. The banning listener currently treats any ConnectionCookie error as bannable and unconditionally calls ban_service.increase_counter(...) (packages/udp-server/src/banning/event/handler.rs:19-30), so IP-ban counters and UDP_TRACKER_SERVER_IPS_BANNED_TOTAL will still increase under invalid-ID traffic even though enforcement is skipped in launcher.rs. This conflicts with parts of the issue spec that still say “no ban increment” in Disabled mode and can make the “ips banned” metric misleading (it becomes ‘would-be-banned’).
    let announce_data = {
        // When validation is disabled, still perform the cookie check so the
        // banning listener can count invalid IDs for observability. Emit the
        // same UdpError event (objective fact: a cookie error occurred), but
        // do not return an error — the request is allowed to proceed.
        // Ban enforcement is skipped in the main loop when validation is
        // disabled (see launcher.rs), so the client is never actually blocked.
        let validate_cookie = match cookie_validation.connection_id_validation {
            ConnectionIdValidationPolicy::Strict => true,
            ConnectionIdValidationPolicy::Disabled => {
                if let Err(cookie_error) = check(
                    &request.connection_id,
                    gen_remote_fingerprint(&client_socket_addr),
                    cookie_validation.valid_range.clone(),
                ) {
                    tracing::debug!(
                        target: UDP_TRACKER_LOG_TARGET,
                        %client_socket_addr,
                        error = %cookie_error,
                        "connection ID validation disabled: invalid connection ID observed (request allowed, ban not enforced)"
                    );
                    if let Some(sender) = opt_udp_server_stats_event_sender.as_deref() {
                        sender
                            .send(Event::UdpError {
                                context: ConnectionContext::new(client_socket_addr, server_service_binding.clone()),
                                kind: Some(UdpRequestKind::Announce {
                                    announce_request: *request,
                                }),
                                error: ErrorKind::ConnectionCookie(cookie_error.to_string()),
                            })
                            .await;

packages/udp-server/src/handlers/scrape.rs:72

  • Same as announce: in Disabled mode this emits Event::UdpError { ConnectionCookie(..) }, and the banning listener unconditionally increments the ban-service counter for every such event (packages/udp-server/src/banning/event/handler.rs:19-30). That means Disabled mode still changes ban counters/state and the “ips banned” gauge even though enforcement is skipped, which conflicts with parts of the spec that still state “no ban increment” in Disabled mode.
    let scrape_data = {
        let validate_cookie = match cookie_validation.connection_id_validation {
            ConnectionIdValidationPolicy::Strict => true,
            ConnectionIdValidationPolicy::Disabled => {
                if let Err(cookie_error) = check(
                    &request.connection_id,
                    gen_remote_fingerprint(&client_socket_addr),
                    cookie_validation.valid_range.clone(),
                ) {
                    tracing::debug!(
                        target: UDP_TRACKER_LOG_TARGET,
                        %client_socket_addr,
                        error = %cookie_error,
                        "connection ID validation disabled: invalid connection ID observed (request allowed, ban not enforced)"
                    );
                    if let Some(sender) = opt_udp_server_stats_event_sender.as_deref() {
                        sender
                            .send(Event::UdpError {
                                context: ConnectionContext::new(client_socket_addr, server_service_binding.clone()),
                                kind: Some(UdpRequestKind::Scrape),
                                error: ErrorKind::ConnectionCookie(cookie_error.to_string()),
                            })
                            .await;
                    }

docs/pr-reviews/pr-2032-copilot-suggestions.md:15

  • This file is named for PR #2032, but the header and Source line still contain placeholders (<PR_NUMBER>, <PR_URL>), unlike other entries in docs/pr-reviews/. This makes the audit log ambiguous and harder to link back to the PR.
# PR #<PR_NUMBER> Copilot Suggestions Tracking

Source: Copilot PR review threads for <PR_URL>

packages/udp-core/src/lib.rs:32

  • The doc comment says the enum lives in udp-core “so that the service layer does not need to depend on the configuration crate”, but this crate already depends on and uses torrust_tracker_configuration (e.g. udp-core/src/container.rs) and Cargo.toml includes it. This reads as an outdated rationale and could confuse maintainers.
/// This mirrors [`torrust_tracker_configuration::v3_0_0::udp_tracker_server::ConnectionIdValidationPolicy`]
/// but lives in `udp-core` so that the service layer does not need to depend on
/// the configuration crate.

packages/udp-server/src/testing/environment.rs:71

  • #[allow(dead_code)] is used here without an explanation. Given the repo’s strict “deny warnings” posture, it would help to document why this needs to be allowed (typically because integration tests compile the library without cfg(test)).
    /// Sets the connection ID validation policy for this test environment.
    #[must_use]
    #[allow(dead_code)]
    pub fn with_connection_id_validation(mut self, policy: ConnectionIdValidationPolicy) -> Self {
        self.connection_id_validation = policy;

docs/issues/open/1136-1978-configurable-udp-connection-id-validation-policy.md:325

  • The required focused coverage section still references mixed-policy listeners and claims “IP-ban counters only in strict mode”, but the spec elsewhere pivots to a global policy (no mixed strict/disabled listeners) and the implementation/ADR describe counting invalid cookies for observability even in Disabled mode. These bullets should be updated to match the current design.

This issue also appears in the following locations of the same file:

  • line 331
  • line 358
  - Scrape succeeds with an arbitrary (invalid) connection ID
- Cookie-error metrics are emitted in both modes; IP-ban counters only in strict mode
- Two simultaneous listeners using different policies

@josecelano

Copy link
Copy Markdown
Member Author

ACK 2407165

@josecelano
josecelano merged commit 19c28a5 into torrust:develop Jul 28, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add configurable UDP connection ID validation policy

2 participants