contrib/sni-router: split MTG_SECRET assignment from envsubst in examples
`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.
contrib/sni-router: render mtg-config.toml from a tracked .example
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 #502 made DOMAIN env-driven, the secret was the last hand-edit
of a tracked file in the example. Follow-up to #506.
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: #513.
docs: mark secret as (required) in example.config.toml
Follow-up to #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 #504 ((default) on prefer-ipv6). Rolling the convention
out to the rest of the file is left for separate PRs.
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 #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.
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.
sni-router: extend PROXY-protocol sync list to four pieces
mtg now also sends PROXY v2 on the fronting dial (introduced in the
previous commit via [domain-fronting].proxy-protocol = true), so the
"Real client IPs" section's sync list must include that fourth piece.
Without it, an operator who disables Caddy's PROXY listener wrapper
without also flipping [domain-fronting].proxy-protocol will leave mtg
sending an unparsed PROXY v2 prefix to Caddy on every fronted probe.
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.
When the secret's domain resolves back to this server (the SNI-router
default), mtg's fallback fronting dial lands on HAProxy, the SNI
matches the secret, HAProxy routes the connection back to mtg -> loop.
Set [domain-fronting].host = "web" in mtg-config.toml so mtg dials
Caddy directly via compose-network DNS, bypassing HAProxy. Requires
mtg >= 2.4 (#480 added hostname acceptance for the fronting target).
README gains a "Fronting loop" section explaining the cause.
contrib/sni-router: align mtg-config.toml comments with $DOMAIN flow
Comments said 'Replace example.com everywhere' which became stale once
$DOMAIN drives haproxy.cfg + Caddyfile + docker-compose. Reword so the
user's mental model is: pick a domain, generate the secret with it,
put it in .env once.
contrib/sni-router: read $DOMAIN from env in haproxy.cfg
Closes #501.
Before: the SNI hostname had to be edited in haproxy.cfg, plus
DOMAIN had to be set in .env for Caddy. Two places to keep in sync.
After: DOMAIN is the single source — set it once in .env (or export),
docker-compose forwards it into both haproxy and caddy containers,
and HAProxy interpolates ${DOMAIN} into the SNI ACL at startup.
mtg-config.toml's secret still embeds the domain in its bytes
(generated once with `mtg generate-secret <domain>`), so that one
remains a one-time edit at install — the README is updated to reflect
this.
HAProxy has supported environment-variable substitution in config
strings since 1.6.
Per discussion on #494, this allows external packages (e.g. the upcoming
mtglib/dcprobe for the doctor RPC probe) to reuse the obfuscated2
transport without an internal wrapper.
No public-API change beyond the import path. The only exported names
(Obfuscator, its two methods, and the Secret field) were already
exported within the package.
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
Fix SELinux-related permission denied error for containerized apps
reading configs exposed via volumes.
Also make it possible to use port 80 in the fronted.
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
Do not use custom DNS resolver to dial proxy upstreams
Fixes #439.
When `[network] dns = "tls://..."` (or "https://...") is set, the
resulting *net.Resolver gets attached to the base network's NativeDialer
and was previously also handed to golang.org/x/net/proxy.FromURL via
NewProxyNetwork. As a result, the SOCKS5 client used the user's DoT/DoH
resolver to look up the SOCKS server's own hostname (e.g. "xray" inside
a docker compose stack). Public DNS-over-TLS resolvers don't know about
docker-compose service names, k8s service DNS, /etc/hosts entries, or
corporate split-horizon DNS, so the upstream lookup returned NXDOMAIN
and the proxy chain broke with a misleading "lookup xray on
127.0.0.11:53: no such host" error.
The custom DNS resolver exists to bypass DPI poisoning when resolving
public censored names like Telegram DCs or the SNI/fronting host. Proxy
server addresses are almost always internal and should be resolved via
the system resolver instead. This change introduces proxyServerDialer,
which copies the timeout and fallback-delay from the base dialer but
leaves Resolver==nil, and uses it for the SOCKS upstream.
The new internal test asserts the structural property directly: the
returned dialer must not inherit the base's custom resolver.
docs: link example.config.toml as the config reference
The TOML configuration file is documented in example.config.toml -
every option is listed with its default value and an inline comment.
But the README never links to it directly: it just says "please
checkout an example configuration file" / "please check configuration
file example", which is easy to skim past when looking for a config
reference.
This patch:
- makes example.config.toml an explicit link in the "Prepare a
configuration file" section and calls it out as the configuration
documentation;
- adds the same link to the Doppelganger and Metrics sections, which
also point readers at the example file.
No content/option changes - README only.
Address round-two review: rename mtglib privates, reorder, more tests
- mtglib/proxy.go: rename private field domainFrontingIP -> domainFrontingHost
and update DomainFrontingAddress() doc comment to reflect that hostnames
are now accepted. The exported mtglib.ProxyOpts.DomainFrontingIP is
unchanged (public API), so the assignment in NewProxy now reads
`domainFrontingHost: opts.DomainFrontingIP,` which makes the
public-vs-internal naming explicitly visible at the boundary.
- internal/config/{parse,config}.go: reorder so Host comes before IP in
the [domain-fronting] struct. Cosmetic, but signals Host is the
preferred forward path.
- Add TestDomainFrontingHostAcceptsLiteralIP + domain_fronting_host_ip.toml
fixture exercising the documented "host accepts hostname or literal IP"
contract end-to-end.