首页
/ frp SSH Tunnel Gateway:用标准 SSH 反向隧道替代 frpc 暴露内网服务

frp SSH Tunnel Gateway:用标准 SSH 反向隧道替代 frpc 暴露内网服务

2026-09-04 17:18:35作者:廉皓灿Ida

本文基于 frp 官方文档 SSH Tunnel Gateway 展开,讲解自 v0.53.0 引入的 SSH 隧道网关功能:如何仅通过一个 SSH 客户端命令(无需部署 frpc),利用 SSH 协议原生的 -R 反向隧道能力,将内网服务暴露到公网 frps 一侧,并深入剖析 pkg/ssh 目录下 Gateway 与 TunnelServer 的实现原理、认证机制与完整配置参数。

一、概念:什么是 SSH 反向隧道代理

SSH 协议本身支持反向代理能力(参考 RFC 4254 中 tcpip-forward 请求与 forwarded-tcpip 通道的定义)。frp 的 SSH Tunnel Gateway 利用这一点,让 frps 侧直接监听一个 SSH 端口,用户用系统自带的 ssh 客户端执行 -R 反向端口转发命令,即可在 frps 上完成 TCP 协议代理,而整个过程中不需要 frpc 参与

需要特别区分两个容易混淆的概念:

  • SSH 反向隧道代理(本文主题):不使用 frpc,由 SSH 客户端直连 frps,本质上是借助 SSH 协议完成的一次基础反向代理;
  • 通过 frp 代理 SSH 端口:把一台机器上的 22 端口作为普通 TCP 代理转发出去,与本文功能完全不同。

从源码结构看,frps 在 server/service.go 启动时检查配置:只要 sshTunnelGateway.bindPort > 0,就会调用 ssh.NewGateway 创建网关并将日志打印为 frps sshTunnelGateway listen on port %d,随后把该 Gateway 的监听器挂入服务生命周期。对应的配置结构体定义在 pkg/config/v1/server.go 中:

type SSHTunnelGateway struct {
    BindPort              int    `json:"bindPort,omitempty"`
    PrivateKeyFile        string `json:"privateKeyFile,omitempty"`
    AutoGenPrivateKeyPath string `json:"autoGenPrivateKeyPath,omitempty"`
    AuthorizedKeysFile    string `json:"authorizedKeysFile,omitempty"`
}

func (c *SSHTunnelGateway) Complete() {
    c.AutoGenPrivateKeyPath = util.EmptyOr(c.AutoGenPrivateKeyPath, "./.autogen_ssh_key")
}

Complete() 方法印证了文档中“autoGenPrivateKeyPath 默认为 ./.autogen_ssh_key”的说法——未显式配置时会自动补齐默认路径。

二、配置参数详解

frps.toml 中,SSH 隧道网关的完整配置形如:

# frps.toml
sshTunnelGateway.bindPort = 0
sshTunnelGateway.privateKeyFile = ""
sshTunnelGateway.autoGenPrivateKeyPath = ""
sshTunnelGateway.authorizedKeysFile = ""
字段 类型 说明 是否必填
bindPort int frps 上 SSH 服务监听端口。
privateKeyFile string 默认为空。SSH 服务端使用的私钥文件。为空时 frps 会读取 autoGenPrivateKeyPath 路径下的私钥文件。可以复用本机的 /home/user/.ssh/id_rsa,也可以指定自定义路径。
autoGenPrivateKeyPath string 默认为 ./.autogen_ssh_key。若文件不存在或内容为空,frps 会自动生成 RSA 私钥并写入该文件。
authorizedKeysFile string 默认为空。为空时 SSH 客户端不做认证;不为空时可实现 SSH 免密(公钥)登录认证。可复用本机 /home/user/.ssh/authorized_keys 或指定自定义路径。

私钥的加载逻辑在 pkg/ssh/gateway.goNewGateway 中,优先级非常明确:

  1. privateKeyFile 非空,直接读取该文件;
  2. 否则尝试读取 autoGenPrivateKeyPath 下的内容;
  3. 仍为空时,调用 transport.NewRandomPrivateKey() 现场生成一把 RSA 私钥,并按 0o600 权限写回 autoGenPrivateKeyPath
  4. 最终经 ssh.ParsePrivateKey 解析后通过 sshConfig.AddHostKey 注册到 SSH 服务端。

这段实现解释了文档中的两条注意事项:其一,最小配置下 frps 会在当前工作目录自动生成 .autogen_ssh_key 文件用于加解密;其二,也可以指定 privateKeyFile 复用本机已有的 id_rsa

