2685 字
13 分钟
frpc管理工具frpm

背景#

NOTE

你可以跳过这些废话,直接前往具体实现

这个可爱的小工具是我去年运维的时候vibe写的,今天更新frpc配置的时候挖出来了,正好水一篇博客

至于为什么没有使用素质更高的toml而是ini,是因为我用的那台机器上装的是旧版frpc,而新版也兼容ini其实就是懒得再写一份

文中名词参考frp官方文档

为什么需要frpm#

一般情况下,frpc的使用不会有太大问题,因为一般情况下使用者只会有一台服务器用作frps,同时只有少数几个代理,配置大致如下:

frpc.ini
# 云服务器配置
[common]
server_addr = 1.1.1.1
server_port = 7000
token = token_to_auth
# 代理1
[proxy1]
type = tcp
local_ip = 192.168.1.2
local_port = 22
remote_port = 12345
# 代理2
[proxy2]
type = tcp
local_ip = 192.168.1.3
local_port = 22
remote_port = 12346

但是假如你同时有多个云服务器做frps,他们的addrporttoken各不相同,同时你还有很多很多代理要接入很多主机,那么你的配置文件会变成云服务器数量个除了云服务器配置部分之外完全相同,而且内容极其繁杂的大坨配置文件

server_n.frpc.ini
# placeholder
[common]
server_addr = n.n.n.n
server_port = 7000
token = token_n
[proxy1]
type = tcp
local_ip = 192.168.1.2
local_port = 22
remote_port = 12345
...

很显然,这是非常蠢的,把配置文件拆开是刚需

实现思路#

配置文件要拆是很好拆的,无非把云服务器的配置部分拆成一个文件,把代理按照特定的范围比如主机拆分成多个独立文件,问题是怎么拼起来

frpc:搞半天还要自己拼.jpg

环境变量渲染配置#

frpc作为用go写的程序,在读配置的时候能使用go自带的配置渲染

同时这个配置渲染使用的变量可以是环境变量,这就给了我们集成的自由度——我们只需把云服务器配置以环境变量的形式导入,在启动服务的时候让frpc自己渲染模版就行了

代理配置原子化与systemd的局限性#

既然云服务器的配置都拆出来了,何不把代理的也拆一拆,我个人喜欢拆到主机级

不过这样一来,启动服务的时候就需要两个参数:云服务器配置id代理配置id,也就是说需要切参数了,因为systemd模版单元只能接受一个参数,于是不得不引入一个包装脚本,原因是显而易见的:ExecStart对多命令的支持几乎是没有,也不鼓励你这么做

于是使用了常见的包装器脚本切参数方案,使用:作为分隔符,他大概像这样工作:

moncak terminal
systemctl start frpc@server:proxy
# call: wrapper.sh server:proxy
# 包装器将 "server:proxy" 以 : 为分隔符切成 server 和 proxy
# 随后包装器导入 server 对应的环境变量并使用 proxy 对应的配置文件启动 frpc
# frpc 渲染配置并运行

满地鸡毛#

诶你这个傻呗脚本给我的frpc切成大份了,我本来只需要systemctl start frpc或者systemctl start frpc@server就能启动服务,现在你把含有x个云服务器和y个代理的配置拆成了一共x*ysystemctl命令,多的那一大堆命令你帮我敲吗

手敲x*y个命令肯定是不现实的,所以我们要把systemctl再包一层,做成一个简单方便的统一管理入口

我们一开始切配置的大前提是,这x个云服务器配置和y个代理配置不强相关,那么我们写个脚本遍历我们想要的云服务器配置和代理配置不就行了

于是有frpm

