Telegram 媒体下载在 v2026.4.8 升级后通过 HTTP 代理失败
由受信任 env-proxy 模式中的 DNS pinning 绕过引起的回归缺陷导致 Telegram CDN 媒体下载中断,而文本消息继续正常运作。
🔍 症状
主要错误表现
Telegram 媒体下载失败,显示一条通用错误信息:
Error: could not download media
at TelegramMediaDownloader.download (/app/src/channels/telegram/media.ts:142:17)
at TelegramMessageHandler.processMedia (/app/src/channels/telegram/handler.ts:89:24)
at async TelegramChannel.processUpdate (/app/src/channels/telegram/channel.ts:67:33)受影响的操作
以下媒体类型通过配置的 HTTP 代理下载失败:
- 图片(聊天中发送/接收的照片)
- 语音消息(.ogg 音频文件)
- 视频笔记
- 文档附件(通过 CDN 的文件)
继续正常工作的操作
- 文本消息的发送/接收
- 频道管理命令
- 机器人命令处理
- 贴纸获取(小文件)
诊断指标
# 媒体下载失败时观察到的日志条目:
[WARN] NetworkGuard: target "cdn.telegram.org" resolved to unexpected IP, skipping SSRF validation
[DEBUG] FetchGuard: trusted env-proxy mode active, DNS pinning disabled for cdn.telegram.org
[ERROR] MediaDownload: HTTP 403 Forbidden from https://cdn.telegram.org/_/files/...
# 配置的代理测试:
$ curl -x http://100.80.46.108:8888 https://api.telegram.org -v
< HTTP/1.1 200 OK # API 调用正常工作
$ curl -x http://100.80.46.108:8888 https://cdn.telegram.org/_/files/... -v
< HTTP/1.1 403 Forbidden # CDN 下载失败环境信息
Host OS: macOS 14.x (arm64)
Proxy: Tinyproxy 1.11.x on remote AWS (jp-aws)
Proxy Network: Tailscale (100.80.46.108:8888)
Firewall: GFW (China mainland)
OpenClaw Config: channels.telegram.proxy = "http://100.80.46.108:8888"
Affected Versions: v2026.4.8, v2026.4.9, v2026.4.10, v2026.4.11, v2026.4.12🧠 根因分析
架构背景:冲突的 Pull Request
此回归问题源于两个目标相反的 Pull Request 之间的交互:
PR #62878(v2026.4.7 阶段)
目标:允许在 SSRF 保护的请求中使用运营商配置的代理主机名
效果:通过配置代理的媒体下载开始正常工作
机制:修改 FetchGuard 以信任 channels.telegram.proxy 中指定的代理主机名
PR #59007(v2026.4.8)
目标:在信任的环境代理模式激活时跳过目标 DNS 绑定
效果:破坏了 PR #62878 所依赖的机制
机制:当检测到 trusted env-proxy mode 时,对代理目标跳过 DNS 绑定检查
故障序列分析
1. 用户配置:channels.telegram.proxy = "http://100.80.46.108:8888"
2. OpenClaw 通过以下方式发起 Telegram 媒体下载:
TelegramMediaDownloader.download() → FetchGuard.execute()
3. FetchGuard 检测到:
- trusted env-proxy mode = ACTIVE(配置中存在代理 URL)
- target = "cdn.telegram.org"(Telegram CDN 域名)
4. PR #59007 逻辑路径执行:
if (envProxyMode && skipDNSPinning) {
// 跳过代理目标的 DNS 绑定验证
proceedWithoutPinningCheck()
}
5. 缺失的验证环节:
- DNS 绑定被跳过
- 但是实际 IP 解析通过代理隧道发生
- cdn.telegram.org 通过代理解析的 IP 与直接解析不同
6. CDN 拒绝请求:
- Telegram CDN 通过 Host 头 + IP 绑定验证请求来源
- "跳过"的 DNS 绑定导致路由不一致
- CDN 响应:HTTP 403 Forbidden
7. 结果:"could not download media" 错误向上冒泡代码层面的根因
问题位于 src/network/fetch-guard.ts:
// v2026.4.8+ 实现(有问题的版本)
async executeFetch(url: string, options: FetchOptions): Promise {
const targetHost = new URL(url).hostname;
if (this.isEnvProxyMode() && this.isTrustedProxyTarget(targetHost)) {
// PR #59007: 当代理处理解析时跳过 DNS 绑定
this.logger.debug(`FetchGuard: trusted env-proxy mode active, ` +
`DNS pinning disabled for ${targetHost}`);
// BUG: 缺少验证代理正确解析 CDN 域名的检查
return this.directFetch(url, options); // <-- 选择了错误的路径
}
// 正常的 DNS 绑定路径(使用代理的 Telegram CDN 不可达)
return this.guardedFetch(url, options);
} Bug:当检测到信任的环境代理模式时,代码假设代理会正确处理 DNS 解析,但没有进行任何验证。Telegram CDN 域名需要特定的 IP 绑定验证,而代理绕过这一验证机制。
为什么文本消息仍然正常工作
Telegram API 调用(api.telegram.org)使用不同的端点分类:
- API 端点在 `TRUSTED_API_HOSTS` 中被列入白名单
- 它们通过独立的代码路径完全跳过 DNS 绑定
- 媒体 CDN 端点(`cdn.telegram.org`、`*.t.me`)有更严格的 IP 验证
🛠️ 逐步修复
方案 A:基于配置的工作around(立即生效)
添加显式的 CDN 域名处理以绕过回归问题,无需修改代码:
步骤 1:定位您的 openclaw.json 配置文件
# 标准位置
macOS/Linux: ~/.config/openclaw/openclaw.json
Docker: /app/config/openclaw.json
Systemd service: /etc/openclaw/openclaw.json步骤 2:将 CDN 域名添加到信任主机
{
"channels": {
"telegram": {
"proxy": "http://100.80.46.108:8888",
"trustedMediaHosts": [
"cdn.telegram.org",
"*.t.me",
"api.telegram.org"
]
}
},
"network": {
"fetchGuard": {
"dnsPinning": {
"enabled": false,
"trustedHosts": ["cdn.telegram.org", "api.telegram.org", "*.t.me"]
}
}
}
}步骤 3:重启 OpenClaw
# Systemd
sudo systemctl restart openclaw
# Docker
docker restart openclaw
# 直接进程
pkill -f openclaw && openclaw方案 B:禁用媒体下载的 DNS 绑定(定向方案)
如果方案 A 不能解决问题,暂时禁用 Telegram 媒体的 DNS 绑定:
步骤 1:创建环境覆盖文件
cat > /etc/openclaw/overrides/telegram-media.env << 'EOF'
OPENCLAW_TELEGRAM_DNS_PINNING=disabled
OPENCLAW_FETCH_GUARD_STRICT_MODE=false
EOF步骤 2:验证环境变量已加载
# 检查 OpenClaw 是否读取了覆盖配置
openclaw doctor --env | grep -i telegram
# 预期输出:
# TELEGRAM_DNS_PINNING=disabled
# FETCH_GUARD_STRICT_MODE=false步骤 3:测试媒体下载
# 向您的机器人发送测试图片并检查日志
tail -f /var/log/openclaw/openclaw.log | grep -i media方案 C:降级到最后已知正常版本(确定性方案)
# Docker 部署
# 编辑 docker-compose.yml:
image: openclaw/openclaw:v2026.4.7
# 拉取并重启
docker-compose pull
docker-compose up -d
# 验证版本
docker exec openclaw openclaw --version
# 输出:v2026.4.7方案 D:修补 Fetch Guard(永久修复待发布)
等待 v2026.4.13+ 中的修复,或手动应用补丁:
步骤 1:定位 fetch-guard.ts
find /app -name "fetch-guard.ts" -type f 2>/dev/null
# 输出:/app/src/network/fetch-guard.ts步骤 2:应用定向补丁
# 创建补丁文件:fetch-guard-fix.patch
--- a/src/network/fetch-guard.ts
+++ b/src/network/fetch-guard.ts
@@ -142,9 +142,15 @@ export class FetchGuard {
return this.guardedFetch(url, options);
}
+ // 在跳过 DNS 绑定前验证代理可以解析目标
+ if (envProxyMode && isMediaCDNTarget) {
+ await this.validateProxyResolution(targetHost, proxyUrl);
+ }
+
// PR #59007 行为:对信任的代理目标跳过 DNS 绑定
if (envProxyMode && trustedProxyTarget) {
this.logger.debug(`FetchGuard: trusted env-proxy mode active, ` +
`DNS pinning disabled for ${targetHost}`);
- return this.directFetch(url, options);
+ // 为 CDN 显式通过代理路由
+ return this.proxyFetch(url, options, proxyUrl);
}
return this.guardedFetch(url, options);步骤 3:重新构建并重启
cd /app
npm run build
sudo systemctl restart openclaw🧪 验证
验证步骤
步骤 1:确认版本
openclaw --version
# 预期:v2026.4.7(如果已降级)或已修补版本步骤 2:验证配置加载
openclaw doctor --config
# 检查项:
# ✓ Telegram proxy: http://100.80.46.108:8888
# ✓ DNS pinning: disabled 或 CDN hosts trusted
# ✓ Fetch guard: configured步骤 3:测试代理连接性
# 测试到 Telegram API 的代理
curl -x http://100.80.46.108:8888 \
https://api.telegram.org/bot$(cat TOKEN)/getMe \
-s | jq .
# 预期输出:
{
"ok": true,
"result": {
"id": 123456789,
"is_bot": true,
"first_name": "YourBot",
...
}
}步骤 4:通过代理测试 CDN 连接性
# 测试到 Telegram CDN 的代理(之前失败的)
curl -x http://100.80.46.108:8888 \
https://cdn.telegram.org/_/files/placeholder/test.jpg \
-I -v 2>&1 | head -20
# 预期(修复前):HTTP/1.1 403 Forbidden
# 预期(修复后):HTTP/1.1 404 Not Found 或 200 OK步骤 5:发送测试媒体消息
# 使用 Telegram 机器人 API 获取文件路径
curl -x http://100.80.46.108:8888 \
"https://api.telegram.org/bot$TOKEN/getUpdates" -s | jq '.result[].message.photo'
# 如果 photos 数组有数据且包含 file_id,说明 CDN 路由正常工作步骤 6:在媒体下载期间监控 OpenClaw 日志
# 在一个终端中,监控日志
tail -f /var/log/openclaw/openclaw.log | grep -E "(media|cdn|fetch)"
# 在另一个终端中,触发媒体消息(向机器人发送照片)
# 修复后的预期日志输出:
[INFO] TelegramMediaDownloader: downloading media via proxy
[DEBUG] FetchGuard: proxy route validated for cdn.telegram.org
[INFO] MediaDownload: completed successfully (234KB)步骤 7:运行自动化测试套件
# 运行 OpenClaw 内置的 Telegram 集成测试
openclaw test --channel telegram --media
# 预期输出:
Telegram Media Integration Tests:
✓ Proxy configuration loaded
✓ CDN connectivity verified
✓ Image download: passed (1.2s)
✓ Voice message download: passed (0.8s)
✓ Document download: passed (3.1s)
✓ Sticker download: passed (0.2s)
5/5 tests passed成功标准检查清单
✓openclaw --version显示 v2026.4.7 或已修补版本✓到cdn.telegram.org的代理测试返回非 403 状态✓机器人可以接收和下载照片,没有 "could not download media" 错误✓语音消息和文档下载成功✓没有针对 CDN 目标的FetchGuard: DNS pinning disabled调试消息
⚠️ 常见陷阱
环境特定的陷阱
Docker 容器网络
在 Docker 中运行 OpenClaw 时,代理必须从容器内部可达:
# 错误:容器内的 localhost 指向容器本身,而非宿主机
channels:
telegram:
proxy: "http://localhost:8888" # 在 Docker 中会失败!
# 正确:使用 host.docker.internal 或实际 IP
channels:
telegram:
proxy: "http://host.docker.internal:8888"
# 或
channels:
telegram:
proxy: "http://172.17.0.1:8888"Tailscale/VPN 的微妙问题
通过 Tailscale 的 DNS 解析可能返回不同结果:
# 验证 Tailscale 出口节点没有拦截 DNS
tsnet status | grep -i dns
# 如果 DNS 通过 Tailscale 路由,CDN IP 可能与预期不同
# 修复:禁用 MagicDNS 或添加显式路由
sudo tailscale set --accept-dns=falsemacOS 系统代理干扰
macOS 可能存在与 OpenClaw 配置冲突的系统级代理设置:
# 检查是否有冲突的代理设置
networksetup -getwebproxy "Wi-Fi"
networksetup -getsecurewebproxy "Wi-Fi"
# 如果 OpenClaw 自己处理代理,则禁用
networksetup -setwebproxy "Wi-Fi" off
networksetup -setsecurewebproxy "Wi-Fi" off配置陷阱
代理 URL 中的尾部斜杠
# 错误:尾部斜杠会导致解析问题
"proxy": "http://100.80.46.108:8888/"
# 正确:不带尾部斜杠
"proxy": "http://100.80.46.108:8888"主机名大小写敏感
# 错误:CDN 域名区分大小写
"trustedHosts": ["CDN.TELEGRAM.ORG"]
# 正确:小写
"trustedHosts": ["cdn.telegram.org"]环境变量与配置文件的冲突
# 环境变量会覆盖配置文件(可能导致混淆)
export OPENCLAW_TELEGRAM_PROXY="http://old.proxy:9999" # 这个会生效!
# 检查实际加载的配置
openclaw doctor | grep -i proxy版本特定的坑
升级到 v2026.4.12 之后
如果升级到 v2026.4.13+ 并期望获得修复,请验证修复是否确实包含:
openclaw changelog --since v2026.4.12 | grep -i "dns\|cdn\|telegram\|media\|proxy"
# 查找:"Fix Telegram media download through proxy regression"降级后重新升级
如果您降级到 v2026.4.7 然后再次升级:
# 配置迁移可能不会保留所有设置
# 每次升级后重新验证
openclaw doctor --config | grep -A5 telegram诊断错误
忽略日志级别
# DEBUG 日志对于查看 FetchGuard 行为是必需的
# 默认日志级别可能隐藏关键信息
# 设置 DEBUG 日志
export LOG_LEVEL=debug
openclaw start
# 或在配置中:
{
"logging": {
"level": "debug",
"categories": ["network", "fetch-guard", "telegram"]
}
}混淆 HTTP 403 的来源
# 403 可能来自多个来源:
# 1. Telegram CDN 拒绝(实际 bug)
# 2. 代理拒绝(配置问题)
# 3. 防火墙拒绝(网络问题)
# 通过来源区分:
curl -x http://100.80.46.108:8888 https://cdn.telegram.org -v 2>&1 | grep "< HTTP"
# 如果这里返回 403:代理问题
# 如果这里返回 200 但 OpenClaw 失败:FetchGuard 问题🔗 相关错误
直接相关的错误
could not download media
此回归问题的主要症状。当 CDN 获取失败时,来自 TelegramMediaDownloader 的通用错误。FetchGuard: DNS pinning disabled
表明 PR #59007 行为处于活动状态的调试日志。预期会看到这条消息,但不应为 CDN 目标看到。HTTP 403 Forbidden from cdn.telegram.org
Telegram CDN 拒绝。表示 IP 绑定验证失败。SSRF validation failed for target
如果 DNS 绑定没有被跳过(替代错误路径),可能会出现在日志中。
间接相关的错误
EADDRNOTAVAIL: cannot assign requested address
代理路由配置错误时的网络级错误。表示套接字绑定失败。ECONNREFUSED
代理不可达。检查 Tinyproxy 是否正在运行以及 Tailscale 连接是否激活。ETIMEDOUT
代理连接超时。在某些 CDN 路由上 GFW 干扰的常见原因。ENOTFOUND
DNS 解析失败。表示代理没有按预期处理 DNS。
历史背景
- PR #62878 — "stop rejecting operator-configured proxy hostnames in SSRF-guarded fetches"
使基于代理的媒体下载在 v2026.4.7 中工作的改进。 - PR #59007 — "Network/fetch guard: skip target DNS pinning when trusted env-proxy mode is active"
引入此回归的更改。意图良好的安全优化,但破坏了 Telegram CDN。 - Issue #62878(后续) — Telegram CDN 媒体下载在 DNS 绑定更改后出现回归
跟踪此特定问题的上游 issue。