From 8f95385b8917d93276e9f32cd0b3a87530d9893f Mon Sep 17 00:00:00 2001 From: pipistrello Date: Fri, 24 Jul 2026 14:48:11 +0300 Subject: [PATCH] tailscale-proxy: switch Telegram proxy to Telemt; rework Grafana dashboard 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 --- .../grafana-dashboards/tailscale-proxy.json | 457 ++---------------- tailscale-proxy/docker-compose.yaml | 26 +- tailscale-proxy/mtproto/config.py.example | 34 -- tailscale-proxy/telemt/telemt.toml.example | 38 ++ 4 files changed, 103 insertions(+), 452 deletions(-) delete mode 100644 tailscale-proxy/mtproto/config.py.example create mode 100644 tailscale-proxy/telemt/telemt.toml.example diff --git a/logging/grafana-dashboards/tailscale-proxy.json b/logging/grafana-dashboards/tailscale-proxy.json index fb0c97b..f8fc760 100644 --- a/logging/grafana-dashboards/tailscale-proxy.json +++ b/logging/grafana-dashboards/tailscale-proxy.json @@ -1,13 +1,13 @@ { "uid": "tailscale-proxy", - "title": "Tailscale прокси — трафик LAN", + "title": "Tailscale прокси — HTTP (:3128) + Telegram (:1080)", "tags": [ "proxy", "tailscale", "loki" ], "schemaVersion": 39, - "version": 2, + "version": 3, "editable": true, "refresh": "30s", "time": { @@ -35,9 +35,8 @@ }, "panels": [ { - "id": 1, "type": "row", - "title": "HTTP-прокси (:3128) — tinyproxy", + "title": "HTTP-прокси (:3128) — 3proxy", "collapsed": false, "gridPos": { "h": 1, @@ -48,7 +47,6 @@ "panels": [] }, { - "id": 2, "type": "stat", "title": "Всего запросов", "datasource": { @@ -78,7 +76,7 @@ "defaults": { "color": { "mode": "fixed", - "fixedColor": "blue" + "fixedColor": "purple" }, "unit": "short" }, @@ -91,14 +89,13 @@ "uid": "${datasource}" }, "editorMode": "code", - "expr": "sum(count_over_time({source_ip=\"192.168.0.35\"} |= \"Request (file descriptor\" [$__range]))", + "expr": "sum(count_over_time({source_ip=\"192.168.0.35\"} |~ \" 3128 00000 \" [$__range]))", "refId": "A", "queryType": "instant" } ] }, { - "id": 3, "type": "stat", "title": "Уникальных клиентов", "datasource": { @@ -128,7 +125,7 @@ "defaults": { "color": { "mode": "fixed", - "fixedColor": "blue" + "fixedColor": "purple" }, "unit": "short" }, @@ -141,14 +138,13 @@ "uid": "${datasource}" }, "editorMode": "code", - "expr": "count(sum by (client)(count_over_time({source_ip=\"192.168.0.35\"} |= \"Connect (file descriptor\" | pattern \"<_>): \" [$__range])))", + "expr": "count(sum by (clientip)(count_over_time({source_ip=\"192.168.0.35\"} |~ \" 3128 00000 \" | pattern \" : <_>\" [$__range])))", "refId": "A", "queryType": "instant" } ] }, { - "id": 4, "type": "stat", "title": "Уникальных назначений", "datasource": { @@ -178,7 +174,7 @@ "defaults": { "color": { "mode": "fixed", - "fixedColor": "blue" + "fixedColor": "purple" }, "unit": "short" }, @@ -191,14 +187,13 @@ "uid": "${datasource}" }, "editorMode": "code", - "expr": "count(sum by (dest)(count_over_time({source_ip=\"192.168.0.35\"} |= \"Found upstream proxy\" | pattern \"<_>for \" [$__range])))", + "expr": "count(sum by (dest)(count_over_time({source_ip=\"192.168.0.35\"} |~ \" 3128 00000 \" | pattern \" <_>\" [$__range])))", "refId": "A", "queryType": "instant" } ] }, { - "id": 5, "type": "timeseries", "title": "Запросы по методам", "datasource": { @@ -207,7 +202,7 @@ }, "gridPos": { "h": 8, - "w": 24, + "w": 12, "x": 0, "y": 5 }, @@ -243,7 +238,7 @@ "uid": "${datasource}" }, "editorMode": "code", - "expr": "sum by (method)(count_over_time({source_ip=\"192.168.0.35\"} |= \"Request (file descriptor\" | pattern \"<_>): <_>\" [$__auto]))", + "expr": "sum by (method)(count_over_time({source_ip=\"192.168.0.35\"} |~ \" 3128 00000 \" | pattern \" <_>\" [$__auto]))", "refId": "A", "queryType": "range", "legendFormat": "{{method}}" @@ -251,385 +246,6 @@ ] }, { - "id": 6, - "type": "table", - "title": "Топ назначений", - "datasource": { - "type": "loki", - "uid": "${datasource}" - }, - "gridPos": { - "h": 9, - "w": 12, - "x": 0, - "y": 13 - }, - "options": { - "showHeader": true, - "sortBy": [ - { - "displayName": "запросов", - "desc": true - } - ] - }, - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "filterable": true - } - }, - "overrides": [] - }, - "transformations": [ - { - "id": "labelsToFields", - "options": {} - }, - { - "id": "organize", - "options": { - "excludeByName": { - "Time": true - }, - "renameByName": { - "dest": "dest", - "Value #A": "запросов", - "Value": "запросов" - } - } - } - ], - "targets": [ - { - "datasource": { - "type": "loki", - "uid": "${datasource}" - }, - "editorMode": "code", - "expr": "topk(15, sum by (dest)(count_over_time({source_ip=\"192.168.0.35\"} |= \"Found upstream proxy\" | pattern \"<_>for \" [$__range])))", - "refId": "A", - "queryType": "instant" - } - ] - }, - { - "id": 7, - "type": "table", - "title": "Топ клиентов", - "datasource": { - "type": "loki", - "uid": "${datasource}" - }, - "gridPos": { - "h": 9, - "w": 12, - "x": 12, - "y": 13 - }, - "options": { - "showHeader": true, - "sortBy": [ - { - "displayName": "запросов", - "desc": true - } - ] - }, - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "filterable": true - } - }, - "overrides": [] - }, - "transformations": [ - { - "id": "labelsToFields", - "options": {} - }, - { - "id": "organize", - "options": { - "excludeByName": { - "Time": true - }, - "renameByName": { - "client": "client", - "Value #A": "запросов", - "Value": "запросов" - } - } - } - ], - "targets": [ - { - "datasource": { - "type": "loki", - "uid": "${datasource}" - }, - "editorMode": "code", - "expr": "topk(15, sum by (client)(count_over_time({source_ip=\"192.168.0.35\"} |= \"Connect (file descriptor\" | pattern \"<_>): \" [$__range])))", - "refId": "A", - "queryType": "instant" - } - ] - }, - { - "id": 8, - "type": "logs", - "title": "Журнал доступа HTTP (live)", - "datasource": { - "type": "loki", - "uid": "${datasource}" - }, - "gridPos": { - "h": 9, - "w": 24, - "x": 0, - "y": 22 - }, - "options": { - "showTime": true, - "wrapLogMessage": false, - "enableLogDetails": true, - "dedupStrategy": "none", - "sortOrder": "Descending" - }, - "targets": [ - { - "datasource": { - "type": "loki", - "uid": "${datasource}" - }, - "editorMode": "code", - "expr": "{source_ip=\"192.168.0.35\"} |= \"Request (file descriptor\"", - "queryType": "range", - "refId": "A" - } - ] - }, - { - "id": 9, - "type": "row", - "title": "SOCKS5-прокси (:1080) — 3proxy", - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 31 - }, - "panels": [] - }, - { - "id": 10, - "type": "stat", - "title": "Всего соединений", - "datasource": { - "type": "loki", - "uid": "${datasource}" - }, - "gridPos": { - "h": 4, - "w": 8, - "x": 0, - "y": 32 - }, - "options": { - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "colorMode": "value", - "graphMode": "none", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "fixed", - "fixedColor": "purple" - }, - "unit": "short" - }, - "overrides": [] - }, - "targets": [ - { - "datasource": { - "type": "loki", - "uid": "${datasource}" - }, - "editorMode": "code", - "expr": "sum(count_over_time({source_ip=\"192.168.0.35\"} |~ \" 1080 [0-9]{5} \" [$__range]))", - "refId": "A", - "queryType": "instant" - } - ] - }, - { - "id": 11, - "type": "stat", - "title": "Уникальных клиентов", - "datasource": { - "type": "loki", - "uid": "${datasource}" - }, - "gridPos": { - "h": 4, - "w": 8, - "x": 8, - "y": 32 - }, - "options": { - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "colorMode": "value", - "graphMode": "none", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "fixed", - "fixedColor": "purple" - }, - "unit": "short" - }, - "overrides": [] - }, - "targets": [ - { - "datasource": { - "type": "loki", - "uid": "${datasource}" - }, - "editorMode": "code", - "expr": "count(sum by (clientip)(count_over_time({source_ip=\"192.168.0.35\"} |~ \" 1080 [0-9]{5} \" | pattern \" : <_>\" [$__range])))", - "refId": "A", - "queryType": "instant" - } - ] - }, - { - "id": 12, - "type": "stat", - "title": "Уникальных назначений", - "datasource": { - "type": "loki", - "uid": "${datasource}" - }, - "gridPos": { - "h": 4, - "w": 8, - "x": 16, - "y": 32 - }, - "options": { - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "colorMode": "value", - "graphMode": "none", - "textMode": "auto", - "justifyMode": "auto" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "fixed", - "fixedColor": "purple" - }, - "unit": "short" - }, - "overrides": [] - }, - "targets": [ - { - "datasource": { - "type": "loki", - "uid": "${datasource}" - }, - "editorMode": "code", - "expr": "count(sum by (dest)(count_over_time({source_ip=\"192.168.0.35\"} |~ \" 1080 [0-9]{5} \" | pattern \"<_> CONNECT \" | dest != \"0.0.0.0:0\" [$__range])))", - "refId": "A", - "queryType": "instant" - } - ] - }, - { - "id": 13, - "type": "timeseries", - "title": "Соединения во времени", - "datasource": { - "type": "loki", - "uid": "${datasource}" - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 36 - }, - "fieldConfig": { - "defaults": { - "custom": { - "drawStyle": "line", - "fillOpacity": 15, - "stacking": { - "mode": "none", - "group": "A" - } - }, - "unit": "cps" - }, - "overrides": [] - }, - "options": { - "legend": { - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "multi", - "sort": "desc" - } - }, - "targets": [ - { - "datasource": { - "type": "loki", - "uid": "${datasource}" - }, - "editorMode": "code", - "expr": "sum(count_over_time({source_ip=\"192.168.0.35\"} |~ \" 1080 [0-9]{5} \" [$__auto]))", - "refId": "A", - "queryType": "range", - "legendFormat": "соединения" - } - ] - }, - { - "id": 14, "type": "timeseries", "title": "Трафик клиенту (байт/интервал)", "datasource": { @@ -640,7 +256,7 @@ "h": 8, "w": 12, "x": 12, - "y": 36 + "y": 5 }, "fieldConfig": { "defaults": { @@ -674,7 +290,7 @@ "uid": "${datasource}" }, "editorMode": "code", - "expr": "sum(sum_over_time({source_ip=\"192.168.0.35\"} |~ \" 1080 [0-9]{5} \" | pattern \" <_>\" | unwrap bout [$__auto]))", + "expr": "sum(sum_over_time({source_ip=\"192.168.0.35\"} |~ \" 3128 00000 \" | pattern \" \" | unwrap bout [$__auto]))", "refId": "A", "queryType": "range", "legendFormat": "байт" @@ -682,7 +298,6 @@ ] }, { - "id": 15, "type": "table", "title": "Топ назначений", "datasource": { @@ -693,7 +308,7 @@ "h": 9, "w": 12, "x": 0, - "y": 44 + "y": 13 }, "options": { "showHeader": true, @@ -739,14 +354,13 @@ "uid": "${datasource}" }, "editorMode": "code", - "expr": "topk(15, sum by (dest)(count_over_time({source_ip=\"192.168.0.35\"} |~ \" 1080 [0-9]{5} \" | pattern \"<_> CONNECT \" | dest != \"0.0.0.0:0\" [$__range])))", + "expr": "topk(15, sum by (dest)(count_over_time({source_ip=\"192.168.0.35\"} |~ \" 3128 00000 \" | pattern \" <_>\" [$__range])))", "refId": "A", "queryType": "instant" } ] }, { - "id": 16, "type": "table", "title": "Топ клиентов", "datasource": { @@ -757,7 +371,7 @@ "h": 9, "w": 12, "x": 12, - "y": 44 + "y": 13 }, "options": { "showHeader": true, @@ -803,25 +417,24 @@ "uid": "${datasource}" }, "editorMode": "code", - "expr": "topk(15, sum by (clientip)(count_over_time({source_ip=\"192.168.0.35\"} |~ \" 1080 [0-9]{5} \" | pattern \" : <_>\" [$__range])))", + "expr": "topk(15, sum by (clientip)(count_over_time({source_ip=\"192.168.0.35\"} |~ \" 3128 00000 \" | pattern \" : <_>\" [$__range])))", "refId": "A", "queryType": "instant" } ] }, { - "id": 17, "type": "logs", - "title": "Журнал доступа SOCKS5 (live)", + "title": "Журнал доступа HTTP (live)", "datasource": { "type": "loki", "uid": "${datasource}" }, "gridPos": { - "h": 9, + "h": 10, "w": 24, "x": 0, - "y": 53 + "y": 22 }, "options": { "showTime": true, @@ -837,11 +450,39 @@ "uid": "${datasource}" }, "editorMode": "code", - "expr": "{source_ip=\"192.168.0.35\"} |~ \" 1080 [0-9]{5} \"", + "expr": "{source_ip=\"192.168.0.35\"} |~ \" 3128 [0-9]{5} \"", "queryType": "range", "refId": "A" } ] + }, + { + "type": "row", + "title": "Telegram MTProto (:1080) — Telemt", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 32 + }, + "panels": [] + }, + { + "type": "text", + "title": "Telegram-прокси (Telemt) — наблюдение", + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 33 + }, + "options": { + "mode": "markdown", + "content": "### Telegram MTProto прокси — `ts-lan-telemt` (Telemt), порт `192.168.0.35:1080`\n\nFake-TLS MTProto-прокси для Telegram. Егресс к Telegram DC идёт через `ts-proxy` (Tailscale userspace SOCKS5) → выходной узел Hetzner `89.167.72.79`.\n\nЛоги Telemt **не** отправляются в Loki (формат tracing плохо ложится в syslog-драйвер), поэтому здесь панелей нет. Наблюдать за ним:\n\n- `docker logs ts-lan-telemt` на docker-хосте (подключения `peer=`, `user=tekhnohim`, трафик `c2s_bytes/s2c_bytes`, ошибки апстрима к DC);\n- при необходимости включить Prometheus-метрики Telemt (`[server] metrics_port`) и снимать их Zabbix-ом, как на Hetzner.\n\nВнешний вход `p.technohim.com:1080` (роутер Kirochnaya, port-forward + NAT-loopback для LAN). HTTP-трафик Telegram API (`149.154.167.x`) виден и в панелях HTTP-прокси выше." + }, + "pluginVersion": "", + "datasource": null } ] } \ No newline at end of file diff --git a/tailscale-proxy/docker-compose.yaml b/tailscale-proxy/docker-compose.yaml index 98945cb..4b9d058 100644 --- a/tailscale-proxy/docker-compose.yaml +++ b/tailscale-proxy/docker-compose.yaml @@ -36,21 +36,27 @@ services: tag: tailscale-proxy networks: [proxynet] - # Telegram-only Fake-TLS MTProto proxy on :1080 (replaces the TSPU-blocked - # generic SOCKS5). Chains its Telegram-DC connections through the Tailscale - # userspace SOCKS5 (SOCKS5_HOST=ts-proxy:1055 in config.py) so egress still - # rides the Hetzner exit node. Client-facing hop is domestic Fake-TLS; the - # foreign leg is the WireGuard tunnel that already carries the HTTP proxy. - # config.py is host-only (0600, uid 10000, NOT in git) - holds the secret. - mtproto: - image: alexbers/mtprotoproxy:latest - container_name: ts-lan-mtproto + # Telegram Fake-TLS MTProto proxy on :1080. Uses Telemt (the same proven + # implementation as the Hetzner endpoint) after alexbers/mtprotoproxy's + # Fake-TLS handshake was rejected by real clients (see the mtproto diagnostic). + # Only DC-bound traffic is chained out through the Tailscale userspace SOCKS5 + # ([[upstreams]] socks5 ts-proxy:1055 in telemt.toml), so egress rides the + # Hetzner exit node; the client-facing hop is direct Fake-TLS. + # telemt.toml is host-only (0600, uid 65532/nonroot, NOT in git) - holds the secret. + telemt: + image: ghcr.io/telemt/telemt:3.4.25 + container_name: ts-lan-telemt restart: unless-stopped depends_on: [ts-proxy] + environment: + NO_COLOR: "1" # cleaner `docker logs` (no ANSI escapes) volumes: - - "/mnt/containers/tailscale-proxy/mtproto/config.py:/home/tgproxy/config.py:ro,z" + - "/mnt/containers/tailscale-proxy/telemt/telemt.toml:/app/config.toml:ro,z" ports: - "192.168.0.35:1080:1080" + # Stays on json-file: telemt's tracing logs don't ship cleanly over the + # syslog driver, and `docker logs ts-lan-telemt` is worth keeping for this + # critical service. Observe it there (or wire its Prometheus metrics later). networks: [proxynet] networks: diff --git a/tailscale-proxy/mtproto/config.py.example b/tailscale-proxy/mtproto/config.py.example deleted file mode 100644 index aad14c6..0000000 --- a/tailscale-proxy/mtproto/config.py.example +++ /dev/null @@ -1,34 +0,0 @@ -# alexbers/mtprotoproxy config for the tekhnohim Telegram Fake-TLS MTProto proxy. -# -# The REAL config.py is host-only at /mnt/containers/tailscale-proxy/mtproto/config.py -# (mode 0600, owned by uid 10000 = the container's `tgproxy` user, NOT in git), -# because USERS holds the proxy secret. This file is the secret-free template. -# -# Egress: SOCKS5_HOST/SOCKS5_PORT point at the Tailscale userspace SOCKS5 -# (ts-proxy:1055), so every Telegram-DC connection exits via the Hetzner exit -# node (89.167.72.79). The client-facing side is Fake-TLS (looks like HTTPS to -# TLS_DOMAIN), which survives TSPU where the retired raw SOCKS5 did not. - -PORT = 1080 - -# username -> 32-hex-char secret (16 bytes). Generate: openssl rand -hex 16 -USERS = { - "tekhnohim": "00000000000000000000000000000000", -} - -# Fake-TLS only (best obfuscation). classic/secure are easier to fingerprint. -MODES = { - "classic": False, - "secure": False, - "tls": True, -} - -# SNI presented in the Fake-TLS handshake. Must be a real, reachable HTTPS host. -# Tunable if a given domain is filtered on the client path. -TLS_DOMAIN = "www.google.com" - -# Upstream SOCKS5 = the Tailscale userspace proxy -> Hetzner exit node. -SOCKS5_HOST = "ts-proxy" -SOCKS5_PORT = 1055 -SOCKS5_USER = None -SOCKS5_PASS = None diff --git a/tailscale-proxy/telemt/telemt.toml.example b/tailscale-proxy/telemt/telemt.toml.example new file mode 100644 index 0000000..7c0e5cf --- /dev/null +++ b/tailscale-proxy/telemt/telemt.toml.example @@ -0,0 +1,38 @@ +# Telemt config for the tekhnohim Telegram Fake-TLS MTProto proxy (docker host). +# +# The REAL telemt.toml is host-only at +# /mnt/containers/tailscale-proxy/telemt/telemt.toml (mode 0600, owned by the +# image's nonroot uid 65532, NOT in git) because [access.users] holds the +# proxy secret. This is the secret-free template. +# +# Egress: [[upstreams]] socks5 -> ts-proxy:1055 (Tailscale userspace SOCKS5), +# so ONLY Telegram-DC traffic exits via the Hetzner exit node (89.167.72.79). +# The client-facing side is direct Fake-TLS on :1080. + +# Direct-to-DC mode: with a SOCKS5 upstream, only DC-bound traffic is chained, +# so use_middle_proxy MUST be false (middle-proxy/ME servers are not routed +# through the upstream and fail to init behind the Tailscale SOCKS5). +[general] +use_middle_proxy = false + +[general.modes] +classic = false +secure = false +tls = true # ee / FakeTLS + +[server] +port = 1080 + +[censorship] +tls_domain = "www.google.com" # SNI presented in the Fake-TLS handshake; tunable + +[access.users] +# username = 32-hex-char secret (16 bytes). Generate: openssl rand -hex 16 +tekhnohim = "00000000000000000000000000000000" + +# Chain Telegram-DC connections out through the Tailscale userspace SOCKS5. +[[upstreams]] +type = "socks5" +address = "ts-proxy:1055" +weight = 1 +enabled = true