moncak terminal
frpm --help
frpm - FRP 服务管理工具 v1.0.0
用法: frpm [选项] <命令>
命令:
enable 启用并启动服务
disable 禁用并停止服务
start 启动服务
stop 停止服务
reload 重载服务配置
status 显示服务状态(表格视图)
help 显示此帮助信息
选项:
-s, --server <servername> 包含指定的服务器(可多次使用)
-c, --config <configname> 包含指定的配置(可多次使用)
-ns, --no-server <servername> 排除指定的服务器(可多次使用)
-nc, --no-config <configname> 排除指定的配置(可多次使用)
-h, --help 显示帮助信息
示例:
frpm enable # 启用所有服务组合
frpm status -s server1 # 只显示server1的状态
frpm stop -c config1 -ns server2 # 停止所有使用config1但不包括server2的服务

于是frpc配置管理的最后一块拼图被补齐

顺便一提这个status虽然也是vibe的,但是是我寻思的,两个不强相关的量作为两个轴织出一个阵列真那啥吧

status

具体实现#

这部分纯纯把代码贴上来水字数的,复制拿去用就行

配置部分#

代理配置#

头部是公式化的渲染模版

/etc/frp/proxy_n.frpc.ini
[common]
server_addr = {{ .Envs.FRP_SERVER_ADDR }}
server_port = {{ .Envs.FRP_SERVER_PORT }}
token = {{ .Envs.FRP_TOKEN }}
[proxy-n]
type = tcp
local_ip = 192.168.1.n
local_port = 22
remote_port = 12345

云服务器配置#

写成环境变量方便导入

/etc/frp/servers/server_n.env
export FRP_SERVER_ADDR="n.server.com"
export FRP_SERVER_PORT="7000"
export FRP_TOKEN="token_n"

服务部分#

systemd单元#

/etc/systemd/system/frpc@.service
[Unit]
Description=FRP client for instance %i
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/frpc-wrapper.sh %i start
ExecReload=/usr/local/bin/frpc-wrapper.sh %i reload
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target

包装脚本#

在这里渲染配置

/usr/local/bin/frpc-wrapper.sh
#!/bin/bash
set -euo pipefail
i="$1"
action="${2:-start}"
log() {
echo "[wrapper] $*"
}
if [[ "$i" != *:* ]]; then
log "Error: instance name must be in format servername:confname"
exit 1
fi
servername="${i%%:*}"
confname="${i#*:}"
log "resolved servername:$servername confname:$confname"
envfile="/etc/frp/servers/${servername}.env"
conffile="/etc/frp/${confname}.frpc.ini"
if [ ! -f "$envfile" ]; then
log "Error: environment file $envfile not found"
exit 1
fi
if [ ! -f "$conffile" ]; then
log "Error: config file $conffile not found"
exit 1
fi
log "now sourcing $envfile"
source "$envfile"
log "now starting frpc with config $conffile"
case "$action" in
start)
exec /usr/sbin/frpc -c "$conffile"
;;
reload)
exec /usr/sbin/frpc reload -c "$conffile"
;;
*)
log "Error: unknown action $action"
exit 1
;;
esac

便捷管理脚本#

其实这个才是frpm来的)),不过没有其余部分这玩意也没用,所以无所谓了

