首页
/ frp Server Plugin 详解:用外部 HTTP 服务扩展 frps 的插件化能力

frp Server Plugin 详解:用外部 HTTP 服务扩展 frps 的插件化能力

2026-09-04 15:03:29作者:咎竹峻Karen

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 定义了 nameaddrpathopstlsVerify 五个字段;
  • 注册逻辑位于 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);
  • versionop 同时作为 URL query 参数附带一份;
  • Header 中固定携带 X-Frp-Reqid(每次请求由 newPluginRequestContext 生成的随机 ID)和 Content-Type: application/json,插件侧可以用 reqid 做日志关联追踪;
  • addr 未带协议前缀时默认补 http://;以 https:// 开头时会创建带 TLS 配置的客户端,tlsVerify 控制是否跳过证书校验。

响应格式

插件对每次操作请求可给出以下四类响应之一:

  1. 非 200 HTTP 状态码:frps 直接视为请求失败。源码中 do 方法 对非 200 会返回 do http request error code: %d 错误。
  2. 拒绝操作,并返回原因:
{
    "reject": true,
    "reject_reason": "invalid user"
}
  1. 允许操作,且保持原内容不变
{
    "reject": false,
    "unchange": true
}
  1. 允许操作,并返回修改后的内容(用 content 整体替换原内容):
{
    "unchange": "false",
    "content": {
        ... // 替换后的内容
    }
}

响应结构对应 Response 结构体。值得注意的一个源码细节:当 unchange 为 false 时,frps 会对插件返回的 content 做一次类型断言 retContent.(*T)(见 handleMutableContent),即替换内容必须是与请求 content 相同结构的对象——插件返回的是对整个 content 的替换,而不是差量合并。

Manager 的插件链执行语义

Manager 按操作类型分别维护六个插件列表(loginPluginsnewProxyPlugins 等),注册时依据 IsSupport(op) 将插件分入对应列表。执行语义(handleMutableContent):

  • 同一操作的多个插件按配置顺序串行执行,前一个插件返回的内容会作为后一个插件的输入,形成内容改写链;
  • 任一插件返回 reject: true,操作立即失败,错误信息就是 reject_reason
  • 任一插件请求出错(网络失败/非 200),操作同样失败,frps 日志会记录 send <Op> request to plugin [<name>] error,并带 reqid 前缀便于追踪。

唯一例外是 CloseProxy:它不返回可修改内容,且插件出错时只聚合记录错误而不中断其他插件的执行——这与“代理已经要关闭,插件通知失败不应阻塞清理”的语义相符。

六种支持的 Operation 及其 Content 结构

当前支持的操作为:LoginNewProxyCloseProxyPingNewWorkConnNewUserConn(常量定义见 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"
    }
}

对应结构体 NewProxyContentUserInfomsg.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 — 新用户连接

有真实用户流量命中代理时触发(支持 tcpstcphttpstcpmux 类型),触发点见 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 会包含 tokenversion,而 NewProxy 里除了 user.metas 携带全局值外,metas 还会带上 id。插件侧(例如对接 LDAP/内部 CMDB 的鉴权服务)可以据此实现“用户 + 代理”两级粒度的策略控制。

编写插件服务的实现要点

综合以上协议与源码行为,一个符合 0.1.0 协议的插件服务只需:

  1. 监听 addr 指定的端口,在 path 上提供 POST 接口,从 query 中读取 version/op,从 body 的 content 字段读取操作内容;
  2. ops 声明处理对应操作,返回三类 JSON 之一(拒绝 / 放行不变 / 放行走替换内容),替换内容时须返回与请求 content 同结构的完整对象;
  3. 出错时直接返回非 200 状态码即可让 frps 判定操作失败;
  4. 需要关联日志时读取 X-Frp-Reqid 请求头;
  5. CloseProxy 保持轻量,避免在单客户端多代理场景下成为瓶颈。

相关实现文件汇总:

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

项目优选

收起
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
981
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384