Skip to main content

frpsctl

把 frp 服务端(frps)包装成命令行工具 + 内置 Web 管理台。

CLI 负责精确控制与脚本化,Web 管理台负责可视化与日常操作——两者共用同一套核心逻辑, 同样的操作不会有两套行为。frps 二进制始终是官方原版:本工具只做 配置翻译器、进程保镖、状态聚合器,转发逻辑一行都不碰。

设计哲学:配置合法性走官方 frps verify;状态采集走官方 v2 Admin API; 生命周期走进程管理 / systemd。完整设计(含每条事实的源码依据与复现命令)见 frpsctl-设计方案.md。


目录


介绍

它解决什么问题

直接用官方 frps 管服务端,有四件事必须手工完成,且都容易出错:

痛点 frpsctl 的做法
手写 TOML:字段是驼峰、嵌套结构、取值范围分散在文档各处,写错了要等启动才报错 init 交互式生成安全基线配置;config set 改单键,注释与排版原样保留
配进程守护:frps -c frps.toml 前台运行,关掉 SSH 就断 start 派生后台进程(脱离会话),管 pid、优雅停止、开机自启(systemd)
看状态靠翻日志:谁在线、有几个代理、跑了多少流量 status 一条命令聚合,支持 --json;Web 仪表盘自动刷新
改配置必然重启:frps 没有热重载,改一个端口就要停服 config set 走带回滚的事务:校验 → 备份 → 替换 → 重启 → 失败自动恢复

两副面孔,一套内核

CLI——精确、可脚本化、可接 CI:

分类 命令
二进制与配置 install / init / verify
生命周期 start / stop / restart / status / log
配置子命令 config get / set / unset / edit / list / diff / rollback(set 支持 --dry-run / --stdin / --prompt)
观测与运维 clients / proxies / traffic / instances / doctor / prune
卸载 uninstall(--all / --keep-data / --keep-bin / --force)
systemd 集成 service install / uninstall / status / logs
服务端插件 plugin init / check / serve、`plugin user set
Web 管理台 web serve、`web service install

Web 管理台——浏览器中的同等能力(web serve 启动,默认只绑回环):

区域 能力
仪表盘 实例状态 / 三层健康 / 概览统计 / 近 7 天流量图(按需下钻单代理)/ 会话内实时速率曲线 / 客户端与代理列表(可展开代理曲线)/ 日志(跟随与暂停、行数可选)/ 系统体检(只读 doctor,按钮触发)
配置 逐字段表单(敏感值打码提示)→ 预览 diff(+绿/−红)→ 应用(一次事务、一次重启)→ 失败自动回滚;可删除键、可新增键
历史 快照列表,先看差异再回滚
审计 插件审计只读视图:统计(允许 / 拒绝 / 限速抑制 / 坏行)+ 按用户与按操作分布 + 最近记录;策略缺失或关闭时说明原因而不报错
配置编辑增强 键名搜索过滤;数组/内联表值即时校验;有未保存修改时离页确认
进程 启动 / 重启 / 停止 / 清理离线记录(返回清理条数;操作期间按钮禁用、状态徽章显示"操作中…")
主题 明暗双主题:跟随系统(可实时变化)或手动切换

一条硬边界

绝不重新实现 frp 已有的能力。

能力 由谁提供
配置合法性判定 官方 frps verify -c(唯一权威)
状态、统计、代理列表 官方 v2 Admin API
服务进程 官方 frps 二进制,或 systemd
配置生成、进程编排、失败回滚、终端/浏览器呈现 frpsctl

环境要求

项 要求 为什么
操作系统 仅 Linux 进程身份校验依赖 /proc/<pid>/stat;互斥用 flock;原子写用 fchmod
Python ≥ 3.11 依赖标准库 tomllib
frps ≥ 0.70.0 v2 Admin API 自 0.70.0 引入,本工具只用 v2
建议版本 0.71.0 0.70.x 可用,但缺少一个已知远程 DoS 的修复(start / doctor 会告警)

非 Linux 内核、或未挂载 /proc 的容器会被直接拒绝启动,而不是降级—— 身份校验失效的代价是杀掉无关进程。


安装

方式一:一键安装(uv,无需 Python)(推荐)

curl -LsSf https://astral.sh/uv/install.sh | sh && uv tool install frpsctl

一条命令搞定。uv 是单文件静态二进制,自带 Python 版本管理——服务器上 没有 Python 时它会自动下载一个独立构建,因此这是唯一不依赖系统 Python 的 安装方式。装完命令在 ~/.local/bin/frpsctl。

uv tool upgrade frpsctl          # 升级
uv tool uninstall frpsctl        # 卸载(不动实例数据)

方式二:pipx / pip(已有 Python ≥ 3.11)

pipx install frpsctl        # 或:pip install frpsctl
# 升级:pipx upgrade frpsctl / pip install -U frpsctl

建议开启 shell 补全(支持 bash/zsh/fish):

frpsctl --install-completion

方式三:源码安装

一键脚本(两种跑法,效果相同):

# A. 先 clone 再运行
git clone https://github.com/ThzxxArt/frpsctl.git
cd frpsctl
./install.sh

# B. 在线直跑:脚本自动把源码下载到数据目录(无需 git)
curl -fsSL https://raw.githubusercontent.com/ThzxxArt/frpsctl/main/install.sh | bash

脚本做的事:建一个独立 venv → 装依赖 → 把 frpsctl 注册到 ~/.local/bin → 自检。 不需要 pipx、不需要 uv、不需要 sudo(venv 是标准库自带的)。

$ ./install.sh
检查运行环境
 ✓ 操作系统:Linux
 ✓ Python:Python 3.13.13(/usr/bin/python3)
 ✓ venv 模块可用
 ✓ 源码目录:/mnt/d/CodeWorkspace/frpsctl(脚本同目录)

创建虚拟环境
 ✓ 已创建:/home/u/.local/share/frpsctl-src/venv
安装 frpsctl 及其依赖
 ✓ 依赖就绪(typer / pydantic / tomlkit / httpx)
注册全局命令
 ✓ 已注册:/home/u/.local/bin/frpsctl
自检
 ✓ 命令可用:frpsctl 0.2.5

frpsctl 安装完成
选项 作用
(无) 装到 ~/.local(命令 → ~/.local/bin/frpsctl)
--system 装到 /usr/local(需要 sudo)
--prefix DIR 自定义前缀
--uninstall 卸载(删 venv 与命令,不动实例数据;不需要 Python)
--no-verify 跳过安装后自检
环境变量 作用
FRPSCTL_INSTALL_REF 固定源码版本(tag / 分支,默认 main),管道安装用
FRPSCTL_INSTALL_URL 覆盖源码 tarball 地址(镜像 / 离线内网用)

反复运行即为升级(会重新装依赖并重写命令)。源码用 -e 方式安装,因此改完 源码无需重装,命令立即生效。

脚本不下载 frps 二进制——那是 frpsctl install 的职责(需要网络与校验和, 且要写入用户数据目录)。

手动安装:

git clone https://github.com/ThzxxArt/frpsctl.git && cd frpsctl
uv venv && uv pip install -e ".[dev]"
.venv/bin/frpsctl --version          # 直接用 venv 里的命令,不注册全局

# 或让 pip 直接装到用户环境
pip install --user -e .

服务器没有 Python 怎么办