/usr/local/bin/frpm
#!/bin/bash
# frpm - FRP 服务管理工具
VERSION="1.0.0"
# 启用扩展模式匹配和nullglob
shopt -s extglob nullglob
# 全局变量
declare -a SERVER_NAMES CONFIG_NAMES
declare -a SELECTED_SERVERS SELECTED_CONFIGS
declare -a EXCLUDED_SERVERS EXCLUDED_CONFIGS
# 显示帮助信息
show_help() {
cat << EOF
frpm - FRP 服务管理工具 v${VERSION}
用法: frpm [选项] <命令>
命令:
enable 启用并启动服务
disable 禁用并停止服务
start 启动服务
stop 停止服务
reload 重载服务配置
status 显示服务状态(表格视图)
help 显示此帮助信息
选项:
-s, --server <servername> 包含指定的服务器(可多次使用)
-c, --config <configname> 包含指定的配置(可多次使用)
-ns, --no-server <servername> 排除指定的服务器(可多次使用)
-nc, --no-config <configname> 排除指定的配置(可多次使用)
-h, --help 显示帮助信息
示例:
frpm enable # 启用所有服务组合
frpm status -s server1 # 只显示server1的状态
frpm stop -c config1 -ns server2 # 停止所有使用config1但不包括server2的服务
EOF
}
# 显示错误信息并退出
die() {
echo "错误: $1" >&2
exit 1
}
# 从文件系统读取可用的服务器和配置
read_available_items() {
# 读取服务器列表
SERVER_NAMES=()
for server_file in /etc/frp/servers/*.env; do
servername=$(basename "$server_file" .env)
SERVER_NAMES+=("$servername")
done
# 读取配置列表
CONFIG_NAMES=()
for config_file in /etc/frp/*.frpc.ini; do
confname=$(basename "$config_file" .frpc.ini)
CONFIG_NAMES+=("$confname")
done
if [ ${#SERVER_NAMES[@]} -eq 0 ]; then
die "在 /etc/frp/servers/ 目录下未找到任何 .env 文件"
fi
if [ ${#CONFIG_NAMES[@]} -eq 0 ]; then
die "在 /etc/frp/ 目录下未找到任何 .frpc.ini 文件"
fi
}
# 应用筛选条件
apply_filters() {
SELECTED_SERVERS=("${SERVER_NAMES[@]}")
SELECTED_CONFIGS=("${CONFIG_NAMES[@]}")
# 应用包含筛选
if [ ${#INCLUDE_SERVERS[@]} -gt 0 ]; then
SELECTED_SERVERS=()
for server in "${SERVER_NAMES[@]}"; do
for included in "${INCLUDE_SERVERS[@]}"; do
if [[ "$server" == "$included" ]]; then
SELECTED_SERVERS+=("$server")
break
fi
done
done
fi
if [ ${#INCLUDE_CONFIGS[@]} -gt 0 ]; then
SELECTED_CONFIGS=()
for config in "${CONFIG_NAMES[@]}"; do
for included in "${INCLUDE_CONFIGS[@]}"; do
if [[ "$config" == "$included" ]]; then
SELECTED_CONFIGS+=("$config")
break
fi
done
done
fi
# 应用排除筛选
if [ ${#EXCLUDE_SERVERS[@]} -gt 0 ]; then
TEMP_SERVERS=()
for server in "${SELECTED_SERVERS[@]}"; do
excluded=false
for excluded_server in "${EXCLUDE_SERVERS[@]}"; do
if [[ "$server" == "$excluded_server" ]]; then
excluded=true
break
fi
done
if ! $excluded; then
TEMP_SERVERS+=("$server")
fi
done
SELECTED_SERVERS=("${TEMP_SERVERS[@]}")
fi
if [ ${#EXCLUDE_CONFIGS[@]} -gt 0 ]; then
TEMP_CONFIGS=()
for config in "${SELECTED_CONFIGS[@]}"; do
excluded=false
for excluded_config in "${EXCLUDE_CONFIGS[@]}"; do
if [[ "$config" == "$excluded_config" ]]; then
excluded=true
break
fi
done
if ! $excluded; then
TEMP_CONFIGS+=("$config")
fi
done
SELECTED_CONFIGS=("${TEMP_CONFIGS[@]}")
fi
if [ ${#SELECTED_SERVERS[@]} -eq 0 ]; then
die "筛选后没有可用的服务器"
fi
if [ ${#SELECTED_CONFIGS[@]} -eq 0 ]; then
die "筛选后没有可用的配置"
fi
}
# 执行systemctl命令
execute_systemctl() {
local command="$1"
local service_name
local -a failed_services=()
for server in "${SELECTED_SERVERS[@]}"; do
for config in "${SELECTED_CONFIGS[@]}"; do
service_name="frpc-new@${server}:${config}"
echo "执行: systemctl $command $service_name"
if ! systemctl "$command" "$service_name"; then
failed_services+=("$service_name")
fi
done
done
if [ ${#failed_services[@]} -gt 0 ]; then
echo "警告: 以下服务执行失败:" >&2
printf ' %s\n' "${failed_services[@]}" >&2
return 1
fi
return 0
}
# 显示服务状态表格
show_status_table() {
# 收集所有服务状态
declare -A status_map
local max_server_len=0
local max_config_len=0
# 确定最大长度用于表格格式化
for server in "${SELECTED_SERVERS[@]}"; do
if [ ${#server} -gt $max_server_len ]; then
max_server_len=${#server}
fi
done
for config in "${SELECTED_CONFIGS[@]}"; do
if [ ${#config} -gt $max_config_len ]; then
max_config_len=${#config}
fi
done
# 获取所有服务的状态
for server in "${SELECTED_SERVERS[@]}"; do
for config in "${SELECTED_CONFIGS[@]}"; do
service_name="frpc-new@${server}:${config}"
status=$(systemctl is-active "$service_name" 2>/dev/null)
status="${status:-unknown}"
status_map["${server}:${config}"]=$status
done
done
# 打印表头
printf "%${max_config_len}s" ""
for server in "${SELECTED_SERVERS[@]}"; do
printf " %-${max_server_len}s" "$server"
done
echo
# 打印表格内容
for config in "${SELECTED_CONFIGS[@]}"; do
printf "%-${max_config_len}s" "$config"
for server in "${SELECTED_SERVERS[@]}"; do
status=${status_map["${server}:${config}"]}
# 根据状态着色
case "$status" in
active) color="\033[32m" ;; # 绿色
inactive) color="\033[31m" ;; # 红色
activating) color="\033[33m" ;; # 黄色
deactivating) color="\033[33m" ;; # 黄色
failed) color="\033[31m" ;; # 红色
*) color="\033[90m" ;; # 灰色
esac
printf " ${color}%-${max_server_len}s\033[0m" "$status"
done
echo
done
# 图例
echo
echo "图例:"
echo -e " \033[32mactive\033[0m - 服务正在运行"
echo -e " \033[31minactive\033[0m - 服务未运行"
echo -e " \033[33mactivating/deactivating\033[0m - 服务正在启动/停止"
echo -e " \033[31mfailed\033[0m - 服务启动失败"
echo -e " \033[90munknown\033[0m - 服务状态未知或服务不存在"
}
# 主函数
main() {
local command=""
declare -a INCLUDE_SERVERS INCLUDE_CONFIGS
declare -a EXCLUDE_SERVERS EXCLUDE_CONFIGS
# 解析命令行参数
while [[ $# -gt 0 ]]; do
case "$1" in
enable|disable|start|stop|reload|status|help)
command="$1"
shift
;;
-s|--server)
if [ -z "$2" ]; then
die "选项 $1 需要一个参数"
fi
INCLUDE_SERVERS+=("$2")
shift 2
;;
-c|--config)
if [ -z "$2" ]; then
die "选项 $1 需要一个参数"
fi
INCLUDE_CONFIGS+=("$2")
shift 2
;;
-ns|--no-server)
if [ -z "$2" ]; then
die "选项 $1 需要一个参数"
fi
EXCLUDE_SERVERS+=("$2")
shift 2
;;
-nc|--no-config)
if [ -z "$2" ]; then
die "选项 $1 需要一个参数"
fi
EXCLUDE_CONFIGS+=("$2")
shift 2
;;
-h|--help)
show_help
exit 0
;;
-*)
die "未知选项: $1"
;;
*)
die "未知参数: $1"
;;
esac
done
# 如果没有指定命令,显示帮助
if [ -z "$command" ]; then
show_help
exit 1
fi
# 处理help命令
if [ "$command" = "help" ]; then
show_help
exit 0
fi
# 读取可用项并应用筛选
read_available_items
apply_filters
# 执行请求的命令
case "$command" in
enable|disable|start|stop|reload)
execute_systemctl "$command"
;;
status)
show_status_table
;;
*)
die "未知命令: $command"
;;
esac
}
# 运行主函数
main "$@"
frpc管理工具frpm
https://blog.monblog.top/posts/frpm/
作者
Moncak
发布于
2026-01-27
许可协议
CC BY-NC-SA 4.0