frp Server Plugin 详解:用外部 HTTP 服务扩展 frps 的插件化能力
frp 的 Server Plugin 机制允许开发者在不修改 Golang 源码的前提下,通过一个独立的 HTTP 外部进程扩展 frps 的能力。frps 在执行关键操作(如客户端登录、创建/关闭代理、心跳等)之前,会向插件服务发送基于 JSON over HTTP 的 RPC 请求,并根据响应决定是否放行、拒绝或修改该操作的内容。读完本文,你将掌握 Server Plugin 的完整 RPC 协议(六种 Operation 的请求/响应格式)、frps 侧的配置方法、元数据传递机制,以及从源码角度理解插件链的执行语义,从而可以编写自己的认证、端口分配、审计类插件服务。
Server Plugin 的工作模型
Server Plugin 的定位是“在 frps 关键操作前插入一个可编程的决策点”。其核心模型如下:
- 外部服务运行在独立进程中,监听自己的 HTTP 端口,接收来自 frps 的 RPC 调用;
- frps 在执行某些操作之前,先向外部 RPC 服务发送请求,并根据其返回结果决定后续行为;
- RPC 基于 JSON over HTTP,每次调用是一个带查询参数的
POST请求。
从源码结构看,frps 在启动时遍历配置文件中的 [[httpPlugins]] 条目,将每个条目包装为一个 Plugin 实例并注册进 Manager:
- 配置结构体 HTTPPluginOptions 定义了
name、addr、path、ops、tlsVerify五个字段; - 注册逻辑位于 Service 初始化:
for _, p := range cfg.HTTPPlugins { svr.pluginManager.Register(plugin.NewHTTPPluginOptions(p)) }。
Plugin 接口本身只有三个方法(见 plugin.go):
type Plugin interface {
Name() string
IsSupport(op string) bool
Handle(ctx context.Context, op string, content any) (res *Response, retContent any, err error)
}
其中 IsSupport 由配置中的 ops 字段决定——插件只处理自己声明关心的操作。协议版本常量 APIVersion = "0.1.0",与文档中示例请求里的 version=0.1.0 一致。
RPC 请求与响应协议
请求格式
HTTP path 在每个插件上可独立配置,下面以 path = "/handler" 为例,frps 发出的请求形如:
POST /handler?version=0.1.0&op=Login
{
"version": "0.1.0",
"op": "Login",
"content": {
... // 操作的具体内容
}
}
Request Header:
X-Frp-Reqid: 用于链路追踪
对照 httpPlugin.do 的实现,可以确认几个细节:
- 请求体是
Request{Version, Op, Content}的 JSON 序列化(types.go); version与op同时作为 URL query 参数附带一份;- Header 中固定携带
X-Frp-Reqid(每次请求由 newPluginRequestContext 生成的随机 ID)和Content-Type: application/json,插件侧可以用 reqid 做日志关联追踪; addr未带协议前缀时默认补http://;以https://开头时会创建带 TLS 配置的客户端,tlsVerify控制是否跳过证书校验。
响应格式
插件对每次操作请求可给出以下四类响应之一:
- 非 200 HTTP 状态码:frps 直接视为请求失败。源码中 do 方法 对非 200 会返回
do http request error code: %d错误。 - 拒绝操作,并返回原因:
{
"reject": true,
"reject_reason": "invalid user"
}
- 允许操作,且保持原内容不变:
{
"reject": false,
"unchange": true
}
- 允许操作,并返回修改后的内容(用
content整体替换原内容):
{
"unchange": "false",
"content": {
... // 替换后的内容
}
}
响应结构对应 Response 结构体。值得注意的一个源码细节:当 unchange 为 false 时,frps 会对插件返回的 content 做一次类型断言 retContent.(*T)(见 handleMutableContent),即替换内容必须是与请求 content 相同结构的对象——插件返回的是对整个 content 的替换,而不是差量合并。
Manager 的插件链执行语义
Manager 按操作类型分别维护六个插件列表(loginPlugins、newProxyPlugins 等),注册时依据 IsSupport(op) 将插件分入对应列表。执行语义(handleMutableContent):
- 同一操作的多个插件按配置顺序串行执行,前一个插件返回的内容会作为后一个插件的输入,形成内容改写链;
- 任一插件返回
reject: true,操作立即失败,错误信息就是reject_reason; - 任一插件请求出错(网络失败/非 200),操作同样失败,frps 日志会记录
send <Op> request to plugin [<name>] error,并带 reqid 前缀便于追踪。
唯一例外是 CloseProxy:它不返回可修改内容,且插件出错时只聚合记录错误而不中断其他插件的执行——这与“代理已经要关闭,插件通知失败不应阻塞清理”的语义相符。
六种支持的 Operation 及其 Content 结构
当前支持的操作为:Login、NewProxy、CloseProxy、Ping、NewWorkConn、NewUserConn(常量定义见 plugin.go)。各操作的触发点与 content 字段如下。
Login — 客户端登录操作
frpc 向 frps 登录时触发,触发点见 Service 处理登录。这是实现“外部用户认证”的主要入口:
{
"content": {
"version": "<string>",
"hostname": "<string>",
"os": "<string>",
"arch": "<string>",
"user": "<string>",
"timestamp": "<int64>",
"privilege_key": "<string>",
"run_id": "<string>",
"pool_count": "<int>",
"metas": "map<string>string",
"client_address": "<string>"
}
}
对应结构体 LoginContent 内嵌 msg.Login,额外附加 client_address(frpc 建连来源地址)。由于 Login 内容允许被插件替换(unchange: false),插件可以在这里改写 user、补充 metas 等信息。
NewProxy — 创建新代理
frpc 请求创建代理时触发,触发点见 control.go 中处理 NewProxy。content 携带了完整的代理定义,插件可以在这里做端口冲突检测、动态端口分配、域名白名单等事:
{
"content": {
"user": {
"user": "<string>",
"metas": "map<string>string",
"run_id": "<string>"
},
"proxy_name": "<string>",
"proxy_type": "<string>",
"use_encryption": "<bool>",
"use_compression": "<bool>",
"bandwidth_limit": "<string>",
"bandwidth_limit_mode": "<string>",
"group": "<string>",
"group_key": "<string>",
// 仅 tcp 和 udp
"remote_port": "<int>",
// 仅 http 和 https
"custom_domains": ["<string>"],
"subdomain": "<string>",
"locations": ["<string>"],
"http_user": "<string>",
"http_pwd": "<string>",
"host_header_rewrite": "<string>",
"headers": "map<string>string",
// 仅 stcp
"sk": "<string>",
// 仅 tcpmux
"multiplexer": "<string>",
"metas": "map<string>string"
}
}
对应结构体 NewProxyContent 由 UserInfo 与 msg.NewProxy 组合而成,字段随代理类型不同而有所取舍(如上注释所示)。由于 NewProxy 的内容可被替换,一个典型用法是:客户端配置 remotePort = 0(自动分配),插件在收到 NewProxy 后从自己的端口池中挑一个可用端口写回 remote_port,实现集中式端口管理。
CloseProxy — 代理被关闭
先前创建的代理被关闭时触发,触发点见 control.go 中关闭代理的 notify。注意:
- 每关闭一个代理都会单独发一次请求;如果一个客户端绑定了大量代理,逐个通知可能耗尽服务端资源,文档对此有明确警告——如果你的场景客户端代理数量很多,不要在这个操作上做重活。
{
"content": {
"user": {
"user": "<string>",
"metas": "map<string>string",
"run_id": "<string>"
},
"proxy_name": "<string>"
}
}
Ping — frpc 心跳
frpc 周期性心跳时触发,触发点见 control.go 中处理 Ping:
{
"content": {
"user": {
"user": "<string>",
"metas": "map<string>string",
"run_id": "<string>"
},
"timestamp": "<int64>",
"privilege_key": "<string>"
}
}
插件可以在心跳上实现“租约”式鉴权:登录时放行,但周期性在 Ping 上复核用户状态,发现被吊销则返回 reject: true,使连接失效。
NewWorkConn — 新工作连接
frpc 建立新的 work 连接时触发(在 run_id 与已有 frp 连接匹配成功后发送),触发点见 Service 处理新工作连接。用于对“单客户端可建立的工作连接数”做限制或审计:
{
"content": {
"user": {
"user": "<string>",
"metas": "map<string>string",
"run_id": "<string>"
},
"run_id": "<string>",
"timestamp": "<int64>",
"privilege_key": "<string>"
}
}
NewUserConn — 新用户连接
有真实用户流量命中代理时触发(支持 tcp、stcp、https、tcpmux 类型),触发点见 proxy.go。注意从源码看该操作的插件调用是旁路式的——NewUserConn 调用 的结果不参与连接建立流程,因此它适合做连接审计/计量,而不是拦截流量(拦截用户连接应使用支持面更广的机制或客户端侧方案):
{
"content": {
"user": {
"user": "<string>",
"metas": "map<string>string",
"run_id": "<string>"
},
"proxy_name": "<string>",
"proxy_type": "<string>",
"remote_addr": "<string>"
}
}
frps 侧的插件配置
在 frps.toml 中通过 [[httpPlugins]] 数组配置多个插件:
# frps.toml
bindPort = 7000
[[httpPlugins]]
name = "user-manager"
addr = "127.0.0.1:9000"
path = "/handler"
ops = ["Login"]
[[httpPlugins]]
name = "port-manager"
addr = "127.0.0.1:9001"
path = "/handler"
ops = ["NewProxy"]
参数说明(对应 HTTPPluginOptions):
| 参数 | 说明 |
|---|---|
name |
插件名称,用于日志标识(如 plugin [user-manager] has been registered) |
addr |
外部 RPC 服务监听地址,默认按 http 处理;https 需显式写 schema:addr = "https://127.0.0.1:9001" |
path |
POST 请求的 URL path |
ops |
该插件需要处理的操作列表,如 ["Login", "NewProxy"];未声明的操作不会路由到该插件 |
tlsVerify |
仅对 https 有意义:默认验证证书,设为 false 可跳过校验(见 http 客户端构建) |
示例展示了“多插件分工”的典型用法:user-manager 只管登录鉴权,port-manager 只管代理创建。同一操作配置多个插件时按配置顺序链式执行(见前文 Manager 语义)。
Metadata:客户端自定义数据的透传通道
客户端可以在配置中携带任意键值对元数据,frps 会随每次 RPC 请求转发给插件,这是插件识别客户端身份、做细粒度策略的关键通道。
元数据分两类:
- 全局元数据:在
Login请求中位于content.metas下,在其他所有请求中位于content.user.metas下; - 代理级元数据:仅出现在
NewProxy请求中,位于content.metas下。
对应的 frpc 配置示例:
# frpc.toml
serverAddr = "127.0.0.1"
serverPort = 7000
user = "fake"
metadatas.token = "fake"
metadatas.version = "1.0.0"
[[proxies]]
name = "ssh"
type = "tcp"
localPort = 22
remotePort = 6000
metadatas.id = "123"
这样,Login/Ping 等请求里 metas 会包含 token、version,而 NewProxy 里除了 user.metas 携带全局值外,metas 还会带上 id。插件侧(例如对接 LDAP/内部 CMDB 的鉴权服务)可以据此实现“用户 + 代理”两级粒度的策略控制。
编写插件服务的实现要点
综合以上协议与源码行为,一个符合 0.1.0 协议的插件服务只需:
- 监听
addr指定的端口,在path上提供POST接口,从 query 中读取version/op,从 body 的content字段读取操作内容; - 按
ops声明处理对应操作,返回三类 JSON 之一(拒绝 / 放行不变 / 放行走替换内容),替换内容时须返回与请求 content 同结构的完整对象; - 出错时直接返回非 200 状态码即可让 frps 判定操作失败;
- 需要关联日志时读取
X-Frp-Reqid请求头; - 对
CloseProxy保持轻量,避免在单客户端多代理场景下成为瓶颈。
相关实现文件汇总:
- 插件接口与操作常量:pkg/plugin/server/plugin.go
- HTTP 客户端实现:pkg/plugin/server/http.go
- 插件管理与链式执行:pkg/plugin/server/manager.go
- 请求/响应与 content 结构:pkg/plugin/server/types.go
- frps 侧注册与各操作触发点:server/service.go、server/control.go、server/proxy/proxy.go
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00