用 caddy-l4 实现 Caddy TCP 反向代理与 SSH 代理
核心版 Caddy 只能代理 HTTP。若要代理原始 TCP 和 SSH,需要用 caddy-l4 layer4 应用构建 Caddy,实现端口转发,或按协议让 SSH 与 HTTPS 共用 443 端口。
简答:标准 Caddy 无法代理原始 TCP 或 SSH,因为它的 `reverse_proxy` 只支持 HTTP。要转发 TCP,你需要用 xcaddy 构建带有 caddy-l4 模块(github.com/mholt/caddy-l4)的 Caddy,然后配置其 `layer4` 应用监听端口并代理连接,还可以按协议选择上游,让 SSH 和 HTTPS 共用 443 端口。
caddy-l4 是独立模块,不包含在默认 Caddy 发行版中,其 README 仍称它处于开发阶段,配置可能随版本变化。本指南使用 JSON 配置,它与模块文档中的结构一一对应。如果你只需要 HTTP 或 HTTPS 代理,Caddy 反向代理指南就足够了。
为什么核心版 Caddy 只支持 HTTP
Caddy 内置的应用是 `http`、`tls` 和 `pki`。`reverse_proxy` 处理器位于 HTTP 应用内部,必须先解析出 HTTP 请求才能进行路由。SSH 客户端、PostgreSQL 客户端或游戏协议从不发送 HTTP 请求,因此 Caddy 只会返回 TLS 或 HTTP 错误。
caddy-l4 新增了一个独立的 `layer4` 应用,直接处理原始连接。它可以匹配连接的前几个字节(SSH 横幅、带 SNI 的 TLS ClientHello、HTTP 请求行、PostgreSQL 启动包等),然后进行代理、终止 TLS 或将数据流交给其他处理器。由于它和 `http` 一样是一个应用,两者可以运行在同一进程中。
| 需求 | 核心版 Caddy | Caddy + caddy-l4 |
|---|---|---|
| 带自动 TLS 的 HTTPS 反向代理 | 支持 | 支持 |
| 普通 TCP 端口转发 | 不支持 | 支持(`proxy` 处理器) |
| UDP 转发 | 不支持 | 支持(监听 `udp/:port`) |
| 按 SNI 的 TLS 透传 | 不支持 | 支持(带 `sni` 的 `tls` 匹配器) |
| SSH 与 HTTPS 共用同一端口 | 不支持 | 支持(`ssh` 和 `tls` 匹配器) |
| Caddyfile 支持 | 完整 | 部分;JSON 为参考格式 |
构建带 layer4 模块的 Caddy
xcaddy 可以编译带额外模块的 Caddy。也可以在 Caddy 官方下载页面勾选 `mholt/caddy-l4` 下载现成的二进制文件。无论哪种方式,在编写配置前都请用 `list-modules` 确认已列出 `layer4` 模块。
# Current Caddy needs a recent Go toolchain; distro golang-go packages are
# often too old, so install the latest release from https://go.dev/dl/
# and make sure /usr/local/go/bin is in PATH, then:
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
# Build Caddy with the layer4 app
~/go/bin/xcaddy build --with github.com/mholt/caddy-l4
# Confirm the modules are present
./caddy list-modules | grep layer4安全替换软件包自带的二进制文件
如果 Caddy 是从官方 apt 仓库安装的,官方文档推荐用 `dpkg-divert` 加 `update-alternatives` 换入自定义构建,这样软件包升级不会覆盖你的二进制文件。请记住,此后你需要重新构建才能获得 Caddy 更新。
sudo dpkg-divert --divert /usr/bin/caddy.default --rename /usr/bin/caddy
sudo mv ./caddy /usr/bin/caddy.custom
sudo update-alternatives --install /usr/bin/caddy caddy /usr/bin/caddy.default 10
sudo update-alternatives --install /usr/bin/caddy caddy /usr/bin/caddy.custom 50
sudo systemctl restart caddy示例 1:普通 TCP 反向代理
此配置监听 5432 端口,并把每个连接转发到私有地址上的 PostgreSQL 服务器,例如 WireGuard 隧道的另一端。没有匹配器意味着所有连接都走这条路由。上游可以填写多个 `dial` 地址用于故障转移,`proxy` 处理器支持与 HTTP `reverse_proxy` 类似的负载均衡和健康检查。
后端看到的是 Caddy 的 IP,而不是客户端的。如果后端支持 PROXY 协议,在 proxy 处理器上设置 `"proxy_protocol": "v2"` 即可传递原始地址。
{
"apps": {
"layer4": {
"servers": {
"postgres": {
"listen": [":5432"],
"routes": [
{
"handle": [
{
"handler": "proxy",
"upstreams": [{"dial": ["10.8.0.2:5432"]}]
}
]
}
]
}
}
}
}
}示例 2:在 443 端口复用 SSH 与 HTTPS
SSH 客户端一连接就会发送版本横幅(`SSH-2.0-...`),因此 caddy-l4 可以在服务器响应之前识别出 SSH。下面的配置把 SSH 发往本地 sshd,把 `git.example.com` 的 TLS 发往自行终止 TLS 的服务,其余 TLS 交给监听 8443 的 Caddy HTTP 应用。由于 443 端口已归 layer4 所有,HTTP 应用不能再绑定它:在 Caddyfile 全局选项中设置 `https_port 8443`(或在 JSON 中设置等效的 `listen` 地址),并保持 80 端口开放以完成 HTTP-01 质询。设置 `https_port 8443` 后,Caddy 自动的 HTTP 到 HTTPS 重定向会指向 8443 端口,因此需设置 `auto_https disable_redirects`,并自行添加一个 `http:// { redir https://{host}{uri} permanent }` 站点。
对于由服务器先发言的协议,如 SMTP、FTP 或 MySQL,无法用这种方式识别,因为客户端会静默等待。请为它们分配独立端口。
{
"apps": {
"layer4": {
"servers": {
"mux443": {
"listen": [":443"],
"routes": [
{
"match": [{"ssh": {}}],
"handle": [
{"handler": "proxy", "upstreams": [{"dial": ["127.0.0.1:22"]}]}
]
},
{
"match": [{"tls": {"sni": ["git.example.com"]}}],
"handle": [
{"handler": "proxy", "upstreams": [{"dial": ["127.0.0.1:3443"]}]}
]
},
{
"match": [{"tls": {}}],
"handle": [
{"handler": "proxy", "upstreams": [{"dial": ["127.0.0.1:8443"]}]}
]
}
]
}
}
}
}
}运行与测试
- 01.将 JSON 保存为 `/etc/caddy/caddy.json` 并进行校验。
- 02.停止服务,在前台运行 Caddy,以便连接时观察日志。
- 03.通过 drop-in 覆盖让 systemd 单元指向该 JSON 文件,然后执行 `sudo systemctl daemon-reload` 和 `sudo systemctl start caddy`。
- 04.用 `ssh -p 443` 连接,并在浏览器中打开 HTTPS 站点,确认两条路由都正常。
# Validate and test in the foreground first
sudo caddy validate --config /etc/caddy/caddy.json
sudo systemctl stop caddy
sudo caddy run --config /etc/caddy/caddy.json
# Make the service use the JSON file: sudo systemctl edit caddy
[Service]
ExecStart=
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/caddy.json
ExecReload=
ExecReload=/usr/bin/caddy reload --config /etc/caddy/caddy.json --force
# Client side
ssh -p 443 [email protected]何时改用 HAProxy 或 sslh
如果你已经在运行 Caddy,并希望用一个进程处理 HTTPS 和少量 TCP 路由,caddy-l4 是不错的选择。对于以 TCP 为主的场景,Debian 和 Ubuntu 仓库中的成熟替代方案可能更易运维:`mode tcp` 下的 HAProxy 可用 `req.ssl_sni` 按 SNI 路由,并提供详细的统计和健康检查;sslh 则是专为在 SSH、TLS、OpenVPN 等之间共享一个端口而设计的小型守护进程。两者都通过 apt 安装,并随系统一起更新,无需维护自定义构建。
将 SSH 暴露在 443 端口有助于在只放行 Web 端口的网络中使用,但它并不是安全特性。请保持 SSH 加固指南中的仅密钥认证。在 NoctisNode VPS 上你拥有完整 root 权限,上述任何工具都可以绑定所需端口;HTTPS 端的证书行为请参阅 Caddy 自动 HTTPS,容器场景请参阅将 Caddy 用作 Docker 反向代理。
| 工具 | 优势 | 取舍 |
|---|---|---|
| Caddy + caddy-l4 | 一个进程同时处理自动 HTTPS 与 TCP,协议匹配器,JSON API | 需自定义构建,模块仍在演进 |
| HAProxy | 成熟的 TCP/HTTP 代理,SNI 路由,丰富的健康检查与统计 | 配置较多,无内置 ACME 客户端 |
| sslh | 小巧,专为 SSH/TLS/VPN 端口共享而生 | 只做复用;不支持 TLS 终止或负载均衡 |
常见问题
QCaddy 能代理 TCP 流量吗?
A开箱即用不行。核心版 Caddy 只代理 HTTP;要代理原始 TCP 或 UDP,你需要包含第三方 caddy-l4(layer4)模块的 Caddy 构建。
Q如何通过 Caddy 代理 SSH?
A用 caddy-l4 构建 Caddy,添加一个匹配 `ssh` 并代理到 `127.0.0.1:22` 的 layer4 服务器,然后用 `ssh -p <port>` 连接。在 443 端口上还可以结合 `tls` 路由,让 HTTPS 照常工作。
QSSH 和 HTTPS 能共用 443 端口吗?
A能。caddy-l4、sslh 和 HAProxy 都可以检查连接的前几个字节,将 SSH 和 TLS 路由到同一端口上的不同后端。
Qcaddy-l4 支持 Caddyfile 吗?
A它通过 `layer4` 全局选项支持 Caddyfile,但 JSON 仍是最完整、文档最全的格式。请查阅你所构建版本的模块 README。