
很多人第一次理解 frp,会把它想成一条公网服务器到内网服务的 TCP 转发链:用户连 frps,frps 再把流量送到 frpc 背后的本地端口。
这个理解不算错,但太薄了。源码里更关键的结构是:frpc 和 frps 之间先有一条长期控制面;frpc 通过控制面注册 proxy;真实用户连接到来时,frps 再向对应 control 要一条 work connection;最后由 proxy 类型决定这条数据面连接怎么被消费。
这篇看的不是“如何配置 frp”,而是 frp 如何把“内网服务可被外部访问”拆成几层稳定机制:控制面维护意图,work connection 承载数据,proxy factory 隔离协议差异,visitor/NAT hole 处理私有访问和 P2P 直连。
源码基线:
- • 本地源码目录:
sources/frp - • 分支:
dev - • Commit:
8563a5fc74d583665e807c5e39320705551a6072 - • Commit time:
2026-05-29T13:46:28+08:00 - • Commit subject:
feat: use wire v2 framing for XTCP NatHoleSid (#5343)
说明:本文由 AI 辅助完成,可能存在遗漏、理解偏差或版本差异。关键实现请以本文固定的 commit、本地源码和官方文档为准。
先看源码地图

frp 的入口很薄。cmd/frps/root.go 和 cmd/frpc/sub/root.go 主要负责读配置、校验配置、初始化日志,然后把运行交给 server.Service 和 client.Service。
真正值得先看的目录有几块:
- •
pkg/config/load.go和pkg/config/v1/:把 TOML/JSON/YAML 配置变成强类型 proxy、visitor config。 - •
server/service.go:装配服务端 listener、control manager、proxy manager、resource controller。 - •
client/service.go、client/control_session.go、client/connector.go:负责登录、重连、底层连接建立。 - •
server/control.go和client/control.go:控制面消息、心跳、work connection 池都在这里。 - •
server/proxy/和client/proxy/:不同 proxy 类型的数据面分发。 - •
client/visitor/、server/visitor/、pkg/nathole/:STCP、SUDP、XTCP 相关的 visitor 和 NAT traversal。
一个常见误读是直接从 server/proxy/tcp.go 开始看。这样会很快看到端口监听和 Join,但会错过更重要的问题:这条数据连接是谁请求的?为什么不是直接复用控制连接?HTTP、UDP、XTCP 又为什么能挂在同一套系统上?
配置不是运行时,ProxyConfigurer 才是入口
frp 的配置层会先把用户写的配置转成 typed configurer。
pkg/config/load.go 里,LoadConfigure 支持 TOML、JSON、YAML,也支持 strict parse。客户端侧的 LoadClientConfigResult 会同时产出 common、proxy、visitor 三类配置;后续还可以按 start 和 enabled 过滤。
proxy 类型在 pkg/config/v1/proxy.go 里注册。ProxyBaseConfig 统一描述 name、type、transport、metadatas、backend;ProxyConfigurer 是所有 proxy config 的接口;具体类型包括 tcp、udp、tcpmux、http、https、stcp、xtcp、sudp。
这层的意义是:配置表达的是意图,不是执行方式。tcp 想要远端端口,http 想要 vhost route,xtcp 想要通过 frps 协调打洞。它们先被归一成 configurer,再由后面的 proxy factory 决定怎么落地。
控制面:Login 之后才开始注册能力

frpc 第一次连上 frps 时,走的是 Login。服务端 server.Service.handleConnection 会按消息类型分流:msg.Login 注册 control,msg.NewWorkConn 交给已有 control,msg.NewVisitorConn 交给 visitor manager。
服务端 control 在 server/control.go。NewControl 会创建 dispatcher、transporter、workConnCh;Start 一开始会按 poolCount 请求 work connection;后续 handler 处理 NewProxy、Ping、NAT hole 消息和 CloseProxy。
客户端 control 在 client/control.go。Run 启动 worker 后,会让 proxy manager 和 visitor manager 更新本地配置。收到服务端 ReqWorkConn 时,客户端会新建连接,发送 NewWorkConn,等待 StartWorkConn,再交给对应 client proxy 处理。
所以 frp 的控制面不是“管理命令通道”这么简单。它至少承担四件事:确认客户端身份,注册 proxy 意图,维护心跳和生命周期,在用户流量到来时调度 work connection。
Work connection:数据面是按需配对出来的

用户访问服务端暴露的端口或域名时,server side proxy 不会把控制连接拿来传业务数据。它会向 control 要一条 work connection。
服务端通用逻辑在 server/proxy/proxy.go。GetWorkConnFromPool 会从 control 拿 work connection,再写 StartWorkConn。handleUserTCPConnection 会先跑 plugin hook,再拿 work conn,按配置加 encryption、compression、limiter,最后把用户连接和 work conn 用 libio.Join 接起来。
客户端收到 StartWorkConn 后,进入 client/proxy/proxy.go。默认 InWorkConn 会走 HandleTCPWorkConnection:先连接本地服务或 client plugin,然后把本地连接和远端 work conn join 到一起。
这种设计比“一条固定 tunnel”更灵活。连接池可以降低首包延迟;没有空闲连接时可以按需拉新;不同 proxy 类型可以复用同一套调度机制,再在自己的数据面里处理 TCP byte stream、UDP message stream 或 XTCP sid。
Proxy factory:协议差异被关在分支里

server side 的 proxy factory 在 server/proxy/proxy.go。NewProxy 会创建 BaseProxy、限速器,再根据 config type 找具体 constructor。client side 也有对应的 factory,用来绑定 client config、message transporter 和 callback。
这就是 frp 能同时支持 TCP、HTTP、UDP、STCP、SUDP、XTCP 的关键:它们共享控制面和 work connection,但接入位置不同。
TCP 在 server/proxy/tcp.go 里按 remote port 或 group 拿 listener,用户连接进来后走通用 TCP handler。
HTTP/HTTPS 在 server/proxy/http.go 里不是监听普通端口,而是注册 vhost RouteConfig。HTTP 请求命中 route 时,再去拿 work connection。
UDP 则不能简单当成 TCP stream。server/proxy/udp.go 会用 UDP port 建 read/send channel;client side 的 client/proxy/udp.go 用 UDPPacket 和本地 UDP forwarder 接起来。
如果只看“把流量转过去”,这些差异会被抹平;但源码里它们很清楚:TCP、HTTP、UDP 的 server side 接入口、路由模型和连接生命周期都不同。
Wire v2:协议层在继续收紧
本次 pinned commit 的 subject 是 feat: use wire v2 framing for XTCP NatHoleSid (#5343),刚好落在协议层。
消息定义在 pkg/msg/msg.go,包括 Login、NewProxy、NewWorkConn、ReqWorkConn、StartWorkConn、NewVisitorConn、Ping/Pong 和 NAT hole 相关消息。
v2 frame 在 pkg/msg/wire_v2.go 和 pkg/proto/wire/wire.go。wire.Frame 使用 header、flags、length、payload;WriteMagic / CheckMagic 用于 v2 探测;ClientHello / ServerHello 和 crypto context 负责握手阶段的选择。
这里的重点不是“协议升级了”这句空话,而是 frp 正在把控制消息、frame 边界、加密上下文这些层次拆得更明确。XTCP 的 NatHoleSid 也走 wire v2 framing,说明 v2 不只服务登录链路,也会进入更细的子协议路径。
Visitor 和 XTCP:最容易被低估的部分

STCP、SUDP、XTCP 的 mental model 和普通 TCP 代理不一样。它们引入了 visitor。
STCP/SUDP 的 server side 不暴露公网端口,而是在 server/visitor/visitor.go 里注册 internal visitor listener。visitor 连接上来时,会校验 secretKey 和 allowUsers,再把连接放进 listener。
client side visitor 在 client/visitor/visitor.go。dialRawVisitorConn 会主动连接 frps,发送 NewVisitorConn,里面带 runID、proxyName、signKey、timestamp、encryption、compression。STCP 本地用户连接进来后,visitor 建 raw visitor conn,再和用户连接 join;SUDP 则把 UDP 包装成 UDPPacket。
XTCP 更特殊。它不是让 frps 长期转发数据,而是让两端在 frps 协调下尝试直连。
server side 的 server/proxy/xtcp.go 会通过 NatHoleController.ListenClient 注册可访问的 proxy;当 controller 产生 sid 后,取一条 work conn,把 NatHoleSid 写给 client proxy。
client side 的 client/proxy/xtcp.go 先从 work conn 读 NatHoleSid,再做 STUN Prepare,发送 NatHoleClient,等待 NatHoleResp,最后执行 MakeHole。visitor side 的 client/visitor/xtcp.go 也会做 PreCheck、STUN Prepare、发送 NatHoleVisitor、接收 NatHoleResp,并初始化 KCP 或 QUIC tunnel session。
真正的协调逻辑在 pkg/nathole/controller.go 和 pkg/nathole/nathole.go。源码会根据两端 mapped addresses、assisted addresses 和 NAT feature 选择行为,再生成双方各自的 NatHoleResp。这也是 XTCP 有时能直连、有时失败、有时需要 fallback 的根本原因:它依赖两端网络环境,不是单纯的 frp 配置开关。
最后怎么读
读 frp 最稳的顺序是:
- 1. 先看
cmd/frps/root.go、cmd/frpc/sub/root.go,确认入口只是装配。 - 2. 再看
pkg/config/load.go、pkg/config/v1/proxy.go、pkg/config/v1/visitor.go,理解配置如何变成 typed configurer。 - 3. 接着看
server/service.go、client/service.go、client/control_session.go、client/connector.go,把登录和连接建立串起来。 - 4. 然后读
server/control.go、client/control.go,抓住Login -> NewProxy -> ReqWorkConn -> NewWorkConn -> StartWorkConn。 - 5. 最后进入
server/proxy/、client/proxy/、client/visitor/和pkg/nathole/,分别看每种 proxy 如何复用这套骨架。
frp 的价值不只是“把内网端口暴露出去”。它更像一套把反向访问拆清楚的工程模型:配置层表达意图,控制面维护状态,work connection 承载真实流量,proxy factory 隔离协议分支,visitor 和 NAT traversal 处理更复杂的访问路径。理解了这几层,再看任何一种 proxy 类型,都不会只剩下一句“转发过去了”。
夜雨聆风