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
@@ -0,0 +1,155 @@
---
doc-type: issue
issue-type: enhancement
status: open
priority: p2
github-issue: 1415
spec-path: docs/issues/open/1415-1978-use-service-binding-instead-of-socket-addr.md
branch: "1415-listen-url"
related-pr: null
last-updated-utc: 2026-07-13 21:00
semantic-links:
skill-links:
- create-issue
related-artifacts:
- packages/configuration/src/v3_0_0/
- packages/tracker-core/src/lib.rs
- packages/http-core/src/container.rs
- packages/udp-server/src/server/launcher.rs
- src/bootstrap/jobs/
---

<!-- skill-link: create-issue -->

# Issue #1415 - Use `ServiceBinding` (protocol + address) instead of bare `SocketAddr` for service identity

> **EPIC position**: Subissue #5 of 9. Independent — no config changes, no overlap with other subissues. Can run in parallel with #1453, #1490, #889.

## Goal

Replace bare `SocketAddr` values with `ServiceBinding` (from `torrust-net-primitives`) wherever the socket address is used for service identity — in logs, events, health check info, and metrics. `ServiceBinding` already carries both the protocol scheme and the socket address, so consumers get the full protocol + address context without any new types or external crate changes.

## Background

The tracker currently passes `SocketAddr` values around for each service (UDP tracker, HTTP tracker, API, health check). However, the socket address alone lacks the **protocol/scheme** information. When logging startup messages, the code manually constructs URL-like strings:

```text
UDP TRACKER: Started on: udp://0.0.0.0:6868
HTTP TRACKER: Started on: http://0.0.0.0:7070
API: Started on http://0.0.0.0:1212
```

But this protocol context is not available as a first-class value in the places that need it:

