diff --git a/logging/grafana-dashboards/tailscale-proxy.json b/logging/grafana-dashboards/tailscale-proxy.json new file mode 100644 index 0000000..93404f1 --- /dev/null +++ b/logging/grafana-dashboards/tailscale-proxy.json @@ -0,0 +1,403 @@ +{ + "uid": "tailscale-proxy", + "title": "Tailscale прокси — трафик LAN", + "tags": [ + "proxy", + "tailscale", + "loki" + ], + "schemaVersion": 39, + "version": 1, + "editable": true, + "refresh": "30s", + "time": { + "from": "now-6h", + "to": "now" + }, + "timezone": "", + "templating": { + "list": [ + { + "name": "datasource", + "type": "datasource", + "query": "loki", + "current": {}, + "hide": 0, + "refresh": 1, + "includeAll": false, + "multi": false, + "regex": "" + } + ] + }, + "annotations": { + "list": [] + }, + "panels": [ + { + "id": 1, + "type": "stat", + "title": "Всего запросов", + "datasource": { + "type": "loki", + "uid": "${datasource}" + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 0, + "y": 0 + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "value", + "graphMode": "none", + "textMode": "auto", + "justifyMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "fixed", + "fixedColor": "blue" + }, + "unit": "short" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(count_over_time({source_ip=\"192.168.0.35\"} |= \"Request (file descriptor\" [$__range]))", + "refId": "A", + "queryType": "instant" + } + ] + }, + { + "id": 2, + "type": "stat", + "title": "Уникальных клиентов", + "datasource": { + "type": "loki", + "uid": "${datasource}" + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 8, + "y": 0 + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "value", + "graphMode": "none", + "textMode": "auto", + "justifyMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "fixed", + "fixedColor": "blue" + }, + "unit": "short" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "count(sum by (client)(count_over_time({source_ip=\"192.168.0.35\"} |= \"Connect (file descriptor\" | pattern \"<_>): \" [$__range])))", + "refId": "A", + "queryType": "instant" + } + ] + }, + { + "id": 3, + "type": "stat", + "title": "Уникальных назначений", + "datasource": { + "type": "loki", + "uid": "${datasource}" + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 16, + "y": 0 + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "value", + "graphMode": "none", + "textMode": "auto", + "justifyMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "fixed", + "fixedColor": "blue" + }, + "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\"} |= \"Found upstream proxy\" | pattern \"<_>for \" [$__range])))", + "refId": "A", + "queryType": "instant" + } + ] + }, + { + "id": 4, + "type": "timeseries", + "title": "Запросы по методам", + "datasource": { + "type": "loki", + "uid": "${datasource}" + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 4 + }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "fillOpacity": 15, + "stacking": { + "mode": "normal", + "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 by (method)(count_over_time({source_ip=\"192.168.0.35\"} |= \"Request (file descriptor\" | pattern \"<_>): <_>\" [$__auto]))", + "refId": "A", + "queryType": "range", + "legendFormat": "{{method}}" + } + ] + }, + { + "id": 5, + "type": "table", + "title": "Топ назначений", + "datasource": { + "type": "loki", + "uid": "${datasource}" + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 12 + }, + "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": 6, + "type": "table", + "title": "Топ клиентов", + "datasource": { + "type": "loki", + "uid": "${datasource}" + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 12 + }, + "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": 7, + "type": "logs", + "title": "Журнал доступа (live)", + "datasource": { + "type": "loki", + "uid": "${datasource}" + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 21 + }, + "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" + } + ] + } + ] +} \ No newline at end of file diff --git a/tailscale-proxy/README.md b/tailscale-proxy/README.md new file mode 100644 index 0000000..002111e --- /dev/null +++ b/tailscale-proxy/README.md @@ -0,0 +1,34 @@ +# Tailscale exit-node LAN proxy + +LAN HTTP proxy that egresses through the dedicated Hetzner Tailscale exit node, +so proxied clients appear on the internet as `89.167.72.79` without routing the +docker host itself through the tunnel. + +- client endpoint: `192.168.0.35:3128/tcp` (HTTP/HTTPS forward proxy); +- `tinyproxy` accepts LAN clients (`Allow 192.168.0.0/22`) and forwards upstream + over SOCKS5 to the Tailscale container; +- `ts-proxy` runs Tailscale in **userspace** mode and sends outbound traffic + through the exit node `fedora-technohim` (`100.121.234.85`); +- userspace mode adds **no host route/firewall changes** — the docker host's own + services (NPM, KMS, IPsec) keep egressing via the normal gateway; +- tinyproxy's access log ships to the logging stack (Alloy → Loki) via the + syslog log-driver (`tag: tailscale-proxy`); the Grafana dashboard + `logging/grafana-dashboards/tailscale-proxy.json` visualises it. + +The stack deliberately keeps credential-bearing / stateful files outside Git: + +| Host path | Purpose | Required mode | +|---|---|---| +| `/mnt/containers/tailscale-proxy/ts.env` | `TS_AUTHKEY=` | `0600`, owner `root:root` | +| `/mnt/containers/tailscale-proxy/state/` | Tailscale node state (persists identity across restarts) | dir, owner `root:root` | +| `/mnt/containers/tailscale-proxy/tinyproxy.conf` | tinyproxy config (also tracked in this directory as the source of truth) | `0644` | + +Before deploying: create `ts.env` on the host with a Tailscale auth key, and in +the Tailscale admin console approve the exit node and allow this node to use it. +Deploy this directory as a Portainer Git stack named `tailscale-proxy`, or run it +with Docker Compose using project name `tailscale-proxy`. The absolute +configuration files must already exist on the host before deployment. + +Full build record and rationale (userspace vs TUN dead-ends, SOCKS5-vs-HTTP, +monitoring) is in ops-knowledge: +`diagnostics/2026-07-23-02-tekhnohim-docker-tailscale-exit-proxy-stack.md`. diff --git a/tailscale-proxy/docker-compose.yaml b/tailscale-proxy/docker-compose.yaml new file mode 100644 index 0000000..f5db4c3 --- /dev/null +++ b/tailscale-proxy/docker-compose.yaml @@ -0,0 +1,35 @@ +services: + ts-proxy: + image: tailscale/tailscale:v1.98.9 + container_name: ts-proxy + hostname: technohim-lan-proxy + restart: unless-stopped + env_file: ["/mnt/containers/tailscale-proxy/ts.env"] + environment: + TS_HOSTNAME: technohim-lan-proxy + TS_STATE_DIR: /var/lib/tailscale + TS_USERSPACE: "true" + TS_ACCEPT_DNS: "false" + TS_EXTRA_ARGS: "--exit-node=100.121.234.85 --reset" + TS_TAILSCALED_EXTRA_ARGS: "--socks5-server=0.0.0.0:1055 --outbound-http-proxy-listen=0.0.0.0:1056" + volumes: ["/mnt/containers/tailscale-proxy/state:/var/lib/tailscale:z"] + networks: [proxynet] + + tinyproxy: + image: vimagick/tinyproxy:latest + container_name: ts-lan-proxy + restart: unless-stopped + depends_on: [ts-proxy] + volumes: ["/mnt/containers/tailscale-proxy/tinyproxy.conf:/etc/tinyproxy/tinyproxy.conf:ro,z"] + ports: ["192.168.0.35:3128:3128"] + logging: + driver: syslog + options: + syslog-address: "udp://192.168.0.35:514" + syslog-format: rfc3164 + tag: tailscale-proxy + networks: [proxynet] + +networks: + proxynet: + name: tailscale-proxynet diff --git a/tailscale-proxy/tinyproxy.conf b/tailscale-proxy/tinyproxy.conf new file mode 100644 index 0000000..021c996 --- /dev/null +++ b/tailscale-proxy/tinyproxy.conf @@ -0,0 +1,7 @@ +Port 3128 +Listen 0.0.0.0 +Allow 192.168.0.0/22 +DisableViaHeader Yes +Timeout 600 +LogLevel Info +upstream socks5 ts-proxy:1055