Skip to content

sync: merge upstream/master through 1565071 (Go 1.26, SNI-check consolidation, dcprobe, proxy-protocol listener)#33

Merged
dolonet merged 33 commits into
masterfrom
sync/upstream-2026-05-31
May 31, 2026
Merged

sync: merge upstream/master through 1565071 (Go 1.26, SNI-check consolidation, dcprobe, proxy-protocol listener)#33
dolonet merged 33 commits into
masterfrom
sync/upstream-2026-05-31

Conversation

@dolonet
Copy link
Copy Markdown
Owner

@dolonet dolonet commented May 31, 2026

Merges 32 upstream commits from 9seconds/mtg master through 1565071.

Notable upstream changes

Conflict resolutions (fork behavior preserved)

  • internal/cli (doctor / sni_check / run_proxy): adopted upstream's runSNICheck refactor but parameterized it with an explicit host, so mtg doctor keeps multi-secret semantics (getFirstSecretHost()) while the startup warning keeps using conf.Secret.Host. Taking upstream verbatim would have made doctor report a spurious empty-host resolve error for [secrets] configs.
  • New files (mtglib/dcprobe, internal/cli/sni_check.go): module path rewritten 9seconds/mtg/v2dolonet/mtg-multi.
  • contrib/sni-router/README.md: kept fork's "upstream discussion Контейнер с Caddy не имеет доступа в сеть на OpenWrt 9seconds/mtg#513" wording.

Verification

  • go build ./... ✓ · go vet ./... ✓ · go test ./... all green (incl. mtglib/dcprobe)
  • Main binary builds on Go 1.26.2
  • internal/cli has no unit tests upstream or here — doctor/sni_check changes are behavior-preserving by inspection, not machine-verified

dolonet and others added 30 commits May 9, 2026 00:42
Closes 9seconds#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.
Mirrors the proxy-protocol-listener TOML config option so simple-run can
sit behind a PROXY-protocol-emitting frontend (HAProxy, nginx stream)
without dropping back to a config file.

Discussed in 9seconds#502: with this flag the compose recipe collapses to
'simple-run $BIND $SECRET --proxy-protocol-listener', keeping the
secret in the environment and no mtg-config.toml in the repo.
Follow-up to 9seconds#503, which introduced the (required) marker on bind-to.
secret is the other top-level option without a sensible default, so it
takes the same marker, on the first line of its description.

Symmetric with 9seconds#504 ((default) on prefer-ipv6). Rolling the convention
out to the rest of the file is left for separate PRs.
OpenWrt firewall zones are bound to interface names. With bare podman
you can pin the static podman0 bridge into a zone, but podman-compose
creates a project-scoped network and netavark spawns a fresh bridge
(podman1, podman2, ...) per project — with no firewall rules — so
containers lose outbound access.

Mark the default network as external/name=podman to attach to the
router-managed podman0 instead.

Background: 9seconds#513.
Track `mtg-config.toml.example` with `secret = "${MTG_SECRET}"`; the
rendered `mtg-config.toml` and local `.env` are gitignored, so the
secret never lands in a tracked file.

Quick start switches from "paste the secret into mtg-config.toml" to
either `envsubst < mtg-config.toml.example > mtg-config.toml` or
`cp` + hand-edit `${MTG_SECRET}` for users without envsubst.

After 9seconds#502 made DOMAIN env-driven, the secret was the last hand-edit
of a tracked file in the example. Follow-up to 9seconds#506.
…ples

`MTG_SECRET=<placeholder> envsubst < ...` was shell-broken on literal
copy-paste — bash parses `<placeholder>` as redirection from a
non-existent file. Two-line `export MTG_SECRET=...` + plain envsubst
form removes the ambiguity. Applies to README, docker-compose.yml,
and the .example header.
compose: fix non-functional 'host' option
…onvention

docs: mark secret as (required) in example.config.toml
…ol-listener

simple-run: add --proxy-protocol-listener flag
…ig-example

contrib/sni-router: render mtg-config.toml from a tracked .example
doctor: surface both public IPs in SNI-DNS mismatch message
…-podman

contrib/sni-router: document OpenWrt + podman-compose network workaround
Bridge ingress (Docker's docker-proxy userland forwarder, Podman's
slirp4netns/pasta) rewrites the source IP of inbound connections on a
published port to the bridge gateway address.  HAProxy then stamps that
gateway address into the PROXY v2 header it forwards to mtg and Caddy,
so neither backend ever sees a real client IP.