三条路,按省事程度排序:

  1. 用 uv(推荐)——uv 自带 Python,系统什么都不用装:

    curl -LsSf https://astral.sh/uv/install.sh | sh && uv tool install frpsctl
    
  2. 装一个 Python 再走本文任一方式(需要 ≥ 3.11 且 venv 组件完整):

    sudo apt install python3 python3-venv     # Debian / Ubuntu
    sudo dnf install python3                  # Fedora / RHEL
    sudo apk add python3                      # Alpine
    

    最小化安装的系统常出现"有 python3 但缺 venv/ensurepip"——install.sh 会在 第一步检测出来,并给出上面两条命令;检测到完全没有 Python 时,会把 uv 一键命令直接打印出来,而不是只报一句错。

  3. 独立二进制:当前不提供(设计文档 ADR-4 评估过"只打 Python 侧、收益 有限",内嵌 frps 明确拒绝)。

安装 frps 二进制

Python 侧装好后,用 frpsctl install 下载官方 frps(sha256 强校验):

$ frpsctl install
frps 0.71.0 → /home/u/.local/share/frpsctl/bin/frps-0.71.0
当前版本软链 → /home/u/.local/share/frpsctl/bin/frps

注意:换链**不影响正在运行的进程**(Linux 上可执行映像已绑定 inode),
      只影响下一次 start。运行 `frpsctl status` 可对比两个版本。
  • 下载地址可被镜像替换,但信任锚是官方校验和文件(frp_sha256_checksums.txt)。 内置了官方源与 ghproxy;需要其他镜像时:

    frpsctl install --mirror https://my-mirror.example/frp/releases/download
    # 或
    FRPSCTL_MIRROR=https://a.example,https://b.example frpsctl install
    

    命令行给的镜像会替换(而不是追加)内置源——指定镜像通常意味着"内置源 在我这里不通"。

  • 拿不到校验和就拒绝安装(fail-closed)。确有需要可 --insecure 跳过,风险自负。

  • 低于 0.70.0 的版本直接拒绝——装了也用不了。

  • 幂等:同版本已在盘上时不重复下载,但仍会校正 bin/frps 软链——手工改歪的 链会被修回来。想"只落盘、不动链"请显式加 --only-download。

--with-frpc 会从同一个 tar 包里额外取出 frpc(测试专用件,供插件契约测试用), 不产生额外下载:

frpsctl install --with-frpc        # frps + frpc

验证安装

frpsctl --version                  # frpsctl 0.2.5
frpsctl install                    # 下载 frps 二进制
frpsctl init                       # 生成配置(下一步是五分钟上手)

卸载

frpsctl uninstall              # 卸载当前实例(先列出清单,要求确认)
frpsctl uninstall --all        # 卸载实例根下的全部实例
frpsctl uninstall --keep-data  # 保留配置/快照/审计,只停 unit 与删共享二进制
frpsctl uninstall --keep-bin   # 保留共享二进制(多实例机器上只卸一个实例)
frpsctl uninstall --force      # 运行中的实例先停止再卸载(默认拒绝)
  • 默认只卸当前实例(--instance 指定,默认 default)。共享二进制(bin/) 要求目标覆盖全部实例才能删——多实例机器上删掉它会让其他实例起不来,此时 要么 --all,要么 --keep-bin。

  • ⚠️ 删除不可恢复:实例目录含 auth.token、dashboard 口令、配置快照与插件 审计。因此默认先列出将删清单并要求确认;--yes 跳过确认;--json 下必须 显式 --yes(破坏性操作不做隐式确认)。

  • 运行中的实例默认拒绝卸载(退出码 11,数据分毫未动);--force 会先停止 再卸载。systemd 托管的实例同样处理。

  • 权限不足或不该代删的东西不会静默跳过:unit 清理需要 root(汇总"未清理项" 并给出命令);/var/log/frps 与 frps 服务账户只提示、不代删(可能另有用途)。

  • 多实例机器上只卸一个实例时,共享 unit 模板(frps@.service 等)会保留, 只停用当前实例的 unit——删模板会连累其他实例。

  • Python 包本身用包管理器移除:

    pipx uninstall frpsctl            # 或 pip uninstall frpsctl
    ./install.sh --uninstall          # 源码一键脚本安装的(不动实例数据)
    

五分钟上手

$ frpsctl init
将生成一份安全基线的 frps 配置。直接回车使用方括号中的默认值。

控制端口 bindPort [7000]: 
dashboard 端口(0 = 不启用,将失去状态聚合能力) [7500]: 
允许客户端申请的端口段(如 6000-6100;留空 = 不限,不推荐): 6000-6100

已生成 /home/u/.local/share/frpsctl/instances/default/frps.toml(权限 0600)

  auth.token          ZuqUzY4Huv588VJzEspC-0GZTiXl6dY9
  webServer.user      admin
  webServer.password  NIY0Z-zd8dmeTg59YXbITe9z

⚠ 上面三项只会显示这一次,配置里已写入(文件权限 0600)。

下一步:frpsctl verify && frpsctl start

把这三项记下来——auth.token 要填到每个 frpc 客户端,dashboard 口令用于 prune、Web 管理台等操作。

$ frpsctl verify
/home/u/.local/share/frpsctl/instances/default/frps.toml 校验通过(frps 0.71.0,标志:--strict_config=true)

$ frpsctl start
已启动:pid 179841,frps 0.71.0
health   : L1 process ok  L2 control ok  L3 plugin skipped

$ frpsctl status
instance : default            owner : direct
state    : RUNNING (pid 179841, up 1s)
binary   : frps 0.71.0
config   : /home/u/.local/share/frpsctl/instances/default/frps.toml (0600)
listen   : 0.0.0.0:7000
dashboard: 127.0.0.1:7500 (auth: on)
health   : L1 process ok  L2 control ok  L3 plugin skipped
clients  : 0 online
traffic  : in 0 B / out 0 B  (conns now 0)

frpc 侧对应配置:

serverAddr = "your-server"
serverPort = 7000
auth.token = "上面那个 auth.token"

[[proxies]]
name = "my-ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 6000        # 必须落在 allowPorts 范围内

init 生成的配置长这样(每条安全项旁边都写了它为什么在那儿):

# frps 配置 —— 由 frpsctl init 生成
# 权限已设为 0600:文件内含 auth.token 与 dashboard 口令。

# ── 顶层键(必须写在任何 [table] 之前)─────────────────────
bindAddr = "0.0.0.0"
bindPort = 7000

# 单客户端可申请的端口数上限。frp 默认为 0(不限),
# 不设的话单个客户端就能把端口耗尽。
maxPortsPerClient = 20

# 端口白名单:只允许客户端申请这些端口 / 端口段。
# 不设的话,任何持有 token 的客户端都能申请任意端口。
allowPorts = [
  { start = 6000, end = 6100 },
]

[auth]
# 为空表示不校验客户端 token(等于没有客户端鉴权)。
token = "……"

[webServer]
# dashboard 只有 Basic Auth 一层防护,因此默认只监听本机。
# 改成 0.0.0.0 之前请确认口令已设。
addr = "127.0.0.1"
port = 7500
user = "admin"
password = "……"

[transport.tls]
# 拒绝明文 frpc 连接(frp 默认是 false)。
force = true

[log]
to = "./frps.log"
level = "info"
maxDays = 7

⚠️ TOML 位置纪律:allowPorts、maxPortsPerClient 这类顶层键必须写在任何 [table] 之前。写错了它们会变成那张表的子键,而 frps 的报错是极具误导性的 unknown field "allowPorts"——看起来像键名错了,实际是位置错了。

接下来:想用浏览器看,直接读 Web 管理台教程;想在终端里 深入操作,读 CLI 使用教程。


CLI 使用教程

全局选项与命令总览