认证开关的实现同样在 gateway.go 中:

sshConfig.NoClientAuth = cfg.AuthorizedKeysFile == ""
sshConfig.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
    authorizedKeysMap, err := loadAuthorizedKeysFromFile(cfg.AuthorizedKeysFile)
    ...
    user, ok := authorizedKeysMap[string(key.Marshal())]
    if !ok {
        return nil, fmt.Errorf("unknown public key for remoteAddr %q", conn.RemoteAddr())
    }
    return &ssh.Permissions{
        Extensions: map[string]string{"user": user},
    }, nil
}

可以看出:authorizedKeysFile 为空时 NoClientAuth = true(完全不做客户端认证);不为空时每次握手都会重新解析 authorized_keys 文件,把公钥的 comment 字段作为用户名写入 Permissions.Extensions["user"],后续在 pkg/ssh/server.go 中会回填到客户端配置的 User 字段。

三、基本用法

3.1 服务端 frps 最小配置

sshTunnelGateway.bindPort = 2200

将以上配置放入 frps.toml,运行 ./frps -c frps.toml,frps 即在 2200 端口监听并接收 SSH 反向代理请求。注意事项:

  1. 最小配置下,frps 会在当前工作目录自动创建 .autogen_ssh_key 私钥文件,SSH 服务端使用该私钥完成加解密;也可以选择复用本机已有私钥,如 /home/user/.ssh/id_rsa
  2. 最小配置模式下,SSH 连接 frps 时不需要任何认证。文档强烈建议同时为 frps 配置 token,并在 SSH 命令行中通过 --token 传入,作为 frp 层的访问控制。

3.2 客户端 SSH 命令格式

ssh -R :80:{local_ip:port} v0@{frps_address} -p {frps_ssh_listen_port} {tcp|http|https|stcp|tcpmux} --remote_port {real_remote_port} --proxy_name {proxy_name} --token {frp_token}

关键说明(与源码逐条对应):

  1. --proxy_name 可选,留空时自动生成随机名。对应 pkg/ssh/server.goparseClientAndProxyConfigurer 的逻辑:未设置 Name 时生成 sshtunnel-{type}-{8位随机ID} 作为代理名;
  2. 登录 frps 的用户名固定为 v0,目前没有实际意义;
  3. 服务端代理监听端口由 --remote_port 决定;
  4. {tcp|http|https|stcp|tcpmux} 五种代理类型各自支持完整的命令行参数,可用 --help 查看。例如 ssh -R :80::8080 v0@127.0.0.1 -p 2200 http --help。源码中该白名单为 supportTypes := []string{"tcp", "http", "https", "tcpmux", "stcp"},传入其他类型会直接报错;
  5. token 可选,但出于安全考虑强烈建议在 frps 中配置。

从源码结构看,这套命令行参数并非 SSH 隧道自研:frps 直接把 exec 请求里的 payload 交给一个内嵌的 cobra 命令解析,复用 pkg/config/flags.goRegisterProxyFlagsRegisterClientCommonConfigFlags 注册的 frpc 同款 flag(以 WithSSHMode() 模式),因此 SSH 隧道模式下的 --help 输出与 frpc tcp --help 等保持一致。值得注意的是 SSH 模式会屏蔽 local_iplocal_portserver_addr 等 frpc 常规 flag——因为本地地址已经编码在 -R :80:127.0.0.1:8080 的转发目标里了。

3.3 TCP 代理

ssh -R :80:127.0.0.1:8080 v0@{frp_address} -p 2200 tcp --proxy_name "test-tcp" --remote_port 9090

该命令在 frps 上建立一个监听 9090 端口的代理,把流量转发到本机的 8080 端口服务。执行成功后的终端输出形如:

frp (via SSH) (Ctrl+C to quit)

User:
ProxyName: test-tcp
Type: tcp
RemoteAddress: :9090

这段输出由 pkg/ssh/terminal.gocreateSuccessInfo 生成。该命令等价于:

frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote_port 9090

更多参数可通过 --help 获取。

3.4 HTTP 代理

ssh -R :80:127.0.0.1:8080 v0@{frp_address} -p 2200 http --proxy_name "test-http" --custom_domain test-http.frps.com

等价于:

frpc http --proxy_name "test-http" --custom_domain test-http.frps.com

之后即可通过 curl 'http://test-http.frps.com' 访问该 HTTP 服务(需 frps 开启 vhostHTTPPort 并将 subDomainHost 配置为 frps.com,域名的拼接规则见 pkg/config/v1/server.goSubDomainHost 字段的注释)。

