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.
Problem
ctrld tries the upstreams matched by a policy in strict configured order:
upstream.0is queried first on every request, and the rest are reached only on a transport error, timeout, orfailover_rcodesmatch. For a policy that lists several equivalent upstreams (for example multiple Cloudflare DoH3 endpoints),upstream.0absorbs 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_strategyfield 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, andleak_on_upstream_failureare 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 torandomonly.Reference implementation
I understand you are not currently accepting PRs. I have a working, tested implementation of the
randomstrategy 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 smallorderUpstreamshelper that shuffles the upstream IDs and their paired configs together, and a single hook inproxy(). Unknown values and length mismatches fall back to configured order. Unit tests cover ordering, validation, fallback, and propagation throughupstreamFor;go test ./...,go vet,gofmt -l, andgo test -race ./cmd/cli/are clean, with no new dependencies.Verified on a Raspberry Pi 5 (linux/arm64), 200 queries with unique labels: with
randomthe queries spread roughly evenly across all four upstreams, while absent andsequentialalways usedupstream.0.