--instance, -i NAME    实例名(默认 default,可用 FRPSCTL_INSTANCE 覆盖)
--root PATH            实例根目录(默认 ~/.local/share/frpsctl/instances)
--config PATH          直接指定配置文件(覆盖实例默认)
--binary PATH          直接指定 frps 二进制
--json                 机器可读输出(所有查询类命令支持)
--admin-password       dashboard 口令(优先于配置文件)
--yes, -y              跳过交互确认
--verbose, -v          详细输出(把外部命令与判定过程打进 stderr)
--version              frpsctl 版本

长名可以写在任意位置,短名 -v 只能写在子命令之前:

frpsctl status --json
frpsctl config get bindPort -i web
frpsctl verify --verbose

frpsctl --verbose status 与 frpsctl status --verbose 完全等价。--verbose 只进 stderr,因此 --verbose --json 的 stdout 仍是干净的 JSON,可以直接管道给 jq:

$ frpsctl verify --verbose
[trace] 读取二进制版本:~/.local/share/frpsctl/bin/frps-0.71.0 -v
[trace] 二进制版本:0.71.0(退出码 0)
[trace] 执行权威校验:~/.local/share/frpsctl/bin/frps-0.71.0 --strict_config=true verify -c …/tmpXXXX.toml
[trace] verify 退出码 0
…/instances/default/frps.toml 校验通过(frps 0.71.0,标志:--strict_config=true)

教程 1:看状态

frpsctl status              # 人读
frpsctl status --json       # 机器可读(前后位置都行)
frpsctl status --watch      # 持续刷新
frpsctl status --watch --json   # 持续输出单行 JSON(NDJSON),可逐行消费

status 永远以退出码 0 结束(除非参数写错)——它的职责是回答"现在什么情况", 而不是失败。异常情况会如实报告:

$ frpsctl status            # 实例没在跑
instance : default            owner : none
state    : STOPPED
config   : /home/u/.local/share/frpsctl/instances/default/frps.toml (0600)
listen   : 0.0.0.0:7000
dashboard: 127.0.0.1:7500 (auth: on)
{
  "instance": "default",
  "owner": "direct",
  "state": "RUNNING",
  "pid": 179910,
  "uptime_seconds": 1.72,
  "binary_version": "0.71.0",
  "disk_version": "0.71.0",
  "config_mode": "0600",
  "listen": { "addr": "0.0.0.0", "port": 7000 },
  "health": {
    "l1_process": "ok",
    "l2_control": "ok",
    "l3_plugin": "skipped",
    "detail": "/healthz 200, 2ms"
  },
  "clients": 0,
  "proxy_type_counts": {},
  "proxy_total": null
}

三个健康层,含义不同:

层 探针 回答的问题
L1 进程 pid + 启动时刻 + 命令行三重校验 进程还在,且确实是我们的
L2 控制面 GET /healthz(免认证) frp 的 HTTP 服务在正常应答(webServer.port = 0 时为 skipped)
L3 插件面 对 httpPlugins[].addr 做 TCP 探测 登录链路是否可能成功(未配插件时为 skipped)

start / restart 的成功判据是 L1 ∧ L2(gate)。gate 未通过时退出码为 12,同时向 stderr 告警——但进程不会被清理:它仍由本工具托管, status 看得见、stop 停得掉。直接重试 start 只会得到"已在运行(6)"。

L3 失败不改变退出码,但会显著告警——因为插件是 fail-closed 的:插件不可达 意味着所有客户端都无法登录。

owner 字段说明"谁在管这个进程",永远无歧义:

owner 含义
direct 由 frpsctl 直接托管(state.json 是权威)
systemd 由 systemd 托管,start/stop/restart 委托 systemctl
none 没有进程在跑,也没有 unit

教程 2:看客户端、代理与流量

status 给的是总数;"谁在线、哪个代理在跑、各跑了多少流量"用这两条 (走 v2 Admin API,自动翻页取全量,--json 可管道给 jq):

$ frpsctl clients
name                         user       hostname             online  ip               version
alice.f2a3e2edeef4a920       alice      DESKTOP-S8A3AVK      True    127.0.0.1        0.71.0

$ frpsctl proxies
name                         user       type    port   phase    conns  traffic(in/out)
alice.alice-ssh              alice      tcp     6000   online   0      0 B / 0 B

$ frpsctl proxies --type http      # 只看某类型

近 7 天的流量历史(日粒度;数据源与 Web 趋势图相同):

$ frpsctl traffic                  # 全部代理逐日汇总
date                  in         out
2026-09-16       1.2 MiB     3.4 MiB
2026-09-17       0.4 MiB     1.1 MiB
合计             1.6 MiB     4.5 MiB

$ frpsctl traffic alice.alice-ssh  # 单个代理的明细

离线或已删除的代理在数据源上返回 404 = 无数据(不是错误);单个代理查询失败 也不拖垮整体。代理数超过 50 时会明确提示"仅统计前 50 个"(Web 同理)。

教程 3:改配置(核心事务)

frps 没有热重载,所以"改配置"和"重启"是同一件事。config set 把它实现为一次 带回滚的事务:

$ frpsctl config set maxPortsPerClient 30
--- a/frps.toml
+++ b/frps.toml
@@ -7,7 +7,7 @@
 
 # 单客户端可申请的端口数上限。frp 默认为 0(不限),
 # 不设的话单个客户端就能把端口耗尽。
-maxPortsPerClient = 20
+maxPortsPerClient = 30
 
 # 端口白名单:只允许客户端申请这些端口 / 端口段。
 # 不设的话,任何持有 token 的客户端都能申请任意端口。
 
✓ 已写入并重启,健康检查通过

注意 diff 里注释和排版都原样保留——config set 只对目标键做定点赋值, 文件其余字节完全不变。模型没覆盖的键(allowPorts、httpPlugins…)也不会被丢掉。

事务的 9 个步骤:

取锁 → 内存定点补丁 → 语义校验 → 官方 verify → 备份 → 原子替换
     → 重启 → 健康检查 → 失败自动回滚(退出码 9,并如实告知回滚结果)

常用变体:

frpsctl config set bindPort 8000 --no-restart   # 只写不重启(输出会提示"尚未生效")
frpsctl config set bindPort 8000 --dry-run      # 只校验并展示 diff,不写入、不重启
frpsctl config set webServer.password --prompt  # 敏感值隐藏输入(不进 argv / shell 历史)
frpsctl config set webServer.password --stdin   # 或从管道读:echo -n "$PW" | frpsctl ...
frpsctl config unset maxPortsPerClient          # 删键回落 frp 默认值(同一事务闭环)
frpsctl config list                             # 列出全部键(值自动打码)
frpsctl config list --prefix webServer          # 只看某张表
frpsctl config list --tree                      # 按表分组缩进展示
frpsctl config get bindPort                     # 读单键
frpsctl config get auth                         # 读整张表(机密自动打码)
frpsctl config get auth.token --reveal          # 需要看原值时显式索取
frpsctl config edit                             # 用 $EDITOR 改,保存后走同一闭环
frpsctl config diff                             # 当前 vs 上一份快照
frpsctl config diff --steps 3                   # 当前 vs 第 3 新的一份
frpsctl config rollback                         # 回滚到上一份
frpsctl config rollback 3                       # 回滚到 3 份之前

config list 的输出(值自动打码):

$ frpsctl config list --prefix webServer
webServer.addr = "127.0.0.1"
webServer.port = 7500
webServer.user = "admin"
webServer.password = hQ***x3

--dry-run 与 --no-restart 的区别:前者什么都不写(只校验+预览), 后者已经落盘、只是没有重启;config unset 删除不存在的键会报配置错误(3) ——拼错键名的"成功删除"会让人以为清掉了某个设置。

--no-restart 之后别忘了 frpsctl restart 让变更生效(status 会提示)。

机密保护:config get 默认打码(hQ***x3 形式,保留首尾便于核对是不是同一个 值),--json 与所有 diff 输出同样打码。要看明文必须 --reveal。