3.5 HTTPS / STCP / TCPMUX 代理

获取对应类型的完整用法:

ssh -R :80:127.0.0.1:8080 v0@{frp_address} -p 2200 {https|stcp|tcpmux} --help

以 stcp 为例,实际等价命令需要携带 serverName-n)、secretKey--sk)等参数,例如 stcp -n stcp-test --sk=abcdefg --allow-users="*";tcpmux 则支持 --mux=httpconnect --custom_domain ... 形式。这些用法在 e2e 测试 test/e2e/v1/features/ssh_tunnel.go 中有完整覆盖,五个用例(tcp、http、https、tcpmux、stcp)分别验证了各代理类型经 SSH 隧道建立后,流量确实能从公网端口回环到本地 mock 服务。

四、进阶用法

4.1 复用本机 id_rsa 私钥

# frps.toml
sshTunnelGateway.bindPort = 2200
sshTunnelGateway.privateKeyFile = "/home/user/.ssh/id_rsa"

SSH 协议握手阶段双方交换公钥用于数据加密,因此 frps 侧的 SSH 服务端必须持有一把私钥。指定 privateKeyFile 可以直接复用本机已有私钥;该字段为空时 frps 会自动生成 RSA 私钥(写入 autoGenPrivateKeyPath)。

4.2 指定自动生成私钥的路径

# frps.toml
sshTunnelGateway.bindPort = 2200
sshTunnelGateway.autoGenPrivateKeyPath = "/var/frp/ssh-private-key-file"

frps 会自动创建私钥文件并保存至指定路径。

注意:更换 frps 使用的私钥文件可能导致 SSH 客户端登录失败(known_hosts 校验不通过)。若需要成功登录,可删除 /home/user/.ssh/known_hosts 中的旧记录。

4.3 使用既有 authorized_keys 做 SSH 公钥认证

# frps.toml
sshTunnelGateway.bindPort = 2200
sshTunnelGateway.authorizedKeysFile = "/home/user/.ssh/authorized_keys"

authorizedKeysFile 是 SSH 公钥认证使用的文件,包含用户的公钥信息,一行一把密钥。解析逻辑在 pkg/ssh/gateway.goloadAuthorizedKeysFromFile 中,循环调用 ssh.ParseAuthorizedKey 直到文件读完。

authorizedKeysFile 为空,frps 不会对 SSH 客户端做任何认证;frps 不支持 SSH 用户名/密码认证,只支持公钥认证。

4.4 使用自定义 authorized_keys 文件

# frps.toml
sshTunnelGateway.bindPort = 2200
sshTunnelGateway.authorizedKeysFile = "/var/frps/custom_authorized_keys_file"

指定自定义 authorized_keys 文件路径即可。注意修改该文件内容可能导致认证失败,需要重新添加公钥信息。

4.5 双层认证的分工

文档特别强调了两层认证的独立性,这一点在源码中可以直接验证(pkg/ssh/server.govirtual.NewClientSpec 字段):

Spec: &msg.ClientSpec{
    Type: "ssh-tunnel",
    // If ssh does not require authentication, then the virtual client needs
    // to authenticate through a token. Otherwise, once ssh authentication
    // is passed, the virtual client does not need to authenticate again.
    AlwaysAuthPass: !s.sc.NoClientAuth,
},
  • authorizedKeysFile 作用于 SSH 登录阶段的用户认证;
  • frps 的 token 作用于 frp 协议层的认证;
  • 两者相互独立、先 SSH 认证后 token 认证;
  • 若启用了 SSH 公钥认证(NoClientAuth == false),虚拟客户端会跳过 frp token 认证(AlwaysAuthPass = true);反之若不做 SSH 认证,则强烈建议至少启用 frps token 认证,避免任何人可匿名接入。

五、工作原理:frps 内部的虚拟客户端

