feat: sync bot setup and webhook diagnostics

This commit is contained in:
A 2026-07-22 00:51:52 +08:00
parent f53579416e
commit d9875b5caa
11 changed files with 521 additions and 29 deletions

View file

@ -77,7 +77,124 @@ This document describes every setting loaded by `internal/config`. Defaults and
| `TELESRV_TELEGRAM_LOGIN_SWEEP_INTERVAL` | duration / `5m` | Retention worker interval; bounded to `10s..1h`. |
| `TELESRV_TELEGRAM_LOGIN_SWEEP_BATCH` | int / `500` | Maximum rows per retention pass; bounded to `1..1000`. |
### 3.1 Complete Telegram Login / OIDC setup
### 3.1 Bot API webhook troubleshooting
Start by separating the three addresses below. Never use the webhook receiver domain as the Bot
API endpoint unless an explicit reverse-proxy route maps that domain to telesrv:
| Name | Setting/source | Direction and purpose |
|---|---|---|
| Bot API listener | telesrv `TELESRV_BOT_API_ADDR` | The telesrv bind address; empty disables the gateway. `0.0.0.0` is valid only for binding and is not a client request target. |
| Bot API base URL | the bot application's `TELEGRAM_API_URL` or equivalent | A client-reachable address for telesrv, for example `http://172.17.0.1:8088`. Method URLs are `<base>/bot<TOKEN>/<method>` and file URLs are `<base>/file/bot<TOKEN>/<file_path>`. |
| Webhook receiver URL | the bot application's `WEBHOOK_URL + WEBHOOK_PATH`, registered by `setWebhook` | The target to which telesrv actively POSTs updates, for example `https://bot.example.com/webhook`. It is not the Bot API base URL. |
The network direction is different too: polling is `bot application -> telesrv Bot API`, while
webhook delivery is `telesrv -> bot application webhook receiver`. Working polling proves only the
first path. It does not prove webhook DNS, outbound TCP, TLS, reverse proxy, or Docker hairpin
connectivity.
#### 1. Query the authoritative webhook state from the Bot API
Run this inside the bot application container with its actual Bot API base URL. Do not expand and
paste the token into chat, tickets, or screenshots:
```sh
curl -sS -X POST \
"${TELEGRAM_API_URL%/}/bot${BOT_TOKEN}/getWebhookInfo" | jq
```
If the application uses a differently named variable, replace `TELEGRAM_API_URL` with the
**client-reachable address** corresponding to `TELESRV_BOT_API_ADDR`. For example, if telesrv binds
`0.0.0.0:8088`, a container on the same host might use `http://172.17.0.1:8088`; it must not request
`http://0.0.0.0:8088`.
Interpret the result as follows:
| Result | Conclusion and next step |
|---|---|
| Empty `url` | No webhook is registered on this telesrv instance. Verify that the application uses this Bot API base URL and that startup `setWebhook` succeeded. |
| Increasing `pending_update_count` | Updates reached the telesrv durable queue but are not being delivered successfully. Inspect `last_error_message`. |
| HTTP `401`/`403` in `last_error_message` | The receiver is reachable, but its webhook secret differs or an authentication layer rejected the request. |
| `dial tcp ... i/o timeout` | telesrv cannot connect to the target IP/port. Check outbound firewall rules, Docker networking, loopback/hairpin NAT, and security groups. |
| `connection refused` | The address is reachable, but nothing listens on that port or the port mapping/reverse-proxy upstream is wrong. |
| DNS/`no such host` | The webhook hostname cannot be resolved from the telesrv runtime environment. |
| TLS/`x509` error | The certificate chain, hostname, SNI, or container CA trust is wrong. HTTPS uses the system trust store. |
| Target type absent from `allowed_updates` | Newly produced updates of that type are not queued. A normal `/start` requires at least `message`. |
| Pending reaches zero but the app does not react | telesrv received a 2xx response. Inspect the receiver's internal queue, workers, dispatcher, and handlers. |
`getWebhookInfo` reports telesrv's persisted delivery facts. An application `/health` endpoint only
proves that its receiver route and workers started; it cannot replace this check.
#### 2. Validate the receiver with the correct header
The Telegram webhook secret is distinct from the Bot token, OIDC Client Secret, and other API
keys. The receiver validates `X-Telegram-Bot-Api-Secret-Token`, not `Authorization: Bearer`:
```sh
curl -i -X POST "${WEBHOOK_URL%/}${WEBHOOK_PATH}" \
-H 'Content-Type: application/json' \
-H "X-Telegram-Bot-Api-Secret-Token: ${WEBHOOK_SECRET_TOKEN}" \
-d '{"update_id":2147483000}'
```
Expect an HTTP 2xx response. `401 invalid_secret_token` proves that the request reached the
application but the header was absent or did not match. Recreate/restart the application after
editing `.env`; changing the file alone neither updates the secret already registered in telesrv
nor the receiver process's startup-time secret.
#### 3. Test from the actual telesrv network namespace
A browser or official Telegram reaching the public webhook proves only public inbound
connectivity. Repeat the test from the host, container, or network namespace that actually runs
telesrv:
```sh
docker exec <telesrv-container> sh -lc \
'getent hosts bot.example.com; curl -vk --connect-timeout 10 https://bot.example.com/health/unified'
```
If public clients work but this returns `dial tcp ...:443: i/o timeout`, a same-host public-IP
hairpin failure is a common cause. Prefer split DNS or a container host mapping so the public
hostname resolves to the reverse proxy's internal entry point inside the telesrv container while
preserving the hostname, HTTPS SNI, and certificate validation. If the reverse proxy publishes
443 on the Docker host, test first with:
```sh
curl -vk --resolve bot.example.com:443:172.17.0.1 \
https://bot.example.com/health/unified
```
After that succeeds, a deployment may use a network-appropriate Compose entry such as:
```yaml
extra_hosts:
- "bot.example.com:host-gateway"
```
Other fixes include attaching telesrv to the reverse proxy's Docker network, allowing the Docker
subnet to reach host port 443, or correcting cloud security-group/NAT hairpin rules. telesrv allows
an internal HTTP receiver, but use one only on a controlled shared network and only when the
application's `WEBHOOK_URL` is not also its public OIDC, payment, or media callback base. Do not
blindly replace a global public URL with an internal address to mask a routing problem.
#### 4. Close the loop after the fix
1. Restart the bot application so it calls `setWebhook` again with the current URL, secret, and
`allowed_updates`.
2. Send a new `/start` or press a callback button.
3. Call `getWebhookInfo` again. `pending_update_count` should fall to `0`, with no new
`last_error_date`.
4. Inspect telesrv Warning logs for `bot api webhook delivery failed`. The record contains
`bot_user_id`, `retry_in`, and the failure reason, but must not contain the webhook URL, Bot
token, or secret.
5. Confirm that the receiver recorded and processed the `update_id`. Delivery is at-least-once, so
the application must safely handle duplicate updates caused by retries.
Immediately rotate any Bot token, webhook secret, OIDC Client Secret, API key, or database
password exposed in shell history, chat, or screenshots. Keep only redacted diagnostics in support
material.
### 3.2 Complete Telegram Login / OIDC setup
#### 1. Generate `data/telegram-login` once
@ -154,8 +271,10 @@ and choose that bot. Initial setup returns:
- `Client Secret`: shown once, separate from the Bot API token, and meant to be saved immediately
in a secret manager.
Send each configuration command separately. This example runs the relying party at
`http://192.0.2.30:3000`:
After selecting a bot once, BotFather keeps that configuration session active; there is no need to
repeat `/setlogin` and the bot username for every change. Send commands one at a time or paste them
as separate lines in one message (up to 32 lines per message). This example runs the relying party
at `http://192.0.2.30:3000`:
```text
add origin http://192.0.2.30:3000
@ -164,6 +283,12 @@ algorithm RS256
enable
```
Send `/done` after the changes succeed. BotFather closes the session and returns the final
configuration summary. Every successful change takes effect immediately, so `/cancel` only closes
the session and does not roll back changes. If a multi-line message fails partway through,
BotFather identifies the applied lines, the failed line, and the later lines that were skipped,
then keeps the selected bot active for a corrected command.
An `origin` is an exact Web origin without a path, query, or fragment; it authorizes the JS SDK,
popup CORS, and legacy `login_url`. A `redirect` is the exact full URI that receives an
Authorization Code. Wildcards and prefix matching are not supported. Use `/logininfo` to inspect