拒绝危险组合:config set 与 doctor 双重拦截"dashboard 绑非回环 且 user/password 都为空"。frp 在两者同时为空时完全不鉴权(不是"要求登录"), 因此那种配置等于把状态读取与代理下线的权限交给网络上任何人。

$ frpsctl config set webServer.password '""'      # 先清空口令(此时仍只监听回环,安全)
$ frpsctl config set webServer.addr '"0.0.0.0"'   # 这一步会让它变成完全无鉴权 → 被拒绝
错误:拒绝写入危险配置:webServer 绑定非回环地址 0.0.0.0:7500,且 user/password 均为空
      = **完全不鉴权**(任何人都能读取全部状态并下线任意代理)
提示:请先设置 webServer.user 与 webServer.password,或把 webServer.addr 改回 127.0.0.1

反向也成立:只要口令非空,绑非回环是允许的(远程看 dashboard 是常见需求, 此时有 Basic Auth 保护)。拦截的是"无鉴权 + 对外暴露"这个组合本身。

教程 4:看日志

frpsctl log                 # 最近 100 行
frpsctl log -n 500          # 最近 500 行
frpsctl log -f              # 持续跟踪(tail -f)
frpsctl service logs -f     # systemd 模式:unit 级日志(journalctl -u)

日志路径取自配置里的 log.to(相对路径按实例目录解析)。frpsctl 不写这个 文件——它由 frp 自己写并按天轮转。多一个写入者会和轮转互相破坏。

service logs 看的是 journald 里的 unit 级日志(启动失败、OOM、权限拒绝 这类"frp 还没写进自己的日志文件"的问题),两者互补。

教程 5:启停

frpsctl stop                # SIGTERM → 轮询确认 → 超时 SIGKILL
frpsctl stop --timeout 3    # 缩短等待
frpsctl stop --force        # 直接 SIGKILL
frpsctl restart             # stop → start

frps 没有信号处理器,SIGTERM 即进程立即终止,不存在"存量连接收尾"。 SIGKILL 兜底是为了应对卡死,不是为了"更彻底"。

安全特性:停止前会做三重身份校验(pid 存活 + 启动时刻 + 命令行)。任何一项 对不上就拒绝停止(退出码 11),绝不冒险 kill 一个可能无关的进程:

$ frpsctl stop
错误:pid 12345 存活但身份校验不通过,拒绝停止
提示:该 pid 可能已被复用为无关进程。确认后手工删除 …/state.json 即可恢复

教程 6:体检与巡检

$ frpsctl doctor
[INFO ] 二进制版本: frps 0.71.0 受支持
[INFO ] 配置校验: semantic + frps verify 均通过(标志:--strict_config=true)
[INFO ] 配置文件权限: 0600 正常
[INFO ] 端口可绑定性: bindPort = 7000 已被占用(本实例正在运行,属正常)
[INFO ] 进程所有权: 由 frpsctl 直接托管(direct)

体检通过

检查项与严重度:

检查 级别 说明
二进制存在 / 可执行 / 版本 ERROR < 0.70.0 拒绝;0.70.x 提示缺 DoS 修复
配置可解析 + frps verify ERROR 双保险
配置文件权限 ERROR / WARN 含 token 却非 0600 → ERROR
dashboard 暴露面 ERROR 绑非回环 且 user/password 全空 = 完全不鉴权
dashboard 弱口令 WARN user/password 全空;password 为空而 user 非空(frp 把空口令当合法口令);或 admin/admin
transport.tls.force WARN 未开启时可接受明文 frpc
allowPorts / maxPortsPerClient WARN 未设置时端口可被任意申请
端口可绑定性 ERROR 探测各监听端口;自己占着会识别为"正常"
< 1024 端口 INFO 提示需 CAP_NET_BIND_SERVICE
进程所有权冲突 ERROR systemd 与 direct 同时成立
插件暴露面 ERROR 插件回调指向非回环(协议无认证)
插件可达性 WARN 不可达时提示"客户端将无法登录"
Web 口令文件权限 WARN web service install 生成的口令文件权限过宽(应 0600)

有 ERROR 时退出码为 1,可直接接进 CI 或监控。

多实例巡检用 instances(一行一个:owner / 状态 / pid / 版本 / 健康):

$ frpsctl instances
default          direct   RUNNING (pid 10582, up 2m10s)  frps 0.71.0  L1 process ok  L2 control ok  L3 plugin skipped
web              systemd  SYSTEMD_ACTIVE (pid 20041)

默认不做网络探测(快速);加 --health 会对运行中的实例跑三层健康检查。 instances --json 输出与 status --json 同构的数组。

教程 7:清理离线记录

frpsctl prune               # 清理 dashboard 统计里的离线代理记录

⚠️ frp 没有强制下线在线代理的 API——DELETE /api/proxies 的实际语义是 ClearOfflineProxies()(只接受 ?status=offline,源码与真机均已核实)。 要断开某个客户端请停掉它的 frpc。此前版本的 kick 基于对该端点的误读, 从未真正工作过,已由 prune 取代。


Web 管理台教程

启动与登录

frpsctl web serve                                  # 默认只绑 127.0.0.1:8787
frpsctl web serve --bind 127.0.0.1:9000            # 换端口
FRPSCTL_WEB_PASSWORD=my-pw frpsctl web serve       # 指定口令(默认自动生成并打印一次)
frpsctl web serve --password-file ./web-password   # 从 0600 文件读口令(systemd 部署用)
$ frpsctl web serve
Web 管理台:http://127.0.0.1:8787/
登录口令(仅显示这一次):Ih2x...(24 字符)
Ctrl-C 停止。

浏览器打开上面的 URL,输入口令即进入主界面。会话默认 8 小时(只存服务端内存, 重启即失效);刷新页面不会掉线(Cookie 还在,前端自动恢复会话)。

忘了口令? 分两种情况:

  • systemd 部署(web service install 会把口令写入实例目录的文件):

    frpsctl web password show        # 文件不存在时报配置错误;权限过宽时向 stderr 告警
    
  • 前台 web serve 且口令是自动生成的:它只在启动时打印一次、不落盘, 因此无法找回——重新启动并显式指定一个新口令即可:

    FRPSCTL_WEB_PASSWORD='your-new-password' frpsctl web serve   # 或 --password / --password-file
    

    想以后随时能取回,改用 sudo frpsctl web service install:口令会持久化到 实例目录的 web-password(0600),之后用 frpsctl web password show 读回。

仪表盘

打开后是仪表盘页,每 5 秒自动刷新(右上角可关,或点"刷新"手动更新):

区域 内容 要点
实例状态 owner / 状态 / pid / 运行时长 / 版本 / 监听地址 / systemd unit 二进制版本与磁盘版本不一致时显示"重启生效"
控制面健康 L1 / L2 / L3 三层 + gate + 详情 L2 或 L3 失败时详情直接给出原因
概览 客户端 / 代理总数 / 当前连接 / 今日入站出站 / TLS 强制 数字来自 dashboard 统计
近 7 天流量 按天双色柱状图(蓝=入站 绿=出站) 悬停看单日数值;标题栏带合计;点下方代理行可下钻该代理曲线
实时流量 页面打开期间的速率曲线(每 5 秒采样一次,累计值差分) 显示当前速率与峰值;采样存本地浏览器,刷新不丢
客户端 name / user / hostname / ip / 状态 / 版本 在线状态用彩色标签
代理 name / user / 类型 / 端口 / 状态 / 连接 / 今日流量 点击任意行展开该代理的 7 天曲线;右上角"清理离线记录"
日志 最近的日志尾部(复用 log.to 解析) 向上滚动自动暂停跟随(标题栏显示"已暂停"),滚回底部恢复