Move HAProxy into the host netns (network_mode: host) so it binds
:443/:80 directly with no NAT in the path.  mtg and Caddy stay on the
compose bridge and are published on 127.0.0.1 only; HAProxy reaches
them via host loopback and PROXY v2 carries the real client IP (v4 or
v6) end-to-end.

Also accept IPv6 clients explicitly on the HAProxy frontends — `bind
*:443` is IPv4-only and missed v6 clients on hosts where the previous
example happened to "work" only because of dual-stack quirks.

Add 127.0.0.0/8 to Caddy's PROXY allow-list to cover the new loopback
hop from HAProxy.  README gains a short subsection explaining the
host-mode choice and its trade-off (HAProxy occupies host :443/:80).

Diagnosed and tested by @bam80 on Fedora + Docker 29.  Fixes 9seconds#498.
…rrow Caddy allow)

- Caddy allow: 127.0.0.0/8 → 127.0.0.1/32 (only loopback peer is HAProxy).
- haproxy.cfg: rewrite v6only comment to describe what it actually does
  (suppresses v4-mapped accept, preventing conflict with the v4 bind),
  not the symptom.
- docker-compose.yml: trim the 8-line haproxy comment to 3 lines and
  defer the rationale to README.  Add one-line note explaining why web
  uses host port 8080 (HAProxy owns :80).
- README: condense the "Why network_mode: host" subsection.  Spell out
  trade-offs as a list: own-the-host-ports, Linux-only (Docker Desktop
  doesn't make this layout reachable), userns-remap incompatibility.
  Note that mtg-config.toml stays as-is because mtg/web remain on the
  compose bridge.
Switch to one-line `bind :80,[::]:80` and `bind :443,[::]:443` per
review feedback in 9seconds#522.  The v6only flag was self-documentation, not
load-bearing: with SO_REUSEADDR (HAProxy's default) and bindv6only=0
the kernel routes v4 packets to the more-specific AF_INET socket
regardless.  Comment trimmed to match — the v6only paragraph is gone
because v6only itself is gone.

The shorter form also scales more cleanly when adding ports later,
e.g. `bind :8080,[::]:8080` on a new line.
New leaf package that performs the first step of the MTProto handshake
(req_pq_multi -> resPQ) over the existing obfuscated2 transport. No
auth_key is generated; no long-lived state is introduced. Two TL
messages, one round-trip, no new dependencies.

A generic listener cannot fake the reply because it must echo back our
random nonce in resPQ.

Used by the doctor command in a follow-up commit to distinguish a real
Telegram DC from a generic TCP listener bound to port 443.
Closes 9seconds#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.
`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
…al-ips

sni-router: host-net HAProxy to preserve real client IPs
…i-check

internal/cli: consolidate duplicated SNI-DNS check
Mention default value for tolerate-time-skewness
9seconds and others added 3 commits May 27, 2026 16:25
…lidation, dcprobe, proxy-protocol listener)

Upstream PRs included:
- 9seconds#543 upgrade-go: Go 1.26 + goreleaser/gopls/govulncheck/gofumpt/golangci-lint bumps
- 9seconds#540 tts-default-value: document default for tolerate-time-skewness
- 9seconds#528 consolidate-sni-check: extract shared runSNICheck (doctor + startup warning)
- 9seconds#522 sni-router-host-mode-real-ips: HAProxy host networking for real client IPs
- 9seconds#496 doctor/rpc-probe: deepen DC verification with MTProto handshake probe (mtglib/dcprobe) [our PR, round-tripped]
- 9seconds#505 doctor/sni-dns-message: surface both public IPs in SNI-DNS mismatch
- 9seconds#523 docs/sni-router-openwrt-podman
- 9seconds#525 contrib/sni-router-config-example: render mtg-config.toml from tracked .example
- 9seconds#510 simple-run-proxy-protocol-listener: --proxy-protocol-listener flag
- 9seconds#521 docs/required-default-convention; 9seconds#504 prefer-ipv6 default; 9seconds#514 MTG_SECRET envsubst fix

Conflict resolutions (preserve fork behavior):
- internal/cli: adopt upstream's runSNICheck refactor but parameterize it with an
  explicit host, so doctor keeps multi-secret semantics (getFirstSecretHost) while
  the startup warning keeps using conf.Secret.Host as before.
- new files (mtglib/dcprobe, internal/cli/sni_check.go): rewrite module path
  9seconds/mtg/v2 -> dolonet/mtg-multi.
- contrib/sni-router/README.md: keep fork's "upstream discussion 9seconds#513" wording.
@dolonet dolonet merged commit d2f3b04 into master May 31, 2026
7 checks passed
@dolonet dolonet deleted the sync/upstream-2026-05-31 branch May 31, 2026 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants