feat(gateway): add scoped proxy egress deployment

This commit is contained in:
wmq
2026-08-01 19:53:29 +08:00
parent 06112602ec
commit cf25d71fcf
14 changed files with 114 additions and 4 deletions
+5
View File
@@ -2,6 +2,9 @@
LX_DB_PASSWORD=replace-with-strong-database-password LX_DB_PASSWORD=replace-with-strong-database-password
LX_JWT_SECRET=replace-with-at-least-32-random-characters LX_JWT_SECRET=replace-with-at-least-32-random-characters
LX_WEB_PORT=8088 LX_WEB_PORT=8088
DEBIAN_MIRROR=https://deb.debian.org/debian
DEBIAN_SECURITY_MIRROR=https://deb.debian.org/debian-security
FFMPEG_BINARIES_URL=https://github.com/eugeneware/ffmpeg-static/releases/download
# DeepSeek Anthropic-compatible API # DeepSeek Anthropic-compatible API
DEEPSEEK_API_KEY=replace-with-rotated-deepseek-key DEEPSEEK_API_KEY=replace-with-rotated-deepseek-key
@@ -20,6 +23,8 @@ TELEGRAM_WEBHOOK_SECRET=replace-with-random-webhook-secret
WHATSAPP_GATEWAY_TOKEN=replace-with-at-least-32-random-characters WHATSAPP_GATEWAY_TOKEN=replace-with-at-least-32-random-characters
WHATSAPP_WEBHOOK_SECRET=replace-with-another-32-random-secret WHATSAPP_WEBHOOK_SECRET=replace-with-another-32-random-secret
WHATSAPP_LOG_LEVEL=info WHATSAPP_LOG_LEVEL=info
# 可选。只影响 WhatsApp 网关出站,不设置系统或平台全局代理。
WHATSAPP_DEFAULT_PROXY_URL=
PUBLIC_WEBHOOK_BASE_URL=http://host.docker.internal:8088 PUBLIC_WEBHOOK_BASE_URL=http://host.docker.internal:8088
APP_PUBLIC_URL=http://127.0.0.1:8088 APP_PUBLIC_URL=http://127.0.0.1:8088
+1
View File
@@ -28,6 +28,7 @@ storage/
*.sqlite3 *.sqlite3
*.dump *.dump
*.sql *.sql
!telegram-gateway/docker/scripts/init-db.sql
*.rdb *.rdb
*.binlog *.binlog
+14
View File
@@ -2,6 +2,20 @@
本项目遵循语义化版本。早期版本由经过脱敏的本地源码快照恢复,因此使用带日期的 `snapshot-*` 标签;自 `v1.0.0` 起按正式发布流程维护。 本项目遵循语义化版本。早期版本由经过脱敏的本地源码快照恢复,因此使用带日期的 `snapshot-*` 标签;自 `v1.0.0` 起按正式发布流程维护。
## [Unreleased]
### 新增
- Telegram Gateway 支持环境级默认 SOCKS5/HTTP/MTProto 代理,账号专属绑定仍具有更高优先级。
- WhatsApp Gateway 支持环境级默认 HTTP/HTTPS 代理,账号专属代理仍可覆盖默认值。
- 默认代理仅作用于两个消息网关,不改变平台、邮箱、数据库或 Docker 的全局网络出口。
- Docker 构建支持可配置的 Debian 与 FFmpeg 下载镜像,适配受限网络环境。
### 修复
- 将 Telegram Gateway 首次启动所需的 PostgreSQL 初始化脚本纳入版本控制,避免空数据库因挂载目录而启动失败。
- Telegram Gateway 本机调试端口改为 `127.0.0.1:3010`,避免与 Gitea 的 `3000` 端口冲突并阻止公网直连管理接口。
## [1.1.0] - 2026-08-01 ## [1.1.0] - 2026-08-01
### 新增 ### 新增
+1
View File
@@ -1,4 +1,5 @@
FROM node:20-alpine AS web-build FROM node:20-alpine AS web-build
ARG FFMPEG_BINARIES_URL=https://github.com/eugeneware/ffmpeg-static/releases/download
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm ci RUN npm ci
+6
View File
@@ -19,10 +19,14 @@ services:
whatsapp-gateway: whatsapp-gateway:
build: build:
context: ./whatsapp-gateway context: ./whatsapp-gateway
args:
DEBIAN_MIRROR: ${DEBIAN_MIRROR:-https://deb.debian.org/debian}
DEBIAN_SECURITY_MIRROR: ${DEBIAN_SECURITY_MIRROR:-https://deb.debian.org/debian-security}
environment: environment:
PORT: 3200 PORT: 3200
DATA_DIR: /data DATA_DIR: /data
WHATSAPP_GATEWAY_TOKEN: ${WHATSAPP_GATEWAY_TOKEN:?WHATSAPP_GATEWAY_TOKEN is required} WHATSAPP_GATEWAY_TOKEN: ${WHATSAPP_GATEWAY_TOKEN:?WHATSAPP_GATEWAY_TOKEN is required}
WHATSAPP_DEFAULT_PROXY_URL: ${WHATSAPP_DEFAULT_PROXY_URL:-}
LOG_LEVEL: ${WHATSAPP_LOG_LEVEL:-info} LOG_LEVEL: ${WHATSAPP_LOG_LEVEL:-info}
volumes: volumes:
- lingxi_whatsapp_data:/data - lingxi_whatsapp_data:/data
@@ -89,6 +93,8 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
args:
FFMPEG_BINARIES_URL: ${FFMPEG_BINARIES_URL:-https://github.com/eugeneware/ffmpeg-static/releases/download}
ports: ports:
- "${LX_WEB_PORT:-8088}:80" - "${LX_WEB_PORT:-8088}:80"
depends_on: depends_on:
+20
View File
@@ -37,6 +37,24 @@ docker compose logs --tail=100 api whatsapp-gateway web
平台的 `TELEGRAM_GATEWAY_URL` 指向网关 `/api` 地址。生产环境应通过私有网络连接,不把 Gateway 管理接口直接暴露公网。详细配置见 [Telegram Gateway 接入指南](TELEGRAM-GATEWAY.md)。 平台的 `TELEGRAM_GATEWAY_URL` 指向网关 `/api` 地址。生产环境应通过私有网络连接,不把 Gateway 管理接口直接暴露公网。详细配置见 [Telegram Gateway 接入指南](TELEGRAM-GATEWAY.md)。
### 2.1 仅消息网关使用出站代理
生产环境可以创建一个不映射宿主机端口的专用 Docker 网络和 V2Ray/Xray 容器,并只将 Telegram、WhatsApp 网关接入该网络。不要设置 Docker Daemon、宿主机或平台 API 的全局 `HTTP_PROXY`/`HTTPS_PROXY`。
```env
# Telegram Gateway
TG_DEFAULT_PROXY_TYPE=socks5
TG_DEFAULT_PROXY_HOST=lingxi-egress
TG_DEFAULT_PROXY_PORT=10808
# 平台 WhatsApp Gateway
WHATSAPP_DEFAULT_PROXY_URL=http://lingxi-egress:10809
```
账号级代理优先于上述默认代理。代理容器应使用 `internal/external Docker network + no host ports` 的方式隔离;配置文件和认证信息仅保存在服务器受限目录,不进入 Git。上线前至少验证 Telegram API 与 WhatsApp Web 的 HTTPS 连接,并确认 API、数据库、邮箱和 AI 服务未配置全局代理变量。
若部署网络无法访问 Debian 官方源或 FFmpeg GitHub Release,可通过 `.env` 的 `DEBIAN_MIRROR`、`DEBIAN_SECURITY_MIRROR` 和 `FFMPEG_BINARIES_URL` 指定构建镜像;这些变量只影响镜像构建,不改变运行时出口。
## 3. 生产入口 ## 3. 生产入口
在 Compose 外层部署 Caddy、Nginx 或云负载均衡: 在 Compose 外层部署 Caddy、Nginx 或云负载均衡:
@@ -49,6 +67,8 @@ docker compose logs --tail=100 api whatsapp-gateway web
将 `APP_PUBLIC_URL` 和 `PUBLIC_WEBHOOK_BASE_URL` 改为正式 HTTPS 地址,随后重建 API。 将 `APP_PUBLIC_URL` 和 `PUBLIC_WEBHOOK_BASE_URL` 改为正式 HTTPS 地址,随后重建 API。
当前生产拓扑使用 Caddy 与 NPS 时,应在 NPS HTTP Host 中将域名指向平台内网 `8088`,Caddy 只反向代理到 NPS 的本机 HTTP 入口。修改前分别备份 NPS Host 配置和 Caddyfile,验证配置后再重启 NPS、平滑重载 Caddy。
## 4. 更新 ## 4. 更新
```bash ```bash
+1
View File
@@ -18,6 +18,7 @@ Copy-Item telegram-gateway/.env.example telegram-gateway/.env
- `JWT_SECRET`、`ADMIN_API_KEY`:分别生成强随机值,不得复用。 - `JWT_SECRET`、`ADMIN_API_KEY`:分别生成强随机值,不得复用。
- `DB_PASSWORD`:生产数据库强密码。 - `DB_PASSWORD`:生产数据库强密码。
- `CORS_ORIGINS`:仅填写受信任的管理入口;平台服务端调用不依赖浏览器跨域。 - `CORS_ORIGINS`:仅填写受信任的管理入口;平台服务端调用不依赖浏览器跨域。
- `TG_DEFAULT_PROXY_*`:可选的默认 TDLib 代理;账号绑定的专属代理优先。
生产环境不要使用示例中的 `postgres` 密码。`.env` 不得提交 Git。 生产环境不要使用示例中的 `postgres` 密码。`.env` 不得提交 Git。
+11
View File
@@ -1,5 +1,8 @@
# Application # Application
NODE_ENV=development NODE_ENV=development
# Optional image build mirrors for networks where deb.debian.org is unavailable.
DEBIAN_MIRROR=https://deb.debian.org/debian
DEBIAN_SECURITY_MIRROR=https://deb.debian.org/debian-security
PORT=3000 PORT=3000
APP_NAME=TelegramUserGateway APP_NAME=TelegramUserGateway
CORS_ORIGINS= CORS_ORIGINS=
@@ -29,6 +32,14 @@ TDLIB_PATH=/usr/local/lib/libtdjson.so
TG_API_ID=0 TG_API_ID=0
TG_API_HASH=your_api_hash_here TG_API_HASH=your_api_hash_here
# 可选默认代理。账号未绑定专属代理时使用;只影响 Telegram TDLib 出站。
TG_DEFAULT_PROXY_TYPE=
TG_DEFAULT_PROXY_HOST=
TG_DEFAULT_PROXY_PORT=
TG_DEFAULT_PROXY_USERNAME=
TG_DEFAULT_PROXY_PASSWORD=
TG_DEFAULT_PROXY_SECRET=
# Rate Limiting # Rate Limiting
RATE_LIMIT_DEFAULT_TTL=60000 RATE_LIMIT_DEFAULT_TTL=60000
RATE_LIMIT_DEFAULT_LIMIT=30 RATE_LIMIT_DEFAULT_LIMIT=30
+6
View File
@@ -1,5 +1,8 @@
FROM node:20-bookworm-slim AS builder FROM node:20-bookworm-slim AS builder
ARG DEBIAN_MIRROR=https://deb.debian.org/debian
ARG DEBIAN_SECURITY_MIRROR=https://deb.debian.org/debian-security
RUN sed -i "s|http://deb.debian.org/debian-security|${DEBIAN_SECURITY_MIRROR}|g; s|http://deb.debian.org/debian|${DEBIAN_MIRROR}|g" /etc/apt/sources.list.d/debian.sources
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends g++ make python3 \ && apt-get install -y --no-install-recommends g++ make python3 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
@@ -22,6 +25,9 @@ RUN npm prune --omit=dev
FROM node:20-bookworm-slim FROM node:20-bookworm-slim
ARG DEBIAN_MIRROR=https://deb.debian.org/debian
ARG DEBIAN_SECURITY_MIRROR=https://deb.debian.org/debian-security
RUN sed -i "s|http://deb.debian.org/debian-security|${DEBIAN_SECURITY_MIRROR}|g; s|http://deb.debian.org/debian|${DEBIAN_MIRROR}|g" /etc/apt/sources.list.d/debian.sources
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends tini ca-certificates \ && apt-get install -y --no-install-recommends tini ca-certificates \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
+10 -1
View File
@@ -30,9 +30,12 @@ services:
build: build:
context: .. context: ..
dockerfile: docker/Dockerfile dockerfile: docker/Dockerfile
args:
DEBIAN_MIRROR: ${DEBIAN_MIRROR:-https://deb.debian.org/debian}
DEBIAN_SECURITY_MIRROR: ${DEBIAN_SECURITY_MIRROR:-https://deb.debian.org/debian-security}
container_name: tgw-app container_name: tgw-app
ports: ports:
- '3000:3000' - '127.0.0.1:3010:3000'
env_file: env_file:
- ../.env - ../.env
environment: environment:
@@ -52,6 +55,12 @@ services:
TDLIB_PATH: /app/node_modules/@prebuilt-tdlib/linux-x64-glibc/libtdjson.so TDLIB_PATH: /app/node_modules/@prebuilt-tdlib/linux-x64-glibc/libtdjson.so
TG_API_ID: ${TG_API_ID} TG_API_ID: ${TG_API_ID}
TG_API_HASH: ${TG_API_HASH} TG_API_HASH: ${TG_API_HASH}
TG_DEFAULT_PROXY_TYPE: ${TG_DEFAULT_PROXY_TYPE:-}
TG_DEFAULT_PROXY_HOST: ${TG_DEFAULT_PROXY_HOST:-}
TG_DEFAULT_PROXY_PORT: ${TG_DEFAULT_PROXY_PORT:-}
TG_DEFAULT_PROXY_USERNAME: ${TG_DEFAULT_PROXY_USERNAME:-}
TG_DEFAULT_PROXY_PASSWORD: ${TG_DEFAULT_PROXY_PASSWORD:-}
TG_DEFAULT_PROXY_SECRET: ${TG_DEFAULT_PROXY_SECRET:-}
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
@@ -0,0 +1,8 @@
-- Telegram User Gateway - Database Initialization
-- This script runs on first PostgreSQL startup
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Create indexes will be handled by TypeORM synchronize in dev
-- or by migrations in production
@@ -171,7 +171,7 @@ export class TgAccountsService implements OnModuleInit {
const binding = await this.bindingRepo.findOne({ const binding = await this.bindingRepo.findOne({
where: { accountId, isActive: true }, where: { accountId, isActive: true },
}); });
if (!binding) return undefined; if (!binding) return this.getDefaultProxy();
const proxy = await this.proxyRepo const proxy = await this.proxyRepo
.createQueryBuilder('proxy') .createQueryBuilder('proxy')
@@ -179,7 +179,7 @@ export class TgAccountsService implements OnModuleInit {
.where('proxy.id = :id', { id: binding.proxyId }) .where('proxy.id = :id', { id: binding.proxyId })
.andWhere('proxy.isActive = :isActive', { isActive: true }) .andWhere('proxy.isActive = :isActive', { isActive: true })
.getOne(); .getOne();
if (!proxy) return undefined; if (!proxy) return this.getDefaultProxy();
return { return {
type: proxy.type, type: proxy.type,
@@ -190,4 +190,27 @@ export class TgAccountsService implements OnModuleInit {
secret: proxy.secret, secret: proxy.secret,
}; };
} }
private getDefaultProxy() {
const host = String(process.env.TG_DEFAULT_PROXY_HOST || '').trim();
const port = Number(process.env.TG_DEFAULT_PROXY_PORT || 0);
if (!host || !Number.isInteger(port) || port < 1 || port > 65535) {
return undefined;
}
const type = String(process.env.TG_DEFAULT_PROXY_TYPE || 'socks5').toLowerCase();
if (!['socks5', 'http', 'mtproto'].includes(type)) {
this.logger.warn(`Ignoring unsupported TG_DEFAULT_PROXY_TYPE: ${type}`);
return undefined;
}
return {
type,
host,
port,
username: process.env.TG_DEFAULT_PROXY_USERNAME || '',
password: process.env.TG_DEFAULT_PROXY_PASSWORD || '',
secret: process.env.TG_DEFAULT_PROXY_SECRET || '',
};
}
} }
+4
View File
@@ -1,5 +1,9 @@
FROM node:20-bookworm-slim FROM node:20-bookworm-slim
ARG DEBIAN_MIRROR=https://deb.debian.org/debian
ARG DEBIAN_SECURITY_MIRROR=https://deb.debian.org/debian-security
RUN sed -i "s|http://deb.debian.org/debian-security|${DEBIAN_SECURITY_MIRROR}|g; s|http://deb.debian.org/debian|${DEBIAN_MIRROR}|g" /etc/apt/sources.list.d/debian.sources
ENV NODE_ENV=production ENV NODE_ENV=production
WORKDIR /app WORKDIR /app
+2 -1
View File
@@ -31,6 +31,7 @@ const accountsFile = path.join(dataDir, 'accounts.json')
const webhookDeliveriesFile = path.join(dataDir, 'webhook-deliveries.json') const webhookDeliveriesFile = path.join(dataDir, 'webhook-deliveries.json')
const outboundJobsFile = path.join(dataDir, 'outbound-jobs.json') const outboundJobsFile = path.join(dataDir, 'outbound-jobs.json')
const gatewayToken = String(process.env.WHATSAPP_GATEWAY_TOKEN || '') const gatewayToken = String(process.env.WHATSAPP_GATEWAY_TOKEN || '')
const defaultProxyUrl = String(process.env.WHATSAPP_DEFAULT_PROXY_URL || '').trim()
const maxMessagesPerChat = Math.max(2_000, Number(process.env.MAX_MESSAGES_PER_CHAT || 20_000)) const maxMessagesPerChat = Math.max(2_000, Number(process.env.MAX_MESSAGES_PER_CHAT || 20_000))
const log = pino({ level: process.env.LOG_LEVEL || 'info' }) const log = pino({ level: process.env.LOG_LEVEL || 'info' })
@@ -90,7 +91,7 @@ function accountWebhookSecret(account) {
} }
function accountProxyUrl(account) { function accountProxyUrl(account) {
return decryptCredential(account?.proxyUrlEncrypted || '') return decryptCredential(account?.proxyUrlEncrypted || '') || defaultProxyUrl
} }
function jsonSafe(value) { function jsonSafe(value) {