页面顶部的横幅会明确报告异常:

  • state.json 损坏(附处置指引);0.70.x 缺安全修复;插件不可达(客户端将无法登录); 流量超过 50 个代理时提示已截断。

进程操作在右上角:启动 / 重启 / 停止。操作进行中按钮会禁用、状态徽章显示 "操作中…"——启动最长要等 10 秒健康检查,此前完全无反馈。

配置编辑:预览、应用与回滚

配置页分三块:编辑表单、变更预览、历史与回滚。

编辑:

  1. 每个字段一行(按表分组)。敏感值(token / 口令)不回显,显示打码提示 hQ***x3(已设置;留空不改)——留空表示保留原值,填入新值才会覆盖。
  2. 想删除某个键(回落 frp 默认值),点行尾的"删除"(会划线标记,再点"恢复"可取消)。
  3. 想新增配置里还没有的键(例如 kcpBindPort),用表单底部的"新增键"输入 键名与值(值语法与 CLI 一致:7001、"text"、[1,2]…)。
  4. 顶部计数会显示"N 项待应用(改 x / 删 y)",点它可以跳到第一处修改。

预览 → 应用:

  1. 点"预览变更":服务端在锁内取当前配置快照并生成打码 diff(+ 绿、- 红)。 预览有效期 10 分钟;期间配置若被 CLI 改过,应用会被拒绝而不是覆盖 (CAS 保护)——重新预览即可。
  2. 确认 diff 无误后点"确认应用(重启服务)":多键修改与删除合并成一次事务 (一份快照、一次重启);健康检查失败会自动回滚并如实告知。

历史与回滚:

  1. "历史与回滚"卡片列出最近 10 份快照(时间 / 操作 / 步数)。
  2. 每行先点**"查看差异"**——展开该快照与当前配置的打码 diff(与 CLI config diff --steps N 同一实现)。回滚是危险操作,先看清会改什么。
  3. 确认后点"回滚到此份":与 CLI 一样走完整闭环(校验 → 替换 → 重启 → 失败再回滚)。

配置原文绝不下发浏览器:界面与 API 只返回打码值;快照列表只读元数据 (时间/动作/步数),差异接口也返回打码 diff。要看明文用 CLI config get --reveal。

口令、部署与安全

默认只绑回环。要远程访问必须显式 --allow-non-loopback:

frpsctl web serve --bind 0.0.0.0:8787 --allow-non-loopback

建议再套一层反向代理终结 TLS;反代后加 --trusted-proxy,让登录失败限速按 X-Forwarded-For 的最后一跳区分来源(否则所有请求同源,攻击者的失败会 连带把管理员锁在冷却之外)。默认关闭时该头完全不被读取——伪造它既不能绕开 限速、也不能制造新来源。

用 systemd 托管(生成 0600 口令文件,unit 只引用路径,明文不落 unit):

sudo frpsctl web service install    # 体检 → 渲染 frpsctl-web@.service → enable
sudo frpsctl web service status
sudo frpsctl web service uninstall  # 口令文件保留

web service install 同样需要 frpsctl 位于系统路径(不能被 ProtectHome 挡住)——与插件服务的部署要求一致。--trusted-proxy 会写进 unit。

安全设计(比 frp 自带 dashboard 更严——它正是本项目安全决策的来源):

  • 默认只绑回环;绑非回环必须显式开关;
  • 不允许空口令:自动生成(仅打印一次)或显式指定;会话 Cookie 带 HttpOnly + SameSite=Strict;
  • 一切变更请求要求 X-CSRF-Token(登录时下发,仅存浏览器内存);
  • 登录失败按来源限速(60 秒 5 次),错误口令与限速的响应完全一致;
  • 配置原文绝不回显;响应带 CSP(default-src 'none')与 no-store;
  • 单文件前端、零外部资源(不加载任何 CDN)。

CLI 与 Web 的对应关系

Web 的每个操作都调用与 CLI 同一套 core 函数——不存在"界面专用"的第二套逻辑:

