This PR has an intention of resolving URLs by using multiple endpoints
that identify an IP address of the service. This is handy if one service
is blocked for some reason.
The detection mechanism follows this logic:
1. It tries to access all services in parallel
2. If service respond with some error (like, no route to host for IPv6),
then we accurately collect those errors and return a merged one
3. In case of the first IP resolved, we immediately return it.
Also, this PR refactors how access and SNI check are performed.
`doctor`'s checkSecretHost and the proxy-startup warnSNIMismatch each
carried their own copy of the same logic: resolve the secret hostname,
determine the server's public IPv4/IPv6 (config first, getIP fallback),
and compare the two sets.
Extract that data-gathering into runSNICheck (internal/cli/sni_check.go),
returning an sniCheckResult. The success decision stays with each caller
because the rules genuinely differ — `doctor` reports OK when any family
matches, while the startup warning requires every detected family to
match — so only the gathering is shared, not the verdict.
No behavior change: both callers produce byte-identical output and the
same return values as before.
doctor: deepen DC verification with MTProto handshake probe
Closes #494.
After a successful TCP connect, run an unauthenticated req_pq_multi ->
resPQ exchange via mtglib/dcprobe. This rejects generic listeners that
happen to bind 443 but cannot speak MTProto.
Output now shows "(rpc <rtt>)" on success; on failure the wrapped error
distinguishes "tcp connect to ...: ..." from "rpc handshake to ...: ...".
The probe runs by default — an opt-in flag would defeat the purpose,
since the existing TCP-only check is what motivated the issue.
The hardcoded 128 KiB cap caps single-flow upload throughput at roughly
value / RTT on high-BDP links (~10 Mbit/s at 100 ms RTT), manifesting
as slow uploads through the proxy. Expose it as
[network] tcp-not-sent-lowat so operators can raise it on fast links,
while keeping the default behaviour unchanged.
doctor: surface both public IPs in SNI-DNS mismatch message
Closes #486.
The previous message read "Hostname X is resolved to Y addresses, not
Z" with Z being either the detected IPv4 or IPv6 (whichever was set
first), which made dual-stack mismatches confusing — a hostname
resolving to v6 only on a host with v4 detected and v6 undetected
printed "not <v4>" without hinting that v6 was the missing piece.
The reworked template lists the resolved DNS records and both public
addresses (or "<not detected>" when missing) so the gap is obvious:
Hostname X resolves to "<v6>", but the proxy's public IP is
1.2.3.4 (IPv4) / <not detected> (IPv6) — none of the resolved
addresses match
Pure message change.
Deprecate "ip" in favour of "host" for domain fronting
Per review on #480: warn-and-ignore for the IP-shaped paths,
mirroring the net.Dialer.DualStack precedent — a config that
sets only "ip" will warn at startup and effectively disable
domain-fronting until the user switches to "host".
- mtglib.ProxyOpts: add DomainFrontingHost; mark DomainFrontingIP
Deprecated and warn-and-drop in NewProxy.
- internal/config: GetDomainFrontingHost returns only
[domain-fronting].host; deprecated keys are no longer used to
derive the dial target. runProxy logs a startup warning per
deprecated key that is set.
- internal/cli: add --domain-fronting-host; --domain-fronting-ip
flag is parsed only so the runtime warning can fire.
- internal/cli/doctor: redirect the existing 2.3.0 entry at "host"
and add a 2.4.0 entry for [domain-fronting].ip.
- example.config.toml: mark # ip = ... as deprecated.
doctor: use WaitGroup.Go and recover panics in DC probes
Address review feedback on #485:
- switch to sync.WaitGroup.Go (Go 1.25+) for the per-DC goroutine
- recover panics inside the goroutine and record them as that DC's
error, so a single panicking probe no longer crashes the whole
doctor run and the remaining DCs still report their results
Each DC dial uses a 10s timeout, and "checkNetwork" iterates 6 DCs
sequentially, so worst case is ~60s when egress is broken. Probing in
parallel collapses the worst case to a single timeout window while
preserving the existing DC-ordered output.
Refs #482
Follow-up to the previous commit on this branch:
- Rename Config.GetDomainFrontingIP -> GetDomainFrontingHost. The
helper now returns a hostname or an IP, so the old name was a lie.
Drop the unused defaultValue net.IP parameter (every caller passed
nil). Update internal/cli/run_proxy.go and internal/cli/doctor.go;
rename the misleading `ip` local var in doctor.go to `override`.
- Add TOML fixtures (domain_fronting_host.toml, domain_fronting_ip.toml)
so the new field is exercised through the actual Parse()->JSON->Config
path users hit, not just via direct .Set() calls. Plus a positive
backward-compat test confirming an `ip`-only legacy config still
validates and resolves correctly, and a no-fronting test confirming
the unset case returns empty.
- Clarify example.config.toml: `ip` is kept for backward compatibility,
not because it has stricter validation semantics worth choosing over
`host`.
mtglib.ProxyOpts.DomainFrontingIP keeps its name (public API).
Add public-ipv4/public-ipv6 config options for manual IP override
On some servers ifconfig.co is unreachable (e.g. Hetzner, AdGuard DNS
blocklists), causing 'mtg doctor' SNI-DNS check and 'mtg access' link
generation to fail. New config options allow specifying public IPs
manually, with automatic detection as fallback.
Fixes #405