View file

@ -77,7 +77,117 @@
| `TELESRV_TELEGRAM_LOGIN_SWEEP_INTERVAL` | duration / `5m` | retention worker 周期,限定 `10s..1h`。 |
| `TELESRV_TELEGRAM_LOGIN_SWEEP_BATCH` | int / `500` | 每轮最大清理行数,限定 `1..1000`。 |
### 3.1 Telegram Login / OIDC 完整启用流程
### 3.1 Bot API webhook 故障排查
先区分三个地址,禁止把 webhook 接收域名当成 Bot API 地址:
| 名称 | 配置/来源 | 方向与用途 |
|---|---|---|
| Bot API listener | telesrv 的 `TELESRV_BOT_API_ADDR` | telesrv 的监听地址;空值表示关闭。`0.0.0.0` 只能用于 bind不能作为客户端请求目标。 |
| Bot API base URL | bot 应用的 `TELEGRAM_API_URL` 等配置 | bot 应用访问 telesrv 的可达地址,例如 `http://172.17.0.1:8088`。方法地址为 `<base>/bot<TOKEN>/<method>`,文件地址为 `<base>/file/bot<TOKEN>/<file_path>`。 |
| Webhook receiver URL | bot 应用的 `WEBHOOK_URL + WEBHOOK_PATH`,经 `setWebhook` 登记 | telesrv 主动 POST update 的目标,例如 `https://bot.example.com/webhook`。它不是 Bot API base URL。 |
网络方向也不同polling 是 `bot 应用 -> telesrv Bot API`webhook 是
`telesrv -> bot 应用 webhook receiver`。因此 polling 正常只能证明前一条路径可达,
不能证明 webhook 的 DNS、出站 TCP、TLS、反向代理或 Docker hairpin 路径正常。
#### 1. 从 Bot API 查询真实 webhook 状态
应在 bot 应用容器中使用它实际配置的 Bot API base URL不要把 token 展开后粘贴到
聊天、工单或截图:
```sh
curl -sS -X POST \
"${TELEGRAM_API_URL%/}/bot${BOT_TOKEN}/getWebhookInfo" | jq
```
若没有 `TELEGRAM_API_URL` 这个变量,就把它替换成与
`TELESRV_BOT_API_ADDR` 对应的**客户端可达地址**。例如 telesrv 监听
`0.0.0.0:8088`,同宿主 Docker 容器可能使用 `http://172.17.0.1:8088`;不要请求
`http://0.0.0.0:8088`
按下表判读响应:
| 结果 | 结论与下一步 |
|---|---|
| `url` 为空 | webhook 没有登记到这台 telesrv检查 bot 应用是否确实使用该 Bot API base URL以及启动时 `setWebhook` 是否成功。 |
| `pending_update_count` 增长 | update 已进入 telesrv durable queue但没有成功交付继续看 `last_error_message`。 |
| `last_error_message` 为 HTTP `401`/`403` | 接收端已可达,但 webhook secret 不一致或请求被认证层拒绝。 |
| `dial tcp ... i/o timeout` | telesrv 到目标 IP/端口的连接超时检查出站防火墙、Docker 网络、回环 NAT/hairpin 和安全组。 |
| `connection refused` | 目标地址可达,但相应端口没有监听或端口映射/反代 upstream 错误。 |
| DNS/`no such host` | telesrv 所在运行环境无法解析 webhook hostname。 |
| TLS/`x509` 错误 | 证书链、hostname、SNI 或容器 CA trust 有问题。HTTPS 使用系统信任链。 |
| `allowed_updates` 不含目标类型 | 新产生的该类型 update 不会入队;普通 `/start` 至少需要 `message`。 |
| pending 归零但应用无响应 | telesrv 已收到 2xx转查接收应用内部 queue、worker、dispatcher 和 handler 日志。 |
`getWebhookInfo` 查询的是 telesrv 持久化的交付事实;应用自己的 `/health` 只能证明
接收路由和 worker 已启动,不能代替这一步。
#### 2. 用正确请求头验证接收端
Telegram webhook secret 与 Bot token、OIDC Client Secret、API key 都是不同凭据。
接收端校验的标准请求头是 `X-Telegram-Bot-Api-Secret-Token`,不是
`Authorization: Bearer`
```sh
curl -i -X POST "${WEBHOOK_URL%/}${WEBHOOK_PATH}" \
-H 'Content-Type: application/json' \
-H "X-Telegram-Bot-Api-Secret-Token: ${WEBHOOK_SECRET_TOKEN}" \
-d '{"update_id":2147483000}'
```
预期为 HTTP 2xx。`401 invalid_secret_token` 表示请求已经到达应用,但 header 缺失或
值不匹配。编辑 `.env` 后必须重建/重启读取该配置的应用;只修改磁盘文件不会更新
已经登记到 telesrv 的 secret也不会更新接收进程启动时捕获的 secret。
#### 3. 从 telesrv 的实际网络命名空间测试
浏览器或官方 Telegram 能访问公网 webhook只能证明公网入站正常。必须从实际运行
telesrv 的宿主机、容器或 network namespace 再测一次:
```sh
docker exec <telesrv-container> sh -lc \
'getent hosts bot.example.com; curl -vk --connect-timeout 10 https://bot.example.com/health/unified'
```
如果公网客户端正常而这里 `dial tcp ...:443: i/o timeout`,常见原因是同机公网 IP
回环失败。优先使用 split DNS 或容器 host mapping让公网 hostname 在 telesrv 容器
内解析到反向代理的内部入口,同时保留原 hostname、HTTPS SNI 和证书校验。例如反代
的 443 已发布到 Docker 宿主机时,可先验证:
```sh
curl -vk --resolve bot.example.com:443:172.17.0.1 \
https://bot.example.com/health/unified
```
验证通过后,可在 telesrv Compose 中使用与实际网络匹配的配置:
```yaml
extra_hosts:
- "bot.example.com:host-gateway"
```
其它可选修复包括:把 telesrv 接入反向代理所在 Docker network、为 Docker subnet
放行宿主机 443或修正云安全组/NAT hairpin。telesrv 允许登记内部 HTTP receiver
但只有在两端共享受控内网且调用方的 `WEBHOOK_URL` 不同时承担 OIDC、支付或公开媒体
回调时才应使用;不要为绕过网络问题盲目把应用的全局公开 URL 改成内部地址。
#### 4. 修复后的闭环验证
1. 重新启动 bot 应用,让它用当前 URL、secret 和 `allowed_updates` 再次调用
`setWebhook`
2. 发送一条新的 `/start` 或点击 callback 按钮。
3. 再次调用 `getWebhookInfo``pending_update_count` 应下降到 `0`,且不再出现新的
`last_error_date`
4. 检查 telesrv Warning 日志中的 `bot api webhook delivery failed`。日志包含
`bot_user_id``retry_in` 和失败原因,但不得记录 webhook URL、Bot token 或 secret。
5. 检查接收应用是否记录并处理该 `update_id`。webhook 是 at-least-once应用必须能
安全处理失败重试带来的重复 update。
若凭据曾出现在命令历史、聊天或截图中,立即轮换 Bot token、webhook secret、OIDC
Client Secret 及同屏暴露的其它 API key/数据库密码;排查资料只保留脱敏结果。
### 3.2 Telegram Login / OIDC 完整启用流程
#### 1. 一次性生成 `data/telegram-login`
@ -151,7 +261,9 @@ discovery 返回的 `issuer` 必须等于配置值,`authorization_endpoint`、
- `Client ID`bot user ID 的十进制字符串;
- `Client Secret`:只显示一次,与 Bot API token 不同,必须立即保存到密钥管理系统。
接着逐条发送配置命令。下面假设依赖方页面运行在 `http://192.0.2.30:3000`
选择一次 bot 后会持续停留在它的配置会话中,无需为每项修改重复 `/setlogin` 和 bot
username。可以逐条发送也可以像下面这样在一条消息中粘贴多行命令每条消息最多
32 行)。下面假设依赖方页面运行在 `http://192.0.2.30:3000`
```text
add origin http://192.0.2.30:3000
@ -160,6 +272,11 @@ algorithm RS256
enable
```
全部修改成功后发送 `/done`BotFather 会退出配置会话并返回最终配置摘要。各条修改会
立即生效;`/cancel` 只关闭当前会话,不会回滚已经成功的修改。多行消息若中途失败,
BotFather 会明确列出已应用项、失败行以及未执行的后续行,并保留当前 bot 选择供修正
后继续操作。
`origin` 只能是无 path/query/fragment 的精确 Web origin用于 JS SDK、popup CORS 和
legacy `login_url``redirect` 是 Authorization Code Flow 返回 code 的精确完整 URI。
不支持 wildcard 或 prefix 匹配。用 `/logininfo` 检查状态和登记值;用 `/setlogin`