操作 CLI Web
启停 / 重启 start / stop / restart 右上角按钮 → POST /api/actions/*
看状态与健康 status 仪表盘各卡片
客户端 / 代理 / 流量 clients / proxies / traffic 列表与图表(同一 v2 数据源)
改配置 config set / config unset 表单 → 预览 → 应用(多键一次事务)
看差异 config diff --steps N "查看差异"(同一 snapshot_diff)
回滚 config rollback [N] "回滚到此份"
日志 log 日志面板(同一 core/logs)
清理离线记录 prune "清理离线记录"
口令 web password show 登录页输入

进阶用法

多实例

一台机器跑多个 frps:用 --instance 或环境变量区分,各自有独立的配置、状态、 日志、锁。

frpsctl --instance web init --bind-port 7001 --dashboard-port 7501
frpsctl --instance web start
frpsctl --instance web status

FRPSCTL_INSTANCE=web frpsctl status    # 或长期用环境变量

优先级:--instance > FRPSCTL_INSTANCE > default。服务端场景可把实例根目录 放到 /etc:

frpsctl --root /etc/frps/instances --instance web init

服务端插件(多用户鉴权 + 端口白名单)

frp 的服务端插件是一个 HTTP 回调:frps 在 Login / NewProxy 等事件发生时 POST 一段 JSON,由插件决定放行还是拒绝。本工具用 Python(标准库)实现该回调。

frpsctl plugin init      # 生成策略模板(0600,默认 fail-closed)
frpsctl plugin check     # 离线校验 + 试算典型裁决
frpsctl plugin serve     # 启动(只允许绑回环)

策略里的用户可以用命令维护——不必手写 JSON(写入前用与 plugin check 相同的 判据复验,0600 原子写,_comment 等自定义字段原样保留):

frpsctl plugin user list                               # 用户与权限摘要(不显示策略级凭据)
frpsctl plugin user set alice --ports 6000-6010 --max-proxies 5
frpsctl plugin user set alice --no-random-port         # 只改这一个字段,其余保持
frpsctl plugin user set bob --names ""                 # 空串 = 删除该字段(不限名称)
frpsctl plugin user remove alice

插件服务在启动时载入策略:改完记得重启它才生效 (systemctl restart frpsctl-plugin@<实例>)。

plugin check 让你在部署前就看到"策略会怎么判":

$ frpsctl plugin check
策略文件:/home/u/.local/share/frpsctl/instances/default/plugin-policy.json
  用户数:1(未列出的用户一律拒绝)
  客户端身份校验:开启
  审计:写入 plugin-audit.jsonl
  配额计数来源:(未配置 admin_url,配额计数不准确 ⚠)
    - alice: 端口 6000-6010,最多 5 个代理

典型裁决试算:
  [允许] Login alice
  [允许] NewProxy alice 申请 6000(在许可范围内)
  [拒绝] NewProxy alice 申请 1(预期越界) — 端口 1 不在用户 'alice' 的许可范围(允许:6000-6010)
  [拒绝] Login __nobody__(未列出) — 未知用户 '__nobody__'

策略校验通过

策略文件

plugin init 生成 <实例目录>/plugin-policy.json(可用 --policy 或 FRPSCTL_PLUGIN_POLICY 指定):

{
  "allow_unknown_user": false,
  "require_client_id": true,
  "audit": { "enabled": true, "path": "./plugin-audit.jsonl" },
  "admin_url": "http://127.0.0.1:7500",
  "users": {
    "alice": {
      "allowed_ports": ["6000-6010"],
      "allow_random_port": false,
      "allowed_proxy_types": ["tcp", "udp"],
      "allowed_proxy_names": ["alice-*"],
      "max_proxies": 5
    }
  }
}
字段 说明
allow_unknown_user 默认 false:未列出的用户一律拒绝。开启等于不做鉴权
require_client_id 默认 true:frpc 必须用 metadatas.client_id 声明身份,且与 user 一致
allowed_ports 端口白名单,支持 "6000" 与 "6000-6100"
allow_random_port 默认 false:不允许 remotePort = 0(否则白名单形同虚设)
allowed_proxy_names 代理名通配(alice-*)。不提升安全性,用于防止用户互相抢占名字
max_proxies 每用户代理数上限(0 = 不限)
admin_url 填了它,配额计数走 dashboard 的权威统计;不填只在插件进程内计数

frps 侧配置

[[httpPlugins]]
name = "frpsctl"
addr = "http://127.0.0.1:8080"
path = "/handler"
ops  = ["Login", "NewProxy"]

ops 至少要有 Login 与 NewProxy:前者做鉴权,后者做端口与配额治理。

frpc 侧配置

serverAddr = "your-server"
serverPort = 7000
user = "alice"
metadatas = { client_id = "alice" }     # 必须与 user 一致
auth.token = "……"

[[proxies]]
name = "alice-web"
type = "tcp"
localPort = 8080
remotePort = 6005                        # 必须在自己被允许的范围内

审计

每次裁决写入 JSONL(默认 ./plugin-audit.jsonl,可用 audit.path 改):

$ tail -1 plugin-audit.jsonl
{"at":"2026-09-15T21:33:36","op":"NewProxy","user":"alice","decision":"deny",
 "reason":"frpsctl-plugin: 端口 9999 不在用户 'alice' 的许可范围(允许:6000-6010)",
 "proxy_name":"alice-web","proxy_type":"tcp","remote_port":9999,
 "elapsed_ms":0.03,"quota_source":"dashboard"}

审计是异步的:record() 只入队(实测单次裁决 0.02–0.03 ms),后台线程刷盘。 磁盘不可用时记录退回缓冲,绝不让登录链路失败——服务可用性优先于审计完整性。

⚠️ 两条硬约束

  1. 插件是全部客户端登录的单点,且 fail-closed——它挂掉 = 所有人登录不了。 生产环境必须用 systemd 守护并设 Restart=always。
  2. frp 的插件协议没有任何认证(配置项只有 name/addr/path/ops/tlsVerify), 所以插件只允许绑回环。指向非回环会被 plugin serve 拒绝,doctor 也报 ERROR。

plugin serve 收到 SIGTERM(systemctl stop 发的就是它)会优雅退出:先停 服务、再把审计缓冲刷盘,然后才退出。

用 systemd 托管 frps

sudo frpsctl service install      # 安装 frps@.service 模板并 enable
sudo frpsctl service status       # 查看托管状态
sudo frpsctl service uninstall    # 解除托管

安装后 owner 变为 systemd,start / stop / restart 全部委托 systemctl, pid 文件不再参与任何判定。

部署前置(service install 会在安装前检查)

unit 只是第一步——下面三项不满足时 systemctl start 必然失败。service install 会在安装之前逐项检查并当场拒绝,而不是让你事后去 systemctl 的报错里找原因:

检查 不满足时的典型表现 处置
服务用户存在(默认 frps) Failed to determine user credentials sudo useradd --system --no-create-home --shell /usr/sbin/nologin frps,或用 --user/--group 指定已有账户
二进制对服务用户可执行 Permission denied sudo frpsctl install 默认装在 /root/.local/share(/root 是 0700,frps 用户读不到)。改用共享目录:sudo FRPSCTL_DATA_HOME=/opt/frpsctl frpsctl install
日志目录可写(默认 /var/log/frps) Failed to set up mount namespacing service install 会自动创建并 chown;无法写入时会被拒绝,可用 --log-dir 换位置
二进制与实例目录不在家目录下 unit 看不到路径(ProtectHome=true 的挂载隔离) 用 /opt、/etc、/srv 等系统路径,别用 ~/.local

service install 还会把实例目录移交给服务用户(权限仍是 0700,只是属主从 root 换成服务用户)——否则 frps 进程读不到目录里的 frps.toml。安全性不降级: 同机其他用户依然读不到。因此 systemd 模式下请统一用 root 执行 frpsctl (systemctl 委托本来也需要 root)。

完整流程:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin frps
export D=/opt/frpsctl

sudo FRPSCTL_DATA_HOME=$D frpsctl install          # 下载二进制(可加 --mirror)
sudo FRPSCTL_DATA_HOME=$D frpsctl init --no-input  # 生成配置(会打印 token 与口令)
sudo FRPSCTL_DATA_HOME=$D frpsctl service install  # 体检 → 渲染 unit → enable
sudo FRPSCTL_DATA_HOME=$D frpsctl start            # 委托 systemctl
sudo FRPSCTL_DATA_HOME=$D frpsctl status

渲染出的 unit(/etc/systemd/system/frps@.service):

[Unit]
Description=frps service (%i)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=frps
Group=frps
ExecStart=/usr/local/bin/frps -c /etc/frps/instances/%i/frps.toml
WorkingDirectory=/etc/frps/instances/%i
Restart=on-failure
RestartSec=2
LimitNOFILE=65535

# 若配置使用 <1024 端口(例如 vhostHTTPPort = 80)
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

# 加固
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
# 实例目录也在列:ProtectSystem=strict 下其余路径只读,
# 而 frp 默认要往实例目录写 ./frps.log
ReadWritePaths=/var/log/frps /etc/frps/instances/%i

[Install]
WantedBy=multi-user.target

多实例天然对齐:实例名就是 systemd 的 %i(frps@web.service)。

⚠️ unit 的 ExecStart 写的是具体二进制路径,因此 install 换版本后需要 systemctl restart 才生效(软链换向不影响已加载的 unit)。

用 systemd 托管 Web 管理台与插件

两者的部署要求与 frps 一致(账户 / frpsctl 可达且不在家目录 / 实例目录不在家 目录),都由安装命令在安装前体检:

# Web 管理台:Restart=on-failure(交互工具,正常停止不自启)
sudo frpsctl web service install
sudo systemctl start frpsctl-web@default.service

# 插件:Restart=always(登录单点,退出必须立刻拉起)
sudo frpsctl plugin service install
sudo systemctl start frpsctl-plugin@default.service

升级 frps

frpsctl install --version 0.72.0                    # 下载 + 校验 + 落盘 + 换软链
frpsctl install --version 0.72.0 --only-download    # 只落盘,稍后统一切换
frpsctl install --version 0.72.0 --mirror URL       # 指定镜像源(也可用 FRPSCTL_MIRROR)
frpsctl install --force                             # 同版本重新下载

换软链不影响正在运行的进程(Linux 上可执行映像在 execve 后已绑定 inode), 只影响下一次 start。因此 status 会同时显示两个版本:

binary   : frps 0.70.0 (running) → 0.71.0 (on disk, restart to apply)

旧版本文件不会自动删除,bin/frps-<old> 就是回退的抓手。要回退:

frpsctl install --version 0.71.0 && frpsctl restart

参考手册

退出码(脚本化契约)

码 含义 典型触发
0 成功
1 未分类错误 / doctor 发现 ERROR 意外异常;或体检不通过
2 用法 / 参数错误 未知选项、缺子命令
3 配置非法 语义校验或 frps verify 拒绝;键不存在;状态文件损坏
4 二进制缺失 / 不可执行 / 版本不受支持 未 install,或版本 < 0.70.0
5 实例未运行 stop 时无进程
6 实例已在运行 重复 start
7 dashboard 不可达 / 未启用 clients / proxies / traffic / prune 时 webServer.port = 0;v2 API 缺失
8 权限不足 需要 root 的操作
9 变更已自动回滚 配置写入后启动/健康检查失败,已恢复上一版
10 启动失败 / 进程停不下来 启动即退出(附 frp 原始报错);SIGKILL 后仍存在
11 进程所有权冲突 身份校验不通过;systemd 与 direct 混用;锁被占用
12 已启动但健康检查未通过 L1 进程在、L2 控制面不可达(进程仍被托管,见健康分层)

脚本里应当据此分支:

frpsctl start
case $? in
  0)  echo "启动成功" ;;
  6)  echo "已在运行,跳过" ;;
  4)  echo "需要先 frpsctl install" >&2; exit 4 ;;
  10) echo "启动失败,查看 frpsctl log" >&2; exit 10 ;;
  12) echo "进程已启动但控制面异常,查看 frpsctl status" >&2; exit 12 ;;
  *)  exit 1 ;;
esac

环境变量与全局选项

环境变量 作用
FRPSCTL_INSTANCE 默认实例名
FRPSCTL_ROOT 实例根目录(默认 ~/.local/share/frpsctl/instances)
FRPSCTL_DATA_HOME 数据根目录(默认 $XDG_DATA_HOME/frpsctl)
FRPSCTL_ADMIN_PASSWORD dashboard 口令(优先于配置文件)
FRPSCTL_PLUGIN_POLICY 插件策略文件路径
FRPSCTL_MIRROR frps 下载镜像(逗号分隔;install --mirror 优先于它)
FRPSCTL_WEB_PASSWORD Web 管理台登录口令(web serve --password 优先于它)
FRPSCTL_TRACEBACK 设为 1 时打印完整回溯(排查未分类错误用)

全局选项见 上文。凭据优先级: --admin-password > FRPSCTL_ADMIN_PASSWORD > 配置文件里的 webServer.password。

目录布局

~/.local/share/frpsctl/
├── bin/                          # 全局共享:二进制按版本并存
│   ├── frps-0.71.0
│   └── frps -> frps-0.71.0       # 当前版本软链(install 唯一改动的对象)
└── instances/
    └── default/                  # 实例私有(0700)
        ├── frps.toml             # 0600,含 token 与 dashboard 口令
        ├── state.json            # 0600,所有权判定的权威来源
        ├── frps.pid              # 人类可读副本,不参与任何判定
        ├── frps.log              # 由 frp 自己写并轮转
        ├── .lock                 # flock 互斥
        ├── web-password          # Web 管理台口令文件(web service install 生成,0600)
        ├── plugin-policy.json    # 插件策略(若使用插件)
        ├── plugin-audit.jsonl    # 插件审计
        ├── config-history/       # 最近 10 份配置快照(0600)+ meta.json
        │   └── 0001-20260915-211140/
        └── startup/              # 最近 3 份启动日志(0600,诊断"启动即退出")

state.json 记录 {pid, start_time, binary, config, version, started_at, owner}—— 其中 start_time 是识别 pid 复用的唯一依据,binary 存的是真实路径(不是软链, 否则换版本后身份校验会失配)。

备份与回滚

每次 config set / config unset / config edit / config rollback 都会先把当前 配置存进 config-history/NNNN-<时间戳>/(保留最近 10 份,含 meta.json 记录操作 与结果)。

frpsctl config diff              # 当前 vs 上一份
frpsctl config diff --steps 3    # 当前 vs 第 3 新的一份
frpsctl config rollback          # 回滚到上一份(并重启)
frpsctl config rollback 2        # 回滚到 2 份之前

回滚复用同一闭环(校验 → 替换 → 重启 → 失败再回滚),而不是简单 cp 覆盖—— 否则回滚本身会把服务搞坏。回滚完成后会如实汇报结果:配置与服务是否都恢复了。


排障

启动失败

$ frpsctl start
错误:frps 启动后立即退出
提示:frps: listen tcp :7000: bind: address already in use

提示 那一行是 frp 的原始报错,通常已经说清问题。进一步排查:

frpsctl log                                            # frp 自己的日志
ls ~/.local/share/frpsctl/instances/default/startup/   # 启动阶段日志(保留 3 份)

常见原因:

报错 原因 处置
bind: address already in use 端口被占 换端口,或找出占用者
unknown field "xxx" 键名写错,或顶层键写在了 [table] 之后 检查位置
open xxx: no such file 证书/文件路径不存在 修正路径
无任何输出 二进制不可执行 跑 frpsctl verify、frpsctl doctor

status 显示 FOREIGN

state    : FOREIGN

含义:state.json 里的 pid 存活,但身份校验不通过(启动时刻或命令行对不上), 通常意味着那个 pid 已被系统复用给无关进程。此时 start / stop 都会拒绝执行 (退出码 11)——这是刻意的安全设计:宁可停不下来,也不杀错进程。

处置:确认没有 frps 在跑,然后删除 state.json:

pgrep -af frps                  # 确认
rm ~/.local/share/frpsctl/instances/default/state.json

status 显示 STALE

state.json 存在但进程已退出。这不是错误,直接 start 即可(会自动清理陈旧状态)。

状态文件损坏

$ frpsctl status
⚠ 状态文件已损坏:…/state.json
  无法判断进程归属,因此 stop/start/config set 都会拒绝执行。
  请确认没有 frps 在跑,然后删除该文件。

status 仍以退出码 0 结束(它必须能回答现状),但会显著告警。工具不会在 状态不可信时猜测——因为猜错的代价是杀掉无关进程。Web 管理台遇到同一情况会在 页面顶部显示红色横幅。

锁被占用

错误:另一个 frpsctl 进程正持有实例锁:…/.lock

说明另一个 frpsctl 正在操作同一实例。等它完成即可——锁随进程退出自动释放, 不会留下需要人工清理的陈旧锁文件。

配置校验超时

错误:配置校验超时(30 秒):…/frps verify 没有返回

说明二进制卡住或不可执行。手工跑一次确认:

/path/to/frps --strict_config=true verify -c /path/to/frps.toml

Web 管理台打不开 / 登录不了

  • 打不开:确认 web serve 还在前台运行(Ctrl-C 会停掉它);systemd 托管时 frpsctl web service status 看 active。
  • 口令不对:自动生成的口令只在启动时显示一次;systemd 部署的口令用 frpsctl web password show 取回。
  • 连续失败后被拒:登录失败限速(60 秒 5 次),表现与"口令错误"完全一致 ——等一分钟后重试;反代部署请加 --trusted-proxy,否则所有人的失败会算在同一来源上。

想看完整回溯

FRPSCTL_TRACEBACK=1 frpsctl status

安全说明

init 生成的默认配置直接站在安全侧,每条都对应一个已确认的 frp 行为:

项 默认值 理由
webServer.addr 127.0.0.1 dashboard 只有 Basic Auth 一层防护
webServer.user / password admin / 随机 24 字符 两者全空 = frp 完全不鉴权(不是"要求登录")
transport.tls.force true 拒绝明文 frpc 连接
allowPorts 交互引导填写 否则任何持有 token 的客户端都能申请任意端口
maxPortsPerClient 20 frp 默认为 0(不限),单客户端可耗尽端口
auth.token 随机 32 字符 无 token 等于无客户端鉴权
配置文件权限 0600 内含 token 与口令;原子写落地,无权限窗口

dashboard 鉴权的真实语义(实测)

"两者全空 = 完全不鉴权"容易让人以为"只要填了 user 就安全了"。真机上逐项实测 (frps 0.71.0)后,完整语义是这样的:

webServer.user webServer.password 无 Authorization 头 user:(空口令)
"admin" 未设 / 空 401 200
未设 未设 200 200(任意凭据均可)
"admin" "secret" 401 401(须 admin:secret)
未设 "secret" 401 401(须 :secret)

两条推论:

  1. 鉴权开关是"任一非空即启用"。因此 frpsctl 只拒绝"两者全空 + 绑非回环" ——那是真·完全不鉴权;而"有 user、口令为空"属于强度不足,由 doctor 报 WARN 而不是否决写配置。
  2. Basic Auth 里的空口令是合法口令。user = "admin" 而 password = "" 时, 任何人用 admin + 空口令就能进 dashboard——等于只用用户名保护。doctor 会明确报出来。

其他保证:

  1. 拒绝危险组合:config set 与 doctor 双重拦截"dashboard 绑非回环 + 口令为空"。
  2. 不回显机密:口令与 token 不进日志、不进 --json、不进异常消息; config get 默认打码,diff 输出同样打码,配置快照与启动日志按 0600 落盘。
  3. 拒绝模板语法:写入含 {{ 的值会被拒绝——frp 会对其做 text/template 渲染, 放行等于让配置被悄悄改写。
  4. 拒绝来路不明的二进制:校验和不匹配即拒绝安装。
  5. 不确定就拒绝:进程身份不符、状态文件损坏、所有权冲突——一律报错并给出处置 指引,而不是尝试自愈。

开发

uv venv && uv pip install -e ".[dev]"
.venv/bin/pytest                       # 全部 644 条(契约层缺二进制时自动 skip)
.venv/bin/pytest -m "not contract"     # 快速回归(614 条)
.venv/bin/pytest --cov=frpsctl         # 覆盖率(CI 门禁 80%,当前 85%)
.venv/bin/ruff check src/ tests/       # 静态分析

CI(Linux,Python 3.11/3.12/3.13/3.14)还包含:ruff、覆盖率门禁、真 frp 0.71.0 契约层、真 frp 0.70.0 下界契约矩阵、无二进制降级路径、端到端冒烟与 Web 管理台冒烟(含配置差异接口)、前端静态守卫(逐 <script> 块 node --check + 禁 innerHTML/外部资源 + JS 与 HTML 的 id 双向核对)、 文档一致性守卫(README/设计文档/API 表 vs 代码的交叉核对)。

测试分七层

层 文件 目标
单元 tests/test_units.py 进程原语、锁、原子写、无损补丁、标志构造、机密打码、二进制解包与复验、systemd unit 渲染与部署体检
集成 tests/test_integration.py 生命周期与回滚(假 frps 驱动确定性故障)
CLI tests/test_cli.py 退出码契约、--json 形态、机密不外泄、全局选项位置、config edit 闭环
契约 tests/test_facts.py 设计文档事实基线的自动化守卫(需真 frps)
故障注入 tests/test_faults.py 注入系统调用失败,验证异常路径的五项不变量
插件 tests/test_plugin.py 协议报文、裁决、审计、配额;含真 frpc 端到端契约
前端与文档 tests/test_web_frontend.py、tests/test_docs.py 单文件前端的静态守卫(node --check、禁 innerHTML/外部资源、CSS 变量对齐、id 双向核对)与 README / 设计文档 / API 表的双向一致性

让契约层跑起来(需要真实二进制):

frpsctl install --with-frpc        # 一次下载,同时得到 frps 与 frpc
export FRPSCTL_TEST_BINARY=~/.local/share/frpsctl/bin/frps-0.71.0
export FRPSCTL_TEST_FRPC=~/.local/share/frpsctl/bin/frpc-0.71.0
.venv/bin/pytest tests/test_facts.py tests/test_plugin.py -m contract

没有 frpc 时,tests/test_plugin.py::TestRealFrpcContract 的 4 条会 skip(不是 fail)——它们是"插件真的接得住 frp 调用"的唯一证明,因此宁可跳过也不删掉。

契约层为什么重要:它断言的是 frp 的行为事实(字段名、形状、退出码、 标志可用性)。frp 一旦改动这些,CI 会先于用户发现——而不是等某个用户报告 "状态里的数字不对"。这类错误不会报错,只会给出错误数字。

故障注入层为什么重要:异常路径是缺陷聚集区。它注入真实的系统调用失败 (os.replace、subprocess、httpx…),验证五项不变量:契约内异常、无遗留 进程、无半截/含机密的文件、锁已释放、机密不外泄。


已知边界

以下是刻意不做的事,以及各自的原因。它们不是待办事项,而是能力边界—— 提前写出来,好过让人踩到之后才发现:

边界 说明
只支持 Linux macOS / Windows 直接拒绝启动,不提供降级
只支持 frps ≥ 0.70.0 因为只用 v2 Admin API,不做 v1 降级
restart 没有 --no-rollback 重启不读配置,不存在"新旧版本"可比;自动回滚只属于配置变更路径
start --foreground 不写 state 前台模式只用于调试:进程不进入本工具的托管视图,stop 管不到它(--help 里已警示)
不做并发连接上限 它只能在 NewUserConn 上实施,而那落在每次用户连接的关键路径上、错误只以 info 级记录、且回调内容里没有连接 id。需要真并发限制请在 frpc 侧用连接池与限流
max_proxies 计数需配 admin_url 不配时只在插件进程内计数(重启归零、多实例各算各的),plugin check 会告警
frps 没有热重载 改配置必然重启,因此 config set 的设计目标就是"失败了要能退回去"
systemd 模式下 pid 文件不参与判定 所有权委托 systemctl;install 换版本后需 systemctl restart
Web 管理台不限制并发连接数 单机管理工具的取舍(请求线程随连接创建,监听 backlog 64);公网暴露请在前置反代上做限流
--trusted-proxy 只信 X-Forwarded-For 的最后一跳 前提是前面确实有一层会重写该头的可信代理;直连部署不要开启
Web 无 WebSocket/SSE 推送 5 秒轮询足够,且省掉长连接的生命周期管理
Web 不做 DOM 级前端测试框架 引入 jsdom/构建链会破坏"单文件零依赖"这个安全资产;语法与静态纪律由 CI 守卫,UI 分支仍靠人工点验(残余风险已记账)
一个 web 进程服务一个实例 多实例请起多个 web serve(各自 --instance)

许可

frpsctl 自身代码以 MIT 发布,全文见 LICENSE。

本仓库不包含 frp 的源代码或二进制。frpsctl install 会按需从官方发布页下载 frps,该二进制是独立第三方软件,遵循其自身的 Apache-2.0,不随本包分发。 详见 NOTICE。

Release files for frpsctl 0.2.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for frpsctl 0.2.6
File Size Uploaded
frpsctl-0.2.6.tar.gz 480.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for frpsctl 0.2.6
File Interpreter ABI Platform
frpsctl-0.2.6-py3-none-any.whl Python 3 none any Details

Total release size: 735.7 kB

Release files / frpsctl-0.2.6.tar.gz

Download URL frpsctl-0.2.6.tar.gz
Size 480.5 kB
Tags Source
SHA-256 checksum
How to use checksums
4491c639b1f2b02aee36dd7a26189aa6189244a040002aceef6f57963ed9e9f7
BLAKE2b-256 checksum
How to use checksums
e1bcf299b984047ba4f49bcb0ef6075d5b4a55dccce786c3d930f269752d4f8e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.

Transparency log

Release files / frpsctl-0.2.6-py3-none-any.whl

Download URL frpsctl-0.2.6-py3-none-any.whl
Size 255.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b72c9fb43349c7d70c8b600e354d815486aad800b88136e34c7bccd82c775f06
BLAKE2b-256 checksum
How to use checksums
88275713cd8c9e1124a87d095cea675d6a63e4deef798c92ad1643f6849ddf17
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

This release

0.2.6 This release

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page