OpenResty tp-* HTTP 代理入口部署
OpenResty tp-* HTTP 代理入口部署
本文讲怎么把 deploy/openresty/ 的产物装到生产 OpenResty 上,让 https://tp-<name>.<domain_suffix>
成为标准 HTTPS 代理入口。产物本身的用途、占位符清单与一致性测试见
deploy/openresty/README.md;使用者视角的各端配置与错误码见
HTTP 代理入口;既有 admin 与托管路由的 server 块见
Nginx 推荐配置。
为什么必须是 OpenResty
tp-* 代理入口只处理 HTTP/HTTPS/WebSocket,而浏览器与操作系统的 HTTPS 代理会先发 CONNECT。纯 nginx
做不到“按 SNI 选路由再把 CONNECT 搬进 Server”:CONNECT 跳过 location 匹配,proxy_pass 与
proxy_connect; 链式转发又会丢掉路由身份和 Proxy-Authorization。可行方案只有打过
ngx_http_proxy_connect_module 补丁的 OpenResty 内核 + server 级 access_by_lua_file,用补丁
注册的 $connect_host/$connect_port 变量读出隧道目标,再用 ngx.req.socket(true) 做双向 splice。
tp-* server 块与既有 admin/托管路由 server 块共用 443,由 SNI 分流,不新增公网监听端口。
适用前提
- OpenResty,内核 nginx >= 1.25.1,已编入
lua-nginx-module,并且已打proxy_connect_rewrite_102101.patch(对应 OpenResty 1.25.3.1 与模块 tagv0.0.7)。 - 通配证书覆盖
*.<domain_suffix>。 - 泛解析 DNS:
tp-*.<domain_suffix>指向入口 IP。 - 推荐 OpenResty 与
tunnelmesh-server同机部署,内部入口只绑回环;跨机部署见文末。
上线前必须做两级内核检查,两级都过才算具备条件:
# 第一级:模块有没有编进内核
nginx -V 2>&1 | tr ' ' '\n' | grep proxy_connect
# 第二级:补丁有没有真的打上(CONNECT 能不能进到 server 级 access_by_lua)
bash deploy/openresty/spike-connect-check.sh 18443
两级都要,是因为只看第一级会误判:模块单独 --add-module 编进去时 nginx -V 一样能看到
proxy_connect,但没打补丁的内核会在解析阶段就对每个 CONNECT 回 405,$connect_host 这个
变量也根本不存在,access_by_lua 永远收不到请求。第二级的判读方式写在脚本头部注释里:nc 侧收到
HTTP/1.1 200 Connection Established 且 error.log 里出现 spike connect_host=... 才算通过。
Server 侧开关
内部入口默认关闭,enabled=false 时行为与旧版本完全一致。最小配置:
server:
proxy_entry:
enabled: true
domain_suffix: tm.example.com
listen: 127.0.0.1:8089
trusted_proxies: ["127.0.0.1/32", "::1/128"]
环境变量等价写法:
export TUNNELMESH_SERVER_PROXY_ENTRY_ENABLED=true
export TUNNELMESH_SERVER_PROXY_ENTRY_DOMAIN_SUFFIX=tm.example.com
export TUNNELMESH_SERVER_PROXY_ENTRY_LISTEN=127.0.0.1:8089
全部 13 个键的默认值与含义见配置说明。改完 先校验再重启:
tunnelmesh-server --config /etc/tunnelmesh/server.yaml check-config
enabled=true 时 domain_suffix 必填。trusted_proxies 允许配置 0.0.0.0/0 或 ::/0,但
当 listen 不是回环地址时,这表示信任所有能访问该端口的主机,它们都可能伪造路由身份与来源 IP;
生产环境必须用防火墙、安全组或专线限制内部入口的访问范围。
渲染模板
模板是 deploy/openresty/tunnelmesh-proxy.conf.example,不能直接 include,必须先渲染 7 个占位符:
| 占位符 | 取值来源 | 生产示例 |
|---|---|---|
__LISTEN__ |
监听地址 | 443 |
__SERVER_NAME_REGEX__ |
由 server.proxy_entry.domain_suffix 推导的 tp-* 主机名正则 |
~^tp-[a-z0-9]([a-z0-9-]{0,30}[a-z0-9])?\.tm\.example\.com$ |
__SSL_CERT__ |
覆盖 *.<domain_suffix> 的通配证书 |
/data/ssl/tm.example.com_bundle.crt |
__SSL_CERT_KEY__ |
对应私钥,0600 | /data/ssl/tm.example.com.key |
__LUA_FILE__ |
搬运层 Lua 的绝对路径 | /etc/openresty/lua/tunnelmesh_proxy_entry.lua |
__INTERNAL_UPSTREAM__ |
必须等于 server.proxy_entry.listen |
127.0.0.1:8089 |
__EDGE_ALLOW__ |
粗粒度来源白名单指令 | allow 10.0.0.0/8; allow 11.0.0.0/8; deny all; |
Lua 侧另有 3 个行尾标记,只在内部入口地址或 idle_timeout 与默认值不同时才需要改:
| 行尾标记 | 取值来源 | 默认值 |
|---|---|---|
-- __TM_INTERNAL_HOST__ |
server.proxy_entry.listen 的 host |
127.0.0.1 |
-- __TM_INTERNAL_PORT__ |
server.proxy_entry.listen 的 port |
8089 |
-- __TM_READ_TIMEOUT_MS__ |
server.proxy_entry.idle_timeout + 30s,毫秒 |
330000 |
read_timeout_ms 必须比 Server 的 idle_timeout 长(默认 +30s),让 Server 先判定空闲并关闭,
Lua 只兜底;配反了会表现为 nginx 先掐断隧道,客户端看到莫名断开。这三项默认值与 Go 配置默认值的
一致性由 deploy/openresty/openresty_artifacts_test.go 守护,改配置默认值必须同步改 Lua。
渲染完成后的完整 server 块(后缀 tm.example.com,可直接对照):
upstream tunnelmesh_proxy_entry {
server 127.0.0.1:8089;
keepalive 32;
}
server {
listen 443 ssl;
server_name ~^tp-[a-z0-9]([a-z0-9-]{0,30}[a-z0-9])?\.tm\.example\.com$;
ssl_certificate /data/ssl/tm.example.com_bundle.crt;
ssl_certificate_key /data/ssl/tm.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:TLS:10m;
# 粗粒度前置,只挡明显的公网扫描;按路由的细粒度 ACL 在 Server 侧执行。
allow 10.0.0.0/8;
allow 11.0.0.0/8;
deny all;
# 客户端断开时立刻结束 Lua 请求,避免半开隧道占用 worker connection。
lua_check_client_abort on;
access_by_lua_file /etc/openresty/lua/tunnelmesh_proxy_entry.lua;
# 非 CONNECT(绝对形式)请求。nginx 会把绝对形式改写成 origin-form 再交给
# location,所以 Server 侧从 Host 头恢复目标(见 splitProxyTarget)。
location / {
proxy_pass http://tunnelmesh_proxy_entry;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $http_host;
proxy_set_header Proxy-Authorization $http_proxy_authorization;
proxy_set_header X-TunnelMesh-Route $ssl_server_name;
proxy_set_header X-TunnelMesh-Client-IP $remote_addr;
proxy_set_header X-TunnelMesh-Client-Port $remote_port;
proxy_request_buffering off;
proxy_buffering off;
proxy_connect_timeout 10s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
安装步骤
# 1. 搬运层
install -m 0644 deploy/openresty/tunnelmesh_proxy_entry.lua \
/etc/openresty/lua/tunnelmesh_proxy_entry.lua
# 2. 渲染模板(占位符替换方式自选,渲染结果建议纳入配置管理)
# 把渲染出的 upstream 与 server 块并入既有 nginx.conf 的 http{},
# 与 admin / 托管路由 server 块并列。
# 3. main 上下文加排空窗口(不能写在 server 块里)
# worker_shutdown_timeout 300s;
# 4. 校验并热加载
nginx -t
nginx -s reload
worker_shutdown_timeout 属于 main 上下文,写在 server 块里 nginx -t 会直接报错。它决定
reload 时在途隧道有多久排空窗口,缺省值会让发布瞬间所有代理连接被立刻掐断。
error_log 至少要 info 级,否则看不到隧道关闭统计(route/target/client/字节数/时长/结束原因),
排障时无从下手。
容器方式(可选)
docker build -f deploy/openresty/Dockerfile.proxy-connect \
-t tunnelmesh/openresty-proxy-connect:1.25.3.1 .
docker run --rm tunnelmesh/openresty-proxy-connect:1.25.3.1 -V 2>&1 \
| tr ' ' '\n' | grep proxy_connect
替换生产 OpenResty 之前,必须先在目标机执行 nginx -V,把输出里的 --with-* 参数补进
Dockerfile 的 ./configure,否则现有 server 块会因为缺模块起不来(Dockerfile 里默认只带了
ssl/v2/realip/gzip_static 与 pcre-jit,够跑冒烟但未必够跑你的生产配置)。
镜像默认以非 root 的 openresty 用户运行,因此只适合监听 1024 以上端口。要在容器里直接监听 443,
改用 root 运行,或者 setcap cap_net_bind_service=ep /usr/local/openresty/nginx/sbin/nginx。
验证
# ACL 内主机,凭据正确:出口 IP 应当属于 agent 所在网络
curl -sv --proxy-insecure -x https://tp-demo.tm.example.com \
--proxy-user 'u:p' https://ifconfig.me
--proxy-insecure 只用于自签证书场景,生产用正式通配证书时应去掉。三条预期结果:
| 场景 | 预期 |
|---|---|
| ACL 内 + 凭据正确 | 200,返回的出口 IP 属于出口 agent 所在网络 |
| ACL 外主机 | Received HTTP code 403 from proxy after CONNECT |
| 密码错误 | Received HTTP code 407 from proxy after CONNECT,响应带 Proxy-Authenticate: Basic realm="TunnelMesh", charset="UTF-8" |
创建或修改路由后不需要重启 Server,也不需要 reload nginx,5 秒内生效(沿用既有
loadManagedRoutes 快照 TTL)。
容量评估
每条隧道在 nginx 侧占用 1 个请求 + 2 个 socket(下游客户端 + 内部入口),在 Server 侧占用 1 条到 agent 的 stream。因此:
worker_connections与worker_rlimit_nofile按server.proxy_entry.max_concurrent_tunnels × 2起评,再叠加既有 admin/托管路由的量。- Server 先拒绝超限请求(503 +
Retry-After: 5,稳定错误码proxy_capacity_exhausted)。 nginx 侧的limit_conn只是兜底,不要配得比 Server 更紧,否则排障时看到的是 nginx 自己的 503 而不是稳定错误码,审计与指标里也不会有对应记录。 - 单路由的并发上限走后台的“并发上限”字段(
config.maxConcurrentTunnels),0 表示只受全局上限约束。
reload 与发布影响
nginx -s reload 会让旧 worker 停止接受新连接,在途隧道在 worker_shutdown_timeout 窗口内排空,
超时后被强制关闭;客户端表现为连接断开并重连。发布窗口建议:
- 选低峰期。
- 集群模式先扩 Server 节点,确认新节点 ready 再动 nginx。
nginx -t通过后再 reload;reload 后立刻看tunnelmesh_proxy_entry_tunnels_active是否回升。- Server 侧升级同理:
shutdown_timeout(默认 30s)内等待在途隧道排空,超时强制关闭。
回滚(5 分钟内)
两条独立的止损路径,任选其一,都不涉及数据回滚:
- 删 nginx 配置:把渲染进去的
upstream tunnelmesh_proxy_entry与 tp-*server块删掉,nginx -s reload。入口立刻消失,既有 admin 与托管路由不受影响。 - 关 Server 开关:
server.proxy_entry.enabled=false后重启 Server。内部入口关闭,nginx 侧 表现为 502(proxy_egress_unavailable),效果等价但会留下一层 nginx 错误日志。
路由数据无需回滚:tunnels 表里的 http-proxy 行可以直接在后台停用或删除,Schema 仍是 v13,
本次特性零 DDL。
跨机部署内部入口(不推荐)
只有在 OpenResty 与 Server 无法同机时才这么做。此时内部段是明文,链路上的 Basic 凭据可被 嗅探,必须满足:
listen绑内网地址而不是0.0.0.0。trusted_proxies收紧到 OpenResty 主机的具体 IP(/32)。- 如确需配置
0.0.0.0/0或::/0,必须依赖防火墙、安全组或专线限制内部入口访问范围。 - 内部网段走专线或 IPSec,不要跨公网。
同机部署时内部入口只绑回环,trusted_proxies 保持默认的 ["127.0.0.1/32", "::1/128"] 即可。