nfrp-client 0.6.1

NFrp 客户端:注册代理、按需建立工作连接
nfrp-client-0.6.1 is not a library.

NFrp 是 fatedier/frp 的 Rust 重实现,完整兼容官方 wire protocol v1 与 v2,可与官方 frps / frpc(v0.71.0 实测)直接互通,第三方 frp 平台下发的配置也能直接用。

默认走 v1,与官方 frp 的默认值一致 —— 所以官方客户端 / 服务端不需要改任何配置就能连上。

资源占用很小:服务端常驻内存约 3.5 MB(同负载下 Go 版 frps 为 28.8~31.8 MB),单文件部署,无运行时依赖。

功能特性

代理类型

八种,与官方 frp 的类型清单完全一致:

类型 说明
tcp 最常用的端口转发
udp UDP 转发,每个代理只占一条工作连接,靠访客地址区分会话
http 按域名路由,支持 locations 前缀、Basic Auth、Host 改写、自定义请求/响应头
https 只嗅探 ClientHello 的 SNI 做路由,不终止 TLS,证书仍由内网服务提供
tcpmux HTTP CONNECT 复用,多条代理共用一个服务端端口,支持 routeByHTTPUser 二级路由
stcp 私密隧道,服务端不开公网端口,secret_key 校验 + allow_users 白名单
sudp 与 stcp 同一套鉴权,数据面为 UDP,同样不占公网端口
xtcp UDP 打洞 + QUIC / KCP 直连,数据不经服务端;打不通自动回退中继。v0.5.8 起 P2P 握手加两层校验(provider 回派生应答口令 + 服务端把牵线 HELLO 绑定到控制连接来源 IP)

传输层

  • frp v1 / v2 双线协议,默认 v1,可用 transport.wireProtocol = "v2" 显式启用
  • QUIC 传输(transport_protocol = "quic"):1-RTT 握手、流级多路复用
  • KCP 弱网通道:既可作为 xtcp 直连通道,也可作为独立传输协议(kcp_bind_port)
  • yamux 多路复用(transport.tcpMux),控制连接与工作连接复用一条 TCP
  • TLS 加密(transport.tls),含官方自定义首字节伪装,服务端自动生成自签名证书
  • WebSocket / WSS(websocket = true),同一端口按路径自动识别,可穿透只放行 HTTP 的防火墙
  • Proxy Protocol v1 / v2,把真实客户端 IP 透传给 Nginx / HAProxy / 后端应用

客户端插件

官方 9 种全部实现:http_proxy / socks5 / static_file / unix_domain_socket / http2http / http2https / https2http / https2https / tls2raw。frpc 本身即可作为正向代理、静态站点或 TLS 终结器。

安全与访问控制

  • OIDC 认证(auth.method = "oidc"),对接 Keycloak / Google / Azure AD 等任意标准 IdP;客户端走 Client Credentials Grant,服务端验签 JWKS(RS256 / PS256 / ES256)
  • RBAC 角色权限表([[roles]]),按序匹配 + deny_unknown / default_role 兜底
  • IP 白 / 黑名单([acl]),CIDR 语法,deny 优先;v0.5.9 起铺满全部数据面入口(TCP / UDP 代理、vhost、VNet、xtcp 牵线、管理面板)
  • 审计日志([audit]),JSONL 追加写 + 内存环形缓冲;关闭时零开销。v0.5.9 起支持 maxFileMB 轮转(默认 64,单份 .1 备份)
  • 凭据比较统一走常量时间;HTTP 转发做逐跳头剥离、Host 规范化、CL+TE 走私拒绝
  • 面板与客户端本地界面绑非回环地址却没有凭据时拒绝启动(逃生开关 allow_insecure_dashboard / allowInsecureRemote)

完整修复清单见 SECURITY-FIXES.md; 第四轮(11 项)见 SECURITY-FIXES-ROUND4.md; 第五轮(39 项)见 SECURITY-FIXES-ROUND5.md; 第六轮(8 项)见 SECURITY-FIXES-ROUND6.md; 第七轮(13 项)见 SECURITY-FIXES-ROUND7.md; 第八轮(5 项)见 SECURITY-FIXES-ROUND8.md。

运维与调度

  • group 负载均衡:同 group 的多个代理共享一个 remote_port,按最小连接数分摊
  • 健康检查:tcp / http 探测,连续失败自动摘除后端,恢复后自动回归
  • 带宽限流:每个代理可配 bandwidth_limit(如 1MB),令牌桶精确限速
  • 资源上限:客户端数 / 代理数 / 转发连接数 / 待处理队列,五个维度全部可限并计入指标
  • 端口白名单:allowPorts + maxPortsPerClient
  • 日志落盘与轮转:log.to + log.maxDays,按天切分与清理
  • 客户端 Store 持久化:动态添加的代理落盘,重启自动恢复
  • 配置热重载:改 log_level / 面板密码无需重启