1. **Health check API** (#1409) — needs to expose the service type and address
2. **Metrics** (#1403 / #1414) — needs protocol as a label for Prometheus metrics
3. **Events** — domain events should carry the service protocol, not just the socket address

The solution is to use the existing `ServiceBinding` type from `torrust-net-primitives` (which already carries `scheme` + `SocketAddr`) wherever bare `SocketAddr` is currently passed. No new types, no external crate changes, no URL path segments.

### What this issue does NOT do

- **Does not add URL path segments** (e.g. `/announce`). Path segments are hardcoded per protocol and not useful for service identity.
- **Does not resolve the bind address to a concrete IP**. `ServiceBinding` carries the configured bind address as-is (e.g. `0.0.0.0:7070`).
- **Does not provide a public-facing URL**. That is handled by #1417 (`public_url` config field).

### Future extension: internal connection URL

A future issue could build an "internal connection URL" from the OS-resolved IP + hardcoded path segment (e.g. `https://192.168.1.5:7070/announce`). This would be useful when the `public_url` is not configured but the service is bound to a concrete reachable IP. This is deferred — the `public_url` field (#1417) covers the primary use case.

## Scope

### In Scope

- Use `ServiceBinding` (from `torrust-net-primitives`) wherever bare `SocketAddr` is currently passed for service identity:
- Server startup logging
- Health check info structs
- Metrics labels
- Domain events (if applicable)
- No new types — `ServiceBinding` already has `protocol()` and `bind_address()` methods
- No changes to `torrust-net-primitives` external crate

### Out of Scope

- Adding URL path segments (e.g. `/announce`) — hardcoded per protocol, not useful for identity
- Resolving bind address to concrete IP — `ServiceBinding` carries the configured address as-is
- Adding a `public_url` config field (tracked in #1417)
- Building an "internal connection URL" from resolved IP + path segment (future extension)
- Changing the tracker protocol types (UDP/HTTP protocol parsing)
- TLS certificate configuration

## Implementation Plan

| ID | Status | Task | Notes |
| --- | ------ | ------------------------------------------------------------------------ | ------------------------------------------------------------ |
| T1 | TODO | Identify all places where bare `SocketAddr` is used for service identity | Logs, health check, metrics, events, server launchers |
| T2 | TODO | Replace `SocketAddr` with `ServiceBinding` in those places | `ServiceBinding` already has `protocol()` + `bind_address()` |
| T3 | TODO | Update startup logging to use `ServiceBinding` | Replace manual URL string construction |
| T4 | TODO | Update health check info to include `ServiceBinding` | For issue #1409 |
| T5 | TODO | Update metrics to use `ServiceBinding` scheme as a label | For issue #1403/#1414 |
| T6 | TODO | Run `linter all` and tests | |

## Progress Tracking

### Workflow Checkpoints

- [ ] Spec drafted in `docs/issues/drafts/`
- [ ] Spec reviewed and approved by user/maintainer
- [ ] GitHub issue created and issue number added to this spec
- [ ] Implementation completed
- [ ] Automatic verification completed (`linter all`, relevant tests)
- [ ] Manual verification scenarios executed and recorded
- [ ] Acceptance criteria reviewed after implementation
- [ ] Issue closed and spec moved to `docs/issues/open/`

### Progress Log

- 2026-07-13 21:00 UTC - josecelano - Initial spec drafted
- 2026-07-14 00:00 UTC - josecelano - Narrowed scope: use existing `ServiceBinding` instead of bare `SocketAddr`; no new types; no external crate changes; no URL path segments. Deferred "internal connection URL" to future extension.

## Acceptance Criteria

- [ ] AC1: `ServiceBinding` is used wherever bare `SocketAddr` was used for service identity
- [ ] AC2: Startup logs show protocol + address (e.g. `udp://0.0.0.0:6969`) via `ServiceBinding`
- [ ] AC3: Health check endpoint exposes `ServiceBinding` per service
- [ ] AC4: Metrics include the protocol scheme as a label (from `ServiceBinding`)
- [ ] AC5: No new config field required — `ServiceBinding` is derived from scheme + bind_address
- [ ] AC6: No changes to `torrust-net-primitives` external crate
- [ ] `linter all` exits with code `0`
- [ ] Relevant tests pass

## Verification Plan

### Automatic Checks

- `linter all`
- `cargo test --workspace`

### Manual Verification Scenarios

| ID | Scenario | Command/Steps | Expected Result | Status | Evidence |
| --- | --------------------------------- | --------------------------------------------- | ------------------------------------------ | ------ | -------- |
| M1 | Verify listen URL in startup logs | Run tracker locally, check startup log output | Logs show `udp://0.0.0.0:6969` etc. | TODO | |
| M2 | Verify listen URL in health check | `curl http://127.0.0.1:1313/health` | Response includes `listen_url` per service | TODO | |

### Acceptance Verification

| AC ID | Status | Evidence |
| ----- | ------ | -------- |
| AC1 | TODO | |
| AC2 | TODO | |
| AC3 | TODO | |
| AC4 | TODO | |
| AC5 | TODO | |

## Risks and Trade-offs

- **Scope creep**: This issue touches many packages (server launchers, health check, metrics). Mitigation: keep changes focused on replacing `SocketAddr` with `ServiceBinding` — no refactoring of how addresses are consumed.
- **No external crate changes**: `ServiceBinding` from `torrust-net-primitives` is used as-is. No coordinated release needed.

## References

- Related issues: #1409 (health check), #1403/#1414 (metrics)
- Related: `packages/tracker-core/src/lib.rs`
- Related: `packages/http-core/src/container.rs`
- Related: `packages/udp-server/src/server/launcher.rs`
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
doc-type: issue
issue-type: enhancement
status: draft
status: open
priority: p3
github-issue: 1417
spec-path: docs/issues/drafts/1417-add-public-service-url-to-configuration.md
spec-path: docs/issues/open/1417-1978-add-public-service-url-to-configuration.md
branch: "1417-add-public-service-url"
related-pr: null
last-updated-utc: 2026-06-23 18:45
Expand All @@ -15,14 +15,18 @@ semantic-links:
- issue #1640
- issue torrust/torrust-tracker-deployer
- issue torrust/torrust-tracker-deployer docs/ai-training/dataset/environment-configs/02-full-stack-lxd.json
- packages/configuration/src/v2_0_0/http_tracker.rs
- packages/configuration/src/v2_0_0/udp_tracker.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/tracker_api.rs
- packages/configuration/src/v3_0_0/health_check_api.rs
---

<!-- skill-link: create-issue -->

# Issue #1417 - Include public service URL in configuration

> **EPIC position**: Subissue #4 of 9. Depends on #1640 (subissue #3) for the `Network` block placement decision — `public_url` stays flat (not inside `Network`). Implements after #1640 is complete.

## Goal

Add an optional `public_url` field to each tracker instance (`HttpTracker`, `UdpTracker`) and API service (`HttpApi`, `HealthCheckApi`) so the application knows the public-facing URL for each service regardless of network topology, reverse proxies, or TLS termination.
Expand Down Expand Up @@ -57,6 +61,7 @@ For example, the [Torrust Tracker Deployer](https://github.com/torrust/torrust-t

- Add optional `public_url: Option<String>` field to `HttpTracker`, `UdpTracker`, `HttpApi`, and `HealthCheckApi`
- Use a **single URL string** (e.g. `"https://tracker1.example.com/announce"`) — not decomposed into domain/path components, since consumers can parse those as needed
- Validate URL protocol at deserialization time (HTTP tracker → `http://`/`https://`, UDP tracker → `udp://`, API → `http://`/`https://`)
- The URL protocol (`https://`) provides TLS status; the domain is extracted by consumers
- Document the field in default config examples
- No runtime behaviour change — the field is stored in config and available for use by consumers (metrics, logging, etc.)
Expand All @@ -81,29 +86,39 @@ No changes are needed in this issue — the field just needs to be present in th

**Single URL string vs decomposed fields**: The field is a single URL string. Consumers parse protocol, domain, and path as needed. This is the simplest user-facing form and avoids duplicating the deployer's `domain` + `use_tls_proxy` approach.

**Where the field lives**: This field is about **public exposure** (how users reach the service), not **network topology** (how the service connects). It could stay flat on the config struct or join a `Network` block depending on future architecture decisions. For now, keep it flat — issue #1640 establishes the `Network` block, and this field can be placed accordingly.
**Where the field lives**: `public_url` is a **flat field** on each config struct (`HttpTracker`, `UdpTracker`, `HttpApi`, `HealthCheckApi`) — **not inside the `Network` block**. The `Network` block (established by #1640) groups **network topology** concerns (external IP, proxy awareness, socket behaviour). `public_url` is about **public exposure** (how users reach the service) — a different axis. A tracker instance can independently configure both `net.on_reverse_proxy` and `public_url`.

**Protocol validation**: The URL protocol is validated at deserialization time:

- HTTP tracker: must use `http://` or `https://`
- UDP tracker: must use `udp://`
- HTTP API / Health Check API: must use `http://` or `https://`

This catches misconfigurations early (e.g., accidentally setting `public_url = "udp://..."` on an HTTP tracker).

## Implementation Plan

| ID | Status | Task | Notes |
| --- | ------ | ----------------------------------------------------------- | -------------- |
| T1 | TODO | Add `public_url: Option<String>` to `HttpTracker` config | Default `None` |
| T2 | TODO | Add `public_url: Option<String>` to `UdpTracker` config | Default `None` |
| T3 | TODO | Add `public_url: Option<String>` to `HttpApi` config | Default `None` |
| T4 | TODO | Add `public_url: Option<String>` to `HealthCheckApi` config | Default `None` |
| T5 | TODO | Document field in default config examples and crate docs | |
| T6 | TODO | Run `linter all` and tests | |
| ID | Status | Task | Notes |
| --- | ------ | ----------------------------------------------------------- | ------------------------------------------------------------ |
| T1 | TODO | Add `public_url: Option<String>` to `HttpTracker` config | Default `None`; validate protocol is `http://` or `https://` |
| T2 | TODO | Add `public_url: Option<String>` to `UdpTracker` config | Default `None`; validate protocol is `udp://` |
| T3 | TODO | Add `public_url: Option<String>` to `HttpApi` config | Default `None`; validate protocol is `http://` or `https://` |
| T4 | TODO | Add `public_url: Option<String>` to `HealthCheckApi` config | Default `None`; validate protocol is `http://` or `https://` |
| T5 | TODO | Document field in default config examples and crate docs | |
| T6 | TODO | Run `linter all` and tests | |

## Progress Tracking

### Progress Log

- 2026-06-23 18:45 UTC - Copilot - Drafted from GitHub issue #1417 and discussions in issue #1640 spec review.
- 2026-07-14 00:00 UTC - josecelano - Resolved placement: `public_url` stays flat (not inside `Network`). Added protocol validation. Updated related-artifacts to v3 paths.

## Acceptance Criteria

- [ ] AC1: All config structs gain `public_url: Option<String>` field
- [ ] AC2: Default config examples include the new field (commented or shown)
- [ ] AC3: No runtime behaviour change — field is present for consumer use
- [ ] AC2: Protocol validation rejects mismatched protocols (e.g., `udp://` on HTTP tracker)
- [ ] AC3: Default config examples include the new field (commented or shown)
- [ ] AC4: No runtime behaviour change — field is present for consumer use
- [ ] `linter all` exits with code `0`
- [ ] Relevant tests pass
Loading