理解该功能最有价值的部分,是 frps 如何在 SSH 会话内“无中生有”出一个完整的 frp 客户端。核心流程在 pkg/ssh/server.goTunnelServer.Run 中:

  1. 完成 SSH 握手ssh.NewServerConn 建立服务端会话;
  2. 捕获反向转发地址与额外 payloadwaitForwardAddrAndExtraPayload 并发监听两类消息——tcpip-forward 全局请求(携带 -R :80:127.0.0.1:8080 中的目标地址)与首个 session 通道中 exec 请求的 payload(携带 tcp --proxy_name ... 等命令行参数),3 秒内收齐两者;
  3. 解析命令行parseClientAndProxyConfigurer 将 payload 拆分为参数,通过 cobra 解析为 ClientCommonConfig + 对应类型的 ProxyConfigurer
  4. 构建虚拟客户端:调用 pkg/virtual/client.govirtual.NewClient,它内部复用完整的 client.Service(与 frpc 相同的服务框架),但把网络连接器替换为 pipeConnector:控制连接不经过真实网络,而是通过 net.Pipe() 直接对端投递给 frps 内部的 peerServerListener。也就是说,SSH 隧道在 frps 进程内部“扮演”了一个 frpc
  5. 接管业务流量:注册 HandleWorkConnCb 回调,每当服务端要为某代理建立工作连接时,openConn 通过 forwarded-tcpip 通道向 SSH 客户端发起反向连接(即把 -R 注册的 :80 端口打回内网),再用 libio.Join 把该 SSH 通道与 frp 工作连接拼接成一条双向管道,流量即完成 公网用户 → frps 9090 → SSH 通道 → 内网 8080 的全链路转发;
  6. 状态回报waitProxyStatusReady 每 100ms 轮询虚拟客户端的代理状态,直到 Running(输出成功信息)或 StartErr/Closed(把错误回写给 SSH 客户端);随后 sshConn.Wait() 保持会话,直到客户端 Ctrl+C 断开;
  7. 保活keepAlive 每 30 秒在首个 session 通道上发送 heartbeat 请求,防止空闲会话被中间设备掐断。

这套“进程内虚拟客户端 + 反向通道回拨”的设计,正是文档所说“本模式不依赖 frpc”的技术本质:frpc 的完整控制/代理逻辑被原样嵌入 frps 进程,仅将传输层换成了 SSH 通道。

六、e2e 测试中的可复制示例

test/e2e/v1/features/ssh_tunnel.go 展示了各代理类型的真实调用方式,其中 SSH 客户端由 test/e2e/pkg/ssh/client.goTunnelClient 模拟,其行为与命令行 ssh -R 完全一致:Listen("tcp", "0.0.0.0:80") 注册反向端口,再 OpenChannel("session") 发送 exec 请求携带代理命令。tcp 用例配置如下:

sshTunnelGateway.bindPort = {sshPort}

客户端发起:

ssh.NewTunnelClient(
    fmt.Sprintf("127.0.0.1:%d", localPort),
    fmt.Sprintf("127.0.0.1:%d", sshPort),
    fmt.Sprintf("tcp --remote-port %d", remotePort),
)

http 用例额外开启 vhostHTTPPort,tcpmux 用例额外开启 tcpmuxHTTPConnectPort 并验证了“不带 HTTP CONNECT 的请求会报错、CONNECT 主机名不匹配会报错、匹配则返回正确响应”的完整语义;stcp 用例则演示了与常规 frpc visitor 协作:SSH 隧道只负责注册 stcp 服务(stcp -n stcp-test --sk=abcdefg --allow-users="*"),访问仍由独立的 visitor 客户端完成。这些用例与本文第三、四节的手动命令行用法一一对应,可作为自动化验证 SSH 隧道是否工作正常的参考。

七、适用场景与限制小结

结合文档与源码,SSH Tunnel Gateway 的适用前提是:

  • 客户端无需安装 frpc,只要系统自带 OpenSSH 客户端(如服务器、容器、移动端),适合临时、轻量地把内网服务暴露出去;
  • 仅支持 tcphttphttpsstcptcpmux 五种代理类型(pkg/ssh/server.go 中的 supportTypes 白名单),不支持 udp 等其他类型;
  • 功能自 v0.53.0 起提供,使用前请确认 frps 版本;
  • 安全上务必二选一(或同时启用):SSH 公钥认证(authorizedKeysFile)或 frp token 认证;两者均为空时任何知道端口的人都可以匿名注册代理;
  • SSH 客户端侧若更换了 frps 私钥,需要清理 known_hosts 中的旧主机指纹才能继续登录;
  • 登录用户名固定为 v0,无实际语义,不要据此做多租户区分;多用户区分应通过 authorized_keys 中每把公钥的 comment 实现(服务端会把 comment 作为 User 回填)。

掌握以上内容后,读者即可在不部署 frpc 的约束下,仅凭一条 ssh -R 命令完成内网服务暴露,并能在出现问题时顺着 pkg/sshpkg/virtual 的源码链路快速定位 SSH 握手、参数解析或虚拟客户端认证环节的原因。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384