Skip to content

Feature request: random upstream selection strategy #315

Description

@Omoeba

Problem

ctrld tries the upstreams matched by a policy in strict configured order: upstream.0 is queried first on every request, and the rest are reached only on a transport error, timeout, or failover_rcodes match. For a policy that lists several equivalent upstreams (for example multiple Cloudflare DoH3 endpoints), upstream.0 absorbs all of the initial query load and the others serve as failover only. There is currently no way to distribute queries across equivalent upstreams.

Proposal

Add an optional upstream_strategy field to [listener.<n>.policy]:

  • sequential (default): current behavior, configured order.
  • random: shuffle the matched upstreams per query to spread load.

Ordering happens once after policy resolution, and the existing sequential failover walk then runs over the result, so timeouts, failover_rcodes, caching, recovery, and leak_on_upstream_failure are unchanged. Default behavior is unchanged, and LAN/PTR queries are never reordered.

This same field is a natural dispatch point for future strategies such as parallel (race upstreams, take the first answer; requested in #263) and latency-aware selection (p2/wp2), but this request is scoped to random only.

[listener.0.policy]
  name = "h3-only"
  upstream_strategy = "random"
  networks = [
    { "network.0" = ["upstream.0", "upstream.1", "upstream.2", "upstream.3"] },
  ]

Reference implementation

I understand you are not currently accepting PRs. I have a working, tested implementation of the random strategy on a fork (PR #314), in case it is useful as a reference:

https://github.com/Omoeba/ctrld/tree/proxy-random-upstream-strategy

It adds the config field (validated as oneof=sequential random), a small orderUpstreams helper that shuffles the upstream IDs and their paired configs together, and a single hook in proxy(). Unknown values and length mismatches fall back to configured order. Unit tests cover ordering, validation, fallback, and propagation through upstreamFor; go test ./..., go vet, gofmt -l, and go test -race ./cmd/cli/ are clean, with no new dependencies.

Verified on a Raspberry Pi 5 (linux/arm64), 200 queries with unique labels: with random the queries spread roughly evenly across all four upstreams, while absent and sequential always used upstream.0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions