Completes the pair started in e41b55c, which deliberately left the proxy service
out because it was still on a runtime prlimit only and had not yet had its
maintenance window.
ts-lan-proxy was subject to the same Docker default soft limit of 1024 that took
Telemt down on 2026-09-09 — 3proxy does not raise its own limit either, and the
container's HostConfig.Ulimits was null. Steady state is ~22 descriptors, so the
ceiling was ~2% used in normal operation.
Applied on the client-02 host at 22:45 MSK with
docker compose up -d --no-deps --force-recreate proxy
never a whole-stack up -d, which would also recreate ts-vpn and leave
ts-lan-ikev2 holding a stale namespace. Verified afterwards: HostConfig.Ulimits
now 65536/524288, container IP unchanged (172.31.0.4), egress through the proxy
still measures the exit node 89.167.72.79 against a direct-egress control of
212.116.102.42, 3proxy.cfg untouched so the conditional-auth behaviour is
unchanged, and the syslog log driver still emits access lines (confirmed on the
wire).
This file is STILL missing ts-vpn and ts-lan-ikev2, which have run on the host
since 2026-09-05 — deploying it as-is would take the IKEv2 VPN down. That drift
is unrelated to this commit and still needs its own.
Mirrors what is now running on the client-02 docker host, so a "pull and
redeploy" from this repo can no longer silently revert it.
On 2026-09-09 a connection flood against the public :1080 (peak 4,301
connections/minute, ~100% failing the Fake-TLS handshake) exhausted the Telemt
container's file-descriptor table, which sat at Docker's default soft limit of
1024. accept() then failed for every caller — real Telegram clients and the
monitoring collector alike — until the burst drained. Steady state needs only
45-73 descriptors, so the ceiling was ~7% used in normal operation.
ulimits: nofile 65536/524288 on the telemt service
The host was mitigated at runtime with prlimit on the day, but that is lost on
any recreate, and this compose is exactly what a redeploy would restore. The
proxy (3proxy) service deliberately does NOT get the block in this commit — it
is still on the runtime limit only and is scheduled for its own window, so
adding it here would make the repo claim something that is not yet true.
rst_on_close = "errors" in telemt.toml.example
Scanners and DPI probes that never complete the handshake leave orphaned sockets
in FIN-WAIT-1. "errors" sets SO_LINGER(0) at accept() and clears it once a client
authenticates, so real sessions still close gracefully with FIN and only pre-auth
closes are aborted. Verified on the wire on the host: a failed handshake now ends
FIN then RST, while authenticated sessions in the same capture closed FIN-only.
Applied on the host at 22:17 MSK via telemt-safe-restart: healthy in 10s,
container IP unchanged, bad-handshake delta 0 over a 5-minute recheck, no EMFILE
since.
NOTE for whoever deploys this stack from this repo: it is still missing the
ts-vpn + ts-lan-ikev2 services that have been running on the client-02 host since
2026-09-05. Deploying this file as-is would take the IKEv2 VPN front down. That
drift is untouched here and needs its own commit.
Publish Telemt's container metrics port 9090 as 192.168.0.35:9092 because Cockpit already owns host port 9090. Bind the application listener to 0.0.0.0 inside the container and restrict Telemt's metrics whitelist to the client-02 Zabbix server at 192.168.0.34/32.\n\nDocument the Zabbix template macro URL and the Telemt 3.4.25 loopback-binding gotcha. The live endpoint returned HTTP 200 with 40,235 bytes from the Zabbix server; Telemt remained healthy and rebuilt its Telegram DC connections in six seconds.
alexbers/mtprotoproxy's Fake-TLS handshake was rejected by real Telegram
clients (server/clock/secret/egress all verified good), so replace it with
Telemt (ghcr.io/telemt/telemt:3.4.25) - the same proven implementation as the
Hetzner endpoint. Config: Fake-TLS on :1080, use_middle_proxy=false (direct-to-DC,
required with a SOCKS5 upstream) and [[upstreams]] socks5 ts-proxy:1055 so only
DC-bound traffic exits via the Tailscale exit node. Secret in host-only
telemt.toml; config.py.example (alexbers) removed. Telemt stays on json-file
(its tracing logs don't ship cleanly via the syslog driver).
Dashboard: retarget the working 3proxy queries from :1080 to the HTTP front
:3128 (the old HTTP section used stale tinyproxy patterns), drop the retired
SOCKS5 section, add a Telegram/Telemt info panel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retire the generic SOCKS5 :1080 (raw SOCKS5 is blocked by TSPU/DPI for the
external Telegram use case) and add ts-lan-mtproto (alexbers/mtprotoproxy) on
:1080 instead: a Fake-TLS MTProto proxy whose Telegram-DC connections chain
through the Tailscale userspace SOCKS5 (SOCKS5_HOST=ts-proxy:1055), so egress
still rides the Hetzner exit node. Client-facing hop is domestic Fake-TLS to
test the hypothesis that TSPU bites hardest at the border. 3proxy keeps only
the HTTP front (:3128). The proxy secret lives in a host-only config.py
(0600, uid 10000, not in git); config.py.example is the secret-free template.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retire the tinyproxy HTTP container (tinyproxy has no per-source-IP
conditional auth) and serve both the HTTP proxy (:3128) and SOCKS5 (:1080)
from a single 3proxy instance. LAN (192.168.0.0/22) stays passwordless;
any non-LAN source (i.e. external clients via the router port-forward) must
authenticate. Credentials live in a host-only /etc/3proxy/3proxy.secret
(0600, not in git), included by 3proxy.cfg. A deny rule on the gateway IP
makes the proxy fail closed if the port-forward ever SNATs to 192.168.0.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a LAN-facing SOCKS5 proxy alongside the existing tinyproxy HTTP proxy,
both egressing through the same Tailscale exit node.
- new service socks5 (ghcr.io/3proxy/3proxy): SOCKS5 on 192.168.0.35:1080,
auth iponly / allow 192.168.0.0/22, chained via `parent socks5 ts-proxy 1055`
to the Tailscale userspace SOCKS5, so clients exit as 89.167.72.79.
- 3proxy.cfg tracked here as source of truth; access log shipped to Loki via
the syslog log-driver (tag tailscale-socks5).
Verified: curl --socks5 / --socks5-hostname 192.168.0.35:1080 -> 89.167.72.79,
wikipedia 200. Carries TCP only (no QUIC/UDP), same as the HTTP front - README
updated to note this.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New stack tailscale-proxy: a LAN HTTP proxy (192.168.0.35:3128) that egresses
through the dedicated Hetzner Tailscale exit node fedora-technohim, so proxied
LAN clients appear on the internet as 89.167.72.79 without routing the docker
host itself through the tunnel.
- ts-proxy: Tailscale in userspace mode (--exit-node=100.121.234.85), exposing a
SOCKS5 proxy on :1055. Userspace mode makes no host route/firewall changes.
- tinyproxy: LAN-facing forward proxy on :3128 (Allow 192.168.0.0/22), upstream
socks5 to ts-proxy. Access log shipped to Alloy/Loki via the syslog
log-driver (tag tailscale-proxy).
The Tailscale auth key stays outside Git in /mnt/containers/tailscale-proxy/
ts.env (0600); node state persists in the state/ dir. tinyproxy.conf is tracked
here as source of truth. See tailscale-proxy/README.md.
Also adds logging/grafana-dashboards/tailscale-proxy.json: a Loki dashboard
(request rate by method, top destinations, top clients, totals, live access
log) built from the tinyproxy syslog stream, matching the existing dashboards'
schemaVersion 39 + ${datasource} template-var pattern.
Full build record in ops-knowledge
diagnostics/2026-07-23-02-tekhnohim-docker-tailscale-exit-proxy-stack.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>