可观测性与管理

  • 内置 Web 面板 + Prometheus /metrics + 免鉴权健康检查端点,支持 Basic Auth
  • 面板可直接增删代理、踢掉客户端,无需改配置重启
  • Dashboard API v2(/api/v2/*):统一错误信封 + 分页 + 稳定排序;v1 接口行为不变
  • 客户端本地 Web UI / API([webServer] port = 7400):查看隧道 / 连接 / 流量,并可增删代理

工程质量

  • 725 个自动化测试,含真实 QUIC 栈握手、端到端集成测试、与官方 frpc/frps 真实抓包密文的解密回归
  • CI 跑 fmt / clippy / 三系统全量测试 / 两个 musl 目标交叉构建 / 端到端冒烟
  • 第三方 GitHub Action 全部 pin 到 commit SHA;cosign 签名失败即红灯
  • 发布产物附 SHA256SUMS + Ed25519 分离签名

快速开始

构建

cargo build --release
# 产物:target/release/nfrp-server(frps)、target/release/nfrp-client(frpc)

需要 Rust 1.75+。Windows 用 MSVC toolchain。

也可以直接下载发布包,包内二进制名为 frps / frpc(与官方一致)。

服务端(公网机器)

# frps.toml
bind_addr = "0.0.0.0"
bind_port = 7000
token = "换成一个足够随机的口令"

# 可选:xtcp 真 P2P 的牵线端口(需放行 UDP)
p2p_port = 7002

# 可选:内置面板 + Prometheus 指标
dashboard_port = 7500
dashboard_user = "admin"
dashboard_pwd = "换成一个强口令"
./nfrp-server -c frps.toml
# 生成带注释的完整示例配置:
./nfrp-server --gen-config frps.toml

客户端(内网机器)

# frpc.toml
server_addr = "your.server.com"
server_port = 7000
token = "与服务端一致的口令"

[[proxies]]
name = "ssh"
type = "tcp"
local_addr = "127.0.0.1:22"
remote_port = 6000
./nfrp-client -c frpc.toml

之后 your.server.com:6000 即映射到内网机器的 22 端口。

与官方 frp 互通

NFrp 可运行在官方 frp 的任一侧,双向实测通过:

服务端 客户端 状态
nfrp frps 官方 frpc(tcpMux on/off、TLS on、各类代理) 通过
官方 frps nfrp frpc(tcp_mux on/off、TLS on、各类代理) 通过
nfrp frps 官方 frpc(stcp 提供者 / 访客) 通过

兼容标识不可更改:frpc -v 仍输出裸 0.71.0,线协议魔术字与版本串、包内 frps / frpc 命名全部保持。改动这三样会立刻破坏与官方 frp 及第三方平台的互通。

配置

配置字段与官方 frp 对齐,原版 frps.toml / frpc.toml 可以直接使用(支持 camelCase、localIP + localPort 两段式写法、[metadatas] 等)。

两处有意偏离官方,都需要注意:

  • local_addr 是合并式 ip:port(127.0.0.1:8080),不像官方分成 localIP + localPort。官方两段式也认,但两种写法不要混用。
  • 日志轮转按 UTC 零点切分(官方按本地时间),因为日志时间戳本身就是 UTC。

完整的带注释示例见仓库内 assets/frps.toml 与 assets/frpc.toml,也可以用 --gen-config 生成。

升级注意

0.5.2 起有几处行为变更,升级前请确认:

变更 影响 处理
面板绑非回环地址且无 dashboard_user 拒绝启动 配凭据 / 改 bind_addr = "127.0.0.1" / 显式写 allow_insecure_dashboard = true
客户端 [webServer] 绑非回环且无凭据 拒绝启动 逃生开关 allowInsecureRemote = true
本地界面写接口 需要 X-Nfrp-Client: 1 头 调写接口的脚本要加上
空 token 且显式绑对外地址 拒绝启动 配 token / 绑回环 / 显式写 allow_insecure_no_auth = true
[acl] 段拼错的键名 现在会报错(原先静默失效) 检查拼写

0.5.5 起追加三处(都是安全修复的必然结果):

变更 影响 处理
远程下发不再允许携带插件(plugin / pluginUser / pluginPasswd / pluginHostHeaderRewrite / pluginStripPrefix) 面板下发的代理若带插件会被拒 插件只能写在本机配置文件里(它决定客户端"变成什么",不该由服务端远程指定)。要连内网服务请用普通 tcp/http 代理
远程下发不能删/覆盖配置文件里定义的代理 面板对这些代理的增删会返回明确错误 改本机配置文件;面板只管理它自己加的(以及 store 恢复的)动态代理
工作连接需要凭证 对官方 frpc 无影响(官方默认不带凭证,服务端退回"源 IP 一致"校验) 无需处理。NFrp ≥ 0.5.5 客户端会自动声明并携带

0.5.6 起追加三处(都有意的行为变更,见 SECURITY-FIXES-ROUND5.md):

变更 影响 处理
INI 的段名与键名统一小写 代理名会小写化([MCCC] → mccc) 这是修正为与官方一致 —— 官方 go-ini 同样小写化,原先两边会产生不同的代理名。如果你的脚本按大写名引用代理,改成小写
跨租户不得靠 routeByHTTPUser 共用域名 + 路径 跨租户的多作用域注册会被拒(返回"路径已被占用") 服务端分辨不出"合法的多作用域"与"寄生劫持",而后者能直接收割 Basic 凭据,故收紧。同租户的多作用域拓扑(官方 tcpmux 文档的用法)不受影响
UDP 回写限"真的发过包的访客 IP" 客户端无法让服务端向未通信过的 IP 发 UDP 防 frps 被当作 UDP 反射跳板。诚实客户端零影响;TFTP 那类"换端口回包"的协议不受影响(只限 IP、不限端口)

0.5.7 起追加三处(见 SECURITY-FIXES-ROUND6.md):

变更 影响 处理
凭证新鲜性校验铺满全部 6 个入口 时间戳超出 maxClockSkewSecs(默认 ±300 秒)的登录 / 工作连接 / vnet 注册 / visitor 签名 / p2p 打洞签名一律拒绝 对官方 frpc 无影响(官方本来就用当前时间,已实测互通)。只有两端时钟偏差超过 5 分钟时才会失败 —— 症状是"能连上控制端口但登录被拒",错误里会写明偏差多少秒。校准时钟,或把 maxClockSkewSecs 调大(0 = 关闭校验)
跨租户不能用同组名共享公网端口 不同 user 的客户端即使组名与 groupKey 都相同也不能共享端口 这是堵一个实测打出来的劫持(攻击者能分走一半用户流量)。同一租户的多个 frpc(重连、多开、一份配置多条 [[proxies]])照常成组,官方"一个运维跑多个冗余后端"的用法不受影响
visitor 的 run_id 不能留空 用 run_id = "" 的 visitor 连接会被拒 官方 frpc 的 visitor 一定带自己的 run_id,所以零影响。原先空值会绕过会话校验(实测匿名连接能把数据送进别人的隧道)

0.5.8 起追加四处(第七 / 八轮;详单见 SECURITY-FIXES-ROUND7.md 与 SECURITY-FIXES-ROUND8.md):

变更 影响 处理
xtcp 牵线加两层校验:服务端把牵线 HELLO 的来源 IP 绑定到该 session 的控制连接 IP;provider 回按 secret_key+sid 派生的应答口令(替代固定 "RTNL-OK"),visitor 定长比较 修复"在途读到明文 HELLO(sid 泄露)⇒ 冒充 provider 劫持 P2P 会话"(第八轮 B-1,实测可拿到用户明文数据)。混合版本时 P2P 直连会失败并自动回退中继(新↔旧任意组合):功能不受影响,只是少一层直连加速 两端都升到 0.5.8+ 才建立 P2P 直连;单边升级不会让连接失败,打洞窗口(8 秒)过后自动走中继。注意同一 NAT 后仍可能被同 IP 的观察者登记(第二层口令会拦住数据)
面板解析卫生 + 全名删除:Authorization 只从请求头区解析(请求体里同形状的行不再"误投");面板列表展示的线上全名(user.proxy)可直接用于"移除代理" ① 修复前,body 里带 Authorization: Basic ... 的请求能通过面板鉴权(与上游反代/WAF 口径不一致,属解析卫生);② 修复前按列表全名删除会得到 502/400 且两端状态分裂(服务端已摘、客户端报"本地没有") 正常用法无感知;②修复后控制台/脚本从列表复制的名字(全名或短名)都能删
https2http / https2https 上游收尾:访问方断开时立刻放弃等待上游并释放上游连接;上游 connect / TLS 握手加 10 秒超时 修复前上游 accept 后悬挂时,每轮访问(连接→发请求→撤离)会留下一条永不释放的上游连接(可被公开访问方反复放大);"等响应头"阶段仍然没有人为超时,长轮询 / SSE 不受影响 行为对齐官方 frpc(Go http.Server 在客户端断开时取消请求上下文)。上游就是慢(正常出数据)的场景不受影响,已实测
[acl] 开始真正作用于数据面(第七轮 F7-1):公网 TCP 代理端口与 HTTP/HTTPS vhost 的 accept 现在过 IP 白/黑名单(修复前只挡控制连接) 被你 deny 挡掉的来源此前"居然一直能访问",本版起被拒(静默关连接 + 审计事件);用白名单模式(allow)的部署尤其要注意:此前它对数据面形同虚设,本版起按白名单收紧 UDP 代理端口与 xtcp 牵线(p2p)UDP 仍不在 [acl] 范围内(面板另有自身鉴权)—— 如实记录在 SECURITY-FIXES-ROUND7.md。需要挡 UDP 来源请用防火墙(v0.5.9 起 UDP 代理端口 / 牵线 / 面板也已纳入 ACL,见下)

0.5.9 起追加六处(第九轮;详单见 SECURITY-FIXES-ROUND9.md):

变更 影响 处理
[acl] 覆盖面补齐(E9-0 / E9-1 / E9-2):UDP 代理端口、xtcp 牵线 UDP、管理面板从此也过 IP 白 / 黑名单 修复前 UDP 代理端口完全不遵守 [acl](无需任何凭据即可把 UDP 包送进内网服务);牵线与面板同样漏过(面板另有独立 Basic Auth,属纵深防御缺失) 用白名单(allow)模式的部署请确认放行面;只想挡 UDP 来源也可以继续用防火墙。升级后来源突然被拦属于配置本意(此前是静默失效)
未认证连接的时间上界 + 全局闸门(C9-3 / C9-4):TLS 握手 30s 超时、yamux 空转会话看门狗(30s)、未认证握手全局并发闸(2048,排队 5s);0x00 / 0x16 / 0x17 单字节后静默的连接不再永久驻留 修复前"发一个字节然后静默"即可无限挂住连接(实测 500 条 0x00 占 500 fd + 6.4 MB);未认证开流可放大 ≈800×(12 字节换 9.5 KB) 常规流量零感知。极端饱和(同时 >2048 个未认证握手)时新连接排队 5 秒后被拒 —— 已有会话与合法工作流不受影响,含合法客户端重新登录
审计日志轮转 maxFileMB(A9-3,默认 64;0 = 不轮转) 修复前审计文件可被公开流量灌到任意大(实测 ~1000 条/秒、37 MB/h 起、无上界,还会挤掉环形缓冲里的真实事件) 轮转保留单份 <path>.1 备份,总占用 ≈ 2× 阈值;需要更长历史请自行归档 .1。要恢复旧的无限增长行为设 maxFileMB = 0
stcp / xtcp visitor 默认配置恢复可用(A9-5,修 0.5.7 / 0.5.8 的功能回归) 双方都未配 user 时(官方默认语义 "" == ""),0.5.7 / 0.5.8 会"能登录、能配对、但数据永远不通" 配了 allow_users 的部署行为不变;若你此前为绕开问题给双方硬配了 user,本版起可以去掉
KCP 会话表封顶 1024(C9-1,仅启用 kcp_bind_port 时):同时最多 1024 条 KCP 会话,超出丢弃新会话(不回包,已有会话不受影响) 修复前 25 字节的 UDP 包可换 ≈15 KB 内存(放大 312×,实测 757 KB 上行把内存推高到 238 MB) 未启用 KCP 的部署不受影响;单实例 KCP 会话数持续 >1024 的部署请拆分实例
客户端启动即点名插件配置错误(D9-7):原先 Windows 上 unix_domain_socket 配置错误仅 DEBUG 一行,默认级别完全静默 影响面很小(配置错误本来就不工作),只是从"看不见"变成"一眼看见" 无 —— 看到 WARN 按提示修配置即可

其余需注意:

  • OIDC 部署必须配 TLS。OIDC 的 privilege_key 是原样的 access token,会明文出现在登录报文里;不加密链路上任何被动抓包的人都能拿走它。
  • 客户端 transport.tls 只加密、不校验对端证书(与官方 frp 行为一致)。它防被动偷看,不防中间人;跨公网直连且需要防 MITM 时请在外层再套一层 TLS。
  • v1 的登录凭据强度完全取决于 token。v0.5.7 起服务端校验时间戳新鲜性(默认 ±300 秒窗口,见上表),把重放面从"无限期"压到"5 分钟";但窗口内仍可重放,且离线枚举 token 的成本与时间窗口无关 ⇒ 跨公网部署请用足够随机的 token,并优先 v2 / OIDC。
  • user 是客户端自报字段,服务端无法验证。未配 [[roles]] 时它只是标签;配了 [[roles]] 之后也一样 —— 任何持有共享 token 的人都能把自己报成别人的 user 从而拿到那个人的角色。⇒ [[roles]] 不是租户隔离,只是一层防误操作(v0.5.7 起服务端会就此打启动告警)。需要真正的隔离请用 OIDC(身份来自验签过的 sub),或给每租户发独立 token 并分实例部署。
  • xtcp 的"数据不经服务端"不包含"防服务端":服务端持有 secret_key 且自行生成 sid,理论上能算出 P2P 口令。它防的是路径上的第三方,不是服务端本身。
  • 对手不想暴露内部细节时,可设 detailed_errors_to_client = false(默认与官方一致为 true):它会把发给客户端的错误换成统一短句,顺带收掉 stcp/xtcp 隧道名的枚举面。
  • UDP 代理的回写目标受限(v0.5.6 起):服务端只向"最近真的给该端口发过包的访客 IP"回写 UDP 报文。这是防 frps 被当成 UDP 反射跳板 —— 正常客户端本来就只回写给刚收到包的地址,不会感知。
  • 示例配置里默认不包含任何代理,需要什么自己打开对应的注释段。示例不再默认暴露 SSH。

质量保障

cargo fmt --all -- --check                # 格式
cargo clippy --workspace --all-targets    # 静态检查(当前 0 告警)
cargo test --workspace                    # 732 个测试

测试分布:

目标 数量
common 单元测试 344
server 单元测试(lib) 232
server 单元测试(bin) 17
client 单元测试 115
server 端到端集成测试 24
合计 732

★ 上表是 v0.5.9 实测值(cargo test --workspace 逐个 target 数的)。 v0.5.8 的 725 加本轮 +7(第九轮 9 条修复 + 1 条加固的回归)= 732。 数字对不上时以 cargo test 的输出为准。

CI(.github/workflows/ci.yml)在每次 push / PR 上跑 fmt --check + clippy -D warnings,然后在 Linux / Windows / macOS 上跑全量测试,再做两个 musl 目标的交叉构建与端到端冒烟。

发布与部署

打包

# 打三平台包(包内统一命名 frps / frpc,附示例配置与 README)
python scripts/release.py pack \
  --target windows-amd64:target/release \
  --target linux-amd64:/path/to/amd64 \
  --target linux-arm64:/path/to/arm64 \
  --out dist/release

# 生成 SHA256SUMS(--sign 还会产出 Ed25519 分离签名)
python scripts/release.py checksum --dir dist/release --sign

# 本地验签 + 校验
python scripts/release.py verify --dir dist/release

Linux 包在云端交叉编译(本机没有 Linux C 工具链,ring 需要编译 C)。arm64 走 +crt-static 全静态链接,无 glibc 依赖。

release.py 不依赖任何第三方 Python 包,也不依赖 Rust 工具链。签名缺失或失败会非零退出;确实要发未签名制品需显式加 --allow-unsigned。

Docker

docker build -t nfrp .
docker run --rm -p 17000:17000 -p 17002:17002/udp \
  -v $PWD/frps.toml:/etc/nfrp/frps.toml nfrp

镜像以非 root 用户运行,多阶段构建(musl 静态),最终镜像只含两个二进制与示例配置。

Dockerfile 有意不加 HEALTHCHECK:唯一合适的探针 - /api/healthz 挂在面板端口上,而示例配置里 dashboard_port 默认是注释掉的,写死探针会让容器一直显示 unhealthy。理由已写在文件里。

当前限制

  • VirtualNet 在 Windows 上未实现(需要 TUN 设备),Linux 与 Android 上可用;配置了会明确报错而不是静默失败
  • 官方 frp 的 xtcp NatHole UDP 地址交换协议未实现,所以官方 frpc 的 xtcp visitor 打不了洞;服务端会明确拒绝并让官方客户端立刻转走它自己的 fallbackTo 中继
  • 与官方 0.71 相比仍有部分配置字段未实现,启动日志会逐条列出「配了但不会生效」的字段

License

本项目基于 Apache License 2.0 发布。

Copyright 2026 nfrp contributors