Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ cat announce.json | tracker_client udp announce 127.0.0.1:6969 --request-stdin
"downloaded": 5678,
"left": 0,
"port": 6881,
"peer_addr": "10.0.0.1",
"ip": "10.0.0.1",
"peer_id": "-RC00000000000000001",
"compact": 1,
"key": 42,
Expand All @@ -77,7 +77,7 @@ cat announce.json | tracker_client udp announce 127.0.0.1:6969 --request-stdin

Notes:

- HTTP uses `peer_addr` and `compact`.
- HTTP uses `ip` and `compact`.
- UDP uses `ip_address`, `key`, and `peers_wanted`.
- A shared schema can allow optional protocol-specific fields.

Expand Down
14 changes: 7 additions & 7 deletions console/tracker-client/src/console/clients/http/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ enum Command {
left: Option<u64>,
#[arg(long, value_parser = parse_non_zero_port)]
port: Option<u16>,
#[arg(long = "peer-addr")]
peer_addr: Option<IpAddr>,
#[arg(long = "ip")]
ip: Option<IpAddr>,
#[arg(long = "peer-id", value_parser = parse_peer_id)]
peer_id: Option<PeerId>,
#[arg(long, value_enum)]
Expand All @@ -173,7 +173,7 @@ struct AnnounceOptions {
downloaded: Option<u64>,
left: Option<u64>,
port: Option<u16>,
peer_addr: Option<IpAddr>,
ip: Option<IpAddr>,
peer_id: Option<PeerId>,
compact: Option<CliCompact>,
output_format: OutputFormat,
Expand All @@ -194,7 +194,7 @@ pub async fn run() -> anyhow::Result<()> {
downloaded,
left,
port,
peer_addr,
ip,
peer_id,
compact,
format,
Expand All @@ -208,7 +208,7 @@ pub async fn run() -> anyhow::Result<()> {
downloaded,
left,
port,
peer_addr,
ip,
peer_id,
compact,
output_format: format,
Expand Down Expand Up @@ -255,8 +255,8 @@ async fn announce_command(options: AnnounceOptions, timeout: Duration) -> anyhow
if let Some(port) = options.port {
query_builder = query_builder.with_port(port);
}
if let Some(peer_addr) = options.peer_addr {
query_builder = query_builder.with_peer_addr(peer_addr);
if let Some(ip) = options.ip {
query_builder = query_builder.with_ip(ip);
}
if let Some(peer_id) = options.peer_id {
query_builder = query_builder.with_peer_id(&peer_id);
Expand Down
14 changes: 7 additions & 7 deletions console/tracker-client/src/console/clients/unified/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ pub enum Command {
left: Option<u64>,
#[arg(long, value_parser = parse_non_zero_port)]
port: Option<u16>,
#[arg(long = "peer-addr")]
peer_addr: Option<IpAddr>,
#[arg(long = "ip")]
ip: Option<IpAddr>,
#[arg(long = "peer-id", value_parser = parse_peer_id)]
peer_id: Option<PeerId>,
#[arg(long, value_enum)]
Expand All @@ -91,7 +91,7 @@ struct AnnounceOptions {
downloaded: Option<u64>,
left: Option<u64>,
port: Option<u16>,
peer_addr: Option<IpAddr>,
ip: Option<IpAddr>,
peer_id: Option<PeerId>,
compact: Option<CliCompact>,
output_format: OutputFormat,
Expand All @@ -110,7 +110,7 @@ pub async fn run(command: Command) -> anyhow::Result<()> {
downloaded,
left,
port,
peer_addr,
ip,
peer_id,
compact,
format,
Expand All @@ -124,7 +124,7 @@ pub async fn run(command: Command) -> anyhow::Result<()> {
downloaded,
left,
port,
peer_addr,
ip,
peer_id,
compact,
output_format: format,
Expand Down Expand Up @@ -171,8 +171,8 @@ async fn announce_command(options: AnnounceOptions, timeout: Duration) -> anyhow
if let Some(port) = options.port {
query_builder = query_builder.with_port(port);
}
if let Some(peer_addr) = options.peer_addr {
query_builder = query_builder.with_peer_addr(peer_addr);
if let Some(ip) = options.ip {
query_builder = query_builder.with_ip(ip);
}
if let Some(peer_id) = options.peer_id {
query_builder = query_builder.with_peer_id(&peer_id);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
semantic-links:
skill-links:
- create-adr
related-artifacts:
- packages/http-protocol/src/v1/requests/announce.rs
- packages/axum-http-server/src/lib.rs
- docs/issues/open/1985-rename-peer-addr-to-ip-in-http-announce-request/ISSUE.md
---

# Accept only IP addresses (not DNS names) in the HTTP announce `ip` GET parameter

- **Date**: 2026-07-16
- **Issue**: [#1985](https://github.com/torrust/torrust-tracker/issues/1985)
- **Spec**: `docs/issues/open/1985-rename-peer-addr-to-ip-in-http-announce-request/ISSUE.md`

## Context

[BEP 3](https://www.bittorrent.org/beps/bep_0003.html) defines the `ip` announce parameter as:

> An optional parameter giving the IP (or dns name) which this peer is at. Generally used
> for the origin if it's on the same machine as the tracker.

The current implementation parses the `ip` GET parameter by calling `IpAddr::from_str`. Any value
that is not a valid IP address (including DNS names) is silently dropped — the field is set to
`None` and the tracker falls back to using the connection IP.

A policy decision is needed: should the tracker support DNS names, resolve them, or explicitly
restrict the parameter to IP addresses only?

## Decision

**Accept only IP addresses in the HTTP announce `ip` GET parameter.**

Non-IP values (including DNS names) are silently ignored; the tracker falls back to the connection
IP. The restriction is documented in the module doc-comments.

## Considered Alternatives

| Approach | What | Pros | Cons |
| ---------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A — IP only (this decision)** | Accept only valid `IpAddr` values; silently ignore non-IP values; document the restriction | Simple, predictable, no latency, no DoS risk, consistent with all major trackers | Deviates from the literal BEP 3 spec text |
| **B — Resolve DNS names** | Accept DNS names and resolve them to IPs at announce time | Closer to BEP 3 literal wording | Latency per announce, DoS amplification risk (attacker-controlled DNS lookups), complexity, no known client sends hostnames |
| **C — Accept and store hostnames** | Parse and store hostnames as strings alongside IPs | Closest to BEP 3 literal wording | Incompatible with the `IpAddr`-based peer list model; no client or tracker implements this; no BEP defines how hostnames are returned in responses |

## Evidence from major trackers

- **opentracker**: accepts only IP addresses in `ip`. Has a separate compile-time feature flag
(`WANT_IP_FROM_QUERY_STRING`) to optionally use the `ip` value for the peer's address; the type
accepted is always an IP address.
- **chihaya**: accepts only IP addresses in `ip`.
- **No known tracker** supports DNS name resolution in the announce `ip` parameter.

## Consequences

- **Positive**: No latency impact on announce handling.
- **Positive**: No DNS-based DoS attack surface.
- **Positive**: Consistent with opentracker, chihaya, and all other known tracker implementations.
- **Positive**: The `IpAddr`-based peer list model is preserved without changes.
- **Negative**: Deviates from the literal BEP 3 spec text ("or dns name"). Mitigated by clear
documentation and the fact that no known client sends a hostname in this field.

A future issue may choose to return an explicit parse error for non-IP values (e.g. DNS names)
instead of silently ignoring them. Clients MUST NOT send hostnames in the `ip` field when
communicating with Torrust Tracker.
3 changes: 2 additions & 1 deletion docs/adrs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ semantic-links:
| [20260617093046](20260617093046_reject_wildcard_external_ip.md) | 2026-06-17 | Reject wildcard IPs as invalid `external_ip` values | Reject `0.0.0.0`/`::` in `external_ip` config at startup, change default to `None`. Fail fast on invalid config. |
| [20260620000000](20260620000000_add_ipv6_v6only_config_option.md) | 2026-06-20 | Add `ipv6_v6only` config option for separate sockets | Add `ipv6_v6only` boolean flag to `UdpTracker` and `HttpTracker` configs, defaulting to `false` (dual-stack), so operators can opt into separate IPv4/IPv6 sockets. |
| [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. |
| [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. |

## ADR Lifecycle

Expand Down
Loading
Loading