Files
op-packages/frp/files/frps.init
T
github-actions[bot] 516a4a541c
Merge-upstream / merge (push) Canceled after 0s
🍉 Sync 2026-08-25 06:20:24
2026-08-25 06:20:24 +08:00

781 lines
17 KiB
Bash

#!/bin/sh /etc/rc.common
START=99
USE_PROCD=1
NAME=frps
PROG=/usr/bin/$NAME
CONF_FILE=/var/etc/$NAME.toml
_err() {
echo "$*" >&2
logger -p daemon.err -t "$NAME" "$*"
}
_trim() {
printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
}
_toml_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
_toml_quote() {
printf '"%s"' "$(_toml_escape "$1")"
}
_toml_key_quote() {
_toml_quote "$1"
}
_toml_bool() {
local v
v="$(printf '%s' "$1" | tr 'A-Z' 'a-z')"
case "$v" in
1|true|yes|on|enabled)
printf 'true'
;;
0|false|no|off|disabled)
printf 'false'
;;
*)
printf 'false'
;;
esac
}
_TOML_ERR=0
_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0
_is_uinteger() {
case "$1" in
''|*[!0-9]*)
return 1
;;
esac
return 0
}
_is_integer() {
local value="$1"
case "$value" in
+*|-*)
value="${value#?}"
;;
esac
_is_uinteger "$value"
}
_is_port_value() {
_is_uinteger "$1" || return 1
[ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null
}
_is_port_or_zero_value() {
_is_uinteger "$1" || return 1
[ "$1" -ge 0 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null
}
_toml_line() {
local key="$1"
local value="$2"
local type="$3"
[ -z "$value" ] && return 0
case "$type" in
bool)
printf '%s = %s\n' "$key" "$(_toml_bool "$value")"
;;
int|integer|number)
if ! _is_integer "$value"; then
_err "invalid integer for $key: $value"
_TOML_ERR=1
return 1
fi
printf '%s = %s\n' "$key" "$value"
;;
port)
if ! _is_port_value "$value"; then
_err "invalid port for $key: $value"
_TOML_ERR=1
return 1
fi
printf '%s = %s\n' "$key" "$value"
;;
port0)
if ! _is_port_or_zero_value "$value"; then
_err "invalid port for $key: $value"
_TOML_ERR=1
return 1
fi
printf '%s = %s\n' "$key" "$value"
;;
*)
printf '%s = %s\n' "$key" "$(_toml_quote "$value")"
;;
esac
}
_emit_opt() {
local section="$1"
local option="$2"
local toml_key="$3"
local type="$4"
local value
config_get value "$section" "$option"
_toml_line "$toml_key" "$value" "$type"
}
_TOML_ARRAY=
_TOML_HAS_LIST=0
_toml_array_add() {
local item="$1"
item="$(_trim "$item")"
[ -z "$item" ] && return 0
if [ -n "$_TOML_ARRAY" ]; then
_TOML_ARRAY="${_TOML_ARRAY}, "
fi
_TOML_ARRAY="${_TOML_ARRAY}$(_toml_quote "$item")"
}
_collect_array_item() {
_TOML_HAS_LIST=1
_toml_array_add "$1"
}
_collect_array_option() {
local section="$1"
local option="$2"
local scalar item
_TOML_ARRAY=
_TOML_HAS_LIST=0
config_list_foreach "$section" "$option" _collect_array_item
if [ "$_TOML_HAS_LIST" = "0" ]; then
config_get scalar "$section" "$option"
while [ -n "$scalar" ]; do
case "$scalar" in
*,*)
item="${scalar%%,*}"
scalar="${scalar#*,}"
;;
*)
item="$scalar"
scalar=
;;
esac
_toml_array_add "$item"
done
fi
return 0
}
_emit_array_opt() {
local section="$1"
local option="$2"
local toml_key="$3"
_collect_array_option "$section" "$option"
[ -n "$_TOML_ARRAY" ] || return 0
printf '%s = [%s]\n' "$toml_key" "$_TOML_ARRAY"
}
_NAME_VALUE_ARRAY=
_NAME_VALUE_HAS_LIST=0
_name_value_array_add() {
local line="$1"
local key value item
case "$line" in
*=*)
key="${line%%=*}"
value="${line#*=}"
;;
*)
return 0
;;
esac
key="$(_trim "$key")"
value="$(_trim "$value")"
[ -n "$key" ] || return 0
item="{ name = $(_toml_quote "$key"), value = $(_toml_quote "$value") }"
if [ -n "$_NAME_VALUE_ARRAY" ]; then
_NAME_VALUE_ARRAY="${_NAME_VALUE_ARRAY}, "
fi
_NAME_VALUE_ARRAY="${_NAME_VALUE_ARRAY}${item}"
}
_collect_name_value_item() {
_NAME_VALUE_HAS_LIST=1
_name_value_array_add "$1"
}
_emit_name_value_array_opt() {
local section="$1"
local option="$2"
local toml_key="$3"
local scalar
_NAME_VALUE_ARRAY=
_NAME_VALUE_HAS_LIST=0
config_list_foreach "$section" "$option" _collect_name_value_item
if [ "$_NAME_VALUE_HAS_LIST" = "0" ]; then
config_get scalar "$section" "$option"
[ -n "$scalar" ] && _name_value_array_add "$scalar"
fi
[ -n "$_NAME_VALUE_ARRAY" ] || return 0
printf '%s = [%s]\n' "$toml_key" "$_NAME_VALUE_ARRAY"
}
_RAW_HAS_LIST=0
_emit_raw_item() {
_RAW_HAS_LIST=1
[ -n "$1" ] || return 0
printf '%s\n' "$1"
}
_emit_raw_opt() {
local section="$1"
local option="$2"
local scalar
_RAW_HAS_LIST=0
config_list_foreach "$section" "$option" _emit_raw_item
if [ "$_RAW_HAS_LIST" = "0" ]; then
config_get scalar "$section" "$option"
[ -n "$scalar" ] || return 0
printf '%s\n' "$scalar"
return $?
fi
return 0
}
_is_port() {
_is_port_value "$1"
}
_ALLOW_PORTS_ARRAY=
_ALLOW_PORTS_HAS_LIST=0
_ALLOW_PORTS_ERR=0
_allow_ports_array_add() {
local item="$1"
local start end table
item="$(_trim "$item")"
[ -z "$item" ] && return 0
case "$item" in
\{*\})
table="$item"
;;
*-*)
start="${item%%-*}"
end="${item#*-}"
start="$(_trim "$start")"
end="$(_trim "$end")"
if ! _is_port "$start" || ! _is_port "$end"; then
_err "invalid allow_ports range: $item"
_ALLOW_PORTS_ERR=1
return 0
fi
if [ "$start" -gt "$end" ]; then
_err "invalid allow_ports range, start is greater than end: $item"
_ALLOW_PORTS_ERR=1
return 0
fi
if [ "$start" = "$end" ]; then
table="{ single = $start }"
else
table="{ start = $start, end = $end }"
fi
;;
*)
if ! _is_port "$item"; then
_err "invalid allow_ports value: $item"
_ALLOW_PORTS_ERR=1
return 0
fi
table="{ single = $item }"
;;
esac
if [ -n "$_ALLOW_PORTS_ARRAY" ]; then
_ALLOW_PORTS_ARRAY="${_ALLOW_PORTS_ARRAY}, "
fi
_ALLOW_PORTS_ARRAY="${_ALLOW_PORTS_ARRAY}${table}"
}
_collect_allow_port_item() {
_ALLOW_PORTS_HAS_LIST=1
_allow_ports_array_add "$1"
}
_emit_allow_ports() {
local section="$1"
local option="$2"
local scalar item
_ALLOW_PORTS_ARRAY=
_ALLOW_PORTS_HAS_LIST=0
_ALLOW_PORTS_ERR=0
config_list_foreach "$section" "$option" _collect_allow_port_item
if [ "$_ALLOW_PORTS_HAS_LIST" = "0" ]; then
config_get scalar "$section" "$option"
while [ -n "$scalar" ]; do
case "$scalar" in
*,*)
item="${scalar%%,*}"
scalar="${scalar#*,}"
;;
*)
item="$scalar"
scalar=
;;
esac
_allow_ports_array_add "$item"
done
fi
[ "$_ALLOW_PORTS_ERR" = "0" ] || return 1
[ -n "$_ALLOW_PORTS_ARRAY" ] || return 0
printf 'allowPorts = [%s]\n' "$_ALLOW_PORTS_ARRAY"
}
_emit_auth_scopes() {
local section="$1"
local hb nwc v
_collect_array_option "$section" auth_additional_scopes
if [ -z "$_TOML_ARRAY" ]; then
config_get hb "$section" authenticate_heartbeats
config_get nwc "$section" authenticate_new_work_conns
v="$(_toml_bool "$hb")"
[ "$v" = "true" ] && _toml_array_add "HeartBeats"
v="$(_toml_bool "$nwc")"
[ "$v" = "true" ] && _toml_array_add "NewWorkConns"
fi
[ -n "$_TOML_ARRAY" ] || return 0
printf 'auth.additionalScopes = [%s]\n' "$_TOML_ARRAY"
}
_emit_admin_web_tls() {
local section="$1"
local enabled cert key
config_get enabled "$section" admin_tls_enable
[ "$(_toml_bool "$enabled")" = "true" ] || return 0
config_get cert "$section" admin_tls_cert_file
config_get key "$section" admin_tls_key_file
if [ -z "$cert" ] || [ -z "$key" ]; then
_err "admin_tls_cert_file and admin_tls_key_file are required when admin_tls_enable is enabled"
return 1
fi
if [ ! -r "$cert" ]; then
_err "admin TLS certificate file is not readable: $cert"
return 1
fi
if [ ! -r "$key" ]; then
_err "admin TLS private key file is not readable: $key"
return 1
fi
_toml_line webServer.tls.certFile "$cert" string
_toml_line webServer.tls.keyFile "$key" string
return 0
}
_emit_admin_web() {
local section="$1"
local port addr
config_get port "$section" admin_port
# Empty or 0 means web server is disabled.
[ -n "$port" ] && [ "$port" != "0" ] || return 0
config_get addr "$section" admin_addr
_toml_line webServer.addr "${addr:-127.0.0.1}" string
_toml_line webServer.port "$port" port
_emit_opt "$section" admin_user webServer.user string
_emit_opt "$section" admin_pwd webServer.password string
_emit_admin_web_tls "$section" || return 1
_emit_opt "$section" assets_dir webServer.assetsDir string
_emit_opt "$section" pprof_enable webServer.pprofEnable bool
_emit_opt "$section" enable_prometheus enablePrometheus bool
return 0
}
_emit_common() {
local section="common"
local method token token_source_type token_source_file_path
# Root options
_emit_opt "$section" bind_addr bindAddr string
_emit_opt "$section" bind_port bindPort port
_emit_opt "$section" kcp_bind_port kcpBindPort port0
_emit_opt "$section" quic_bind_port quicBindPort port0
_emit_opt "$section" proxy_bind_addr proxyBindAddr string
_emit_opt "$section" vhost_http_port vhostHTTPPort port0
_emit_opt "$section" vhost_https_port vhostHTTPSPort port0
_emit_opt "$section" vhost_http_timeout vhostHTTPTimeout int
_emit_opt "$section" tcpmux_httpconnect_port tcpmuxHTTPConnectPort port0
_emit_opt "$section" tcpmux_passthrough tcpmuxPassthrough bool
_emit_opt "$section" subdomain_host subDomainHost string
_emit_opt "$section" custom_404_page custom404Page string
_emit_opt "$section" udp_packet_size udpPacketSize int
_emit_opt "$section" detailed_errors_to_client detailedErrorsToClient bool
_emit_opt "$section" user_conn_timeout userConnTimeout int
_emit_opt "$section" nathole_analysis_data_reserve_hours natholeAnalysisDataReserveHours int
# Auth
config_get method "$section" authentication_method
config_get token "$section" token
config_get token_source_type "$section" token_source_type
config_get token_source_file_path "$section" token_source_file_path
[ -z "$method" ] && { [ -n "$token" ] || [ -n "$token_source_type" ]; } && method="token"
_toml_line auth.method "$method" string
if [ "$method" = "token" ] || [ -z "$method" ]; then
if [ -n "$token_source_type" ]; then
if [ -n "$token" ]; then
_err "token and token_source_type are mutually exclusive"
return 1
fi
case "$token_source_type" in
file)
if [ -z "$token_source_file_path" ]; then
_err "token_source_file_path is required when token_source_type=file"
return 1
fi
_toml_line auth.tokenSource.type "$token_source_type" string
_toml_line auth.tokenSource.file.path "$token_source_file_path" string
;;
exec)
local token_source_exec_command
config_get token_source_exec_command "$section" token_source_exec_command
if [ -z "$token_source_exec_command" ]; then
_err "token_source_exec_command is required when token_source_type=exec"
return 1
fi
_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=1
_toml_line auth.tokenSource.type "$token_source_type" string
_toml_line auth.tokenSource.exec.command "$token_source_exec_command" string
_emit_array_opt "$section" token_source_exec_args auth.tokenSource.exec.args
_emit_name_value_array_opt "$section" token_source_exec_env auth.tokenSource.exec.env
;;
*)
_err "unsupported token_source_type: $token_source_type"
return 1
;;
esac
else
_toml_line auth.token "$token" string
fi
fi
_emit_auth_scopes "$section"
if [ "$method" = "oidc" ]; then
_emit_opt "$section" oidc_issuer auth.oidc.issuer string
_emit_opt "$section" oidc_audience auth.oidc.audience string
_emit_opt "$section" oidc_skip_expiry_check auth.oidc.skipExpiryCheck bool
_emit_opt "$section" oidc_skip_issuer_check auth.oidc.skipIssuerCheck bool
fi
# Transport
_emit_opt "$section" max_pool_count transport.maxPoolCount int
_emit_opt "$section" tcp_mux transport.tcpMux bool
_emit_opt "$section" tcp_mux_keepalive_interval transport.tcpMuxKeepaliveInterval int
_emit_opt "$section" tcp_keepalive transport.tcpKeepalive int
_emit_opt "$section" heartbeat_timeout transport.heartbeatTimeout int
# QUIC
_emit_opt "$section" quic_keepalive_period transport.quic.keepalivePeriod int
_emit_opt "$section" quic_max_idle_timeout transport.quic.maxIdleTimeout int
_emit_opt "$section" quic_max_incoming_streams transport.quic.maxIncomingStreams int
# TLS
_emit_opt "$section" tls_force transport.tls.force bool
_emit_opt "$section" tls_cert_file transport.tls.certFile string
_emit_opt "$section" tls_key_file transport.tls.keyFile string
_emit_opt "$section" tls_trusted_ca_file transport.tls.trustedCaFile string
# Web dashboard server
_emit_admin_web "$section" || return 1
# Access control
_emit_allow_ports "$section" allow_ports || return 1
_emit_opt "$section" max_ports_per_client maxPortsPerClient int
# SSH tunnel gateway
_emit_opt "$section" ssh_tunnel_bind_port sshTunnelGateway.bindPort port0
_emit_opt "$section" ssh_tunnel_private_key_file sshTunnelGateway.privateKeyFile string
_emit_opt "$section" ssh_tunnel_auto_gen_private_key_path sshTunnelGateway.autoGenPrivateKeyPath string
_emit_opt "$section" ssh_tunnel_authorized_keys_file sshTunnelGateway.authorizedKeysFile string
# Log
_emit_opt "$section" log_file log.to string
_emit_opt "$section" log_level log.level string
_emit_opt "$section" log_max_days log.maxDays int
_emit_opt "$section" disable_log_color log.disablePrintColor bool
# Raw extra TOML lines kept for manual UCI usage; LuCI intentionally hides this.
_emit_raw_opt "$section" _
}
_emit_http_plugin() {
local section="$1"
local name addr path
config_get name "$section" name "$section"
config_get addr "$section" addr
config_get path "$section" path
if [ -z "$addr" ] || [ -z "$path" ]; then
_err "http plugin $name requires addr and path"
return 1
fi
_collect_array_option "$section" ops
if [ -z "$_TOML_ARRAY" ]; then
_err "http plugin $name requires at least one operation"
return 1
fi
printf '\n[[httpPlugins]]\n'
_toml_line name "$name" string
_toml_line addr "$addr" string
_toml_line path "$path" string
printf 'ops = [%s]\n' "$_TOML_ARRAY"
_emit_opt "$section" tls_verify tlsVerify bool
# Raw extra TOML lines for plugin options not covered by UCI options above.
_emit_raw_opt "$section" _
}
_find_init_section() {
[ -z "$init_cfg" ] && init_cfg="$1"
return 0
}
_append_conf_file() {
local file="$1"
local dir="/etc/frp/$NAME.d/"
local real_file
case "$file" in
"$dir"*.toml)
case "$file" in
*../*)
_err "additional config file outside $dir is not allowed: $file"
_TOML_ERR=1
return 1
;;
esac
;;
*)
_err "additional config file must be under $dir and end with .toml: $file"
_TOML_ERR=1
return 1
;;
esac
[ -r "$file" ] || {
_err "additional config file not readable: $file"
_TOML_ERR=1
return 1
}
real_file="$(readlink -f "$file")" || {
_err "additional config file path cannot be resolved: $file"
_TOML_ERR=1
return 1
}
case "$real_file" in
"$dir"*.toml)
;;
*)
_err "additional config file resolves outside $dir or is not .toml: $file"
_TOML_ERR=1
return 1
;;
esac
printf '\n' >> "$CONF_FILE"
cat "$real_file" >> "$CONF_FILE"
printf '\n' >> "$CONF_FILE"
}
_watch_conf_file() {
local file="$1"
[ -r "$file" ] && procd_set_param file "$file"
return 0
}
_append_env() {
procd_append_param env "$1"
}
service_triggers() {
procd_add_reload_trigger "$NAME"
}
start_service() {
local init_cfg=
local stdout=1
local stderr=1
local respawn=1
local run_user=
local run_group=
local old_umask
mkdir -p /var/etc
_TOML_ERR=0
_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0
config_load "$NAME"
config_foreach _find_init_section init
old_umask="$(umask)"
umask 077
: > "$CONF_FILE" || {
umask "$old_umask"
_err "failed to create $CONF_FILE"
return 1
}
umask "$old_umask"
chmod 600 "$CONF_FILE" || {
_err "failed to chmod $CONF_FILE"
return 1
}
{
printf '# This file is automatically generated from /etc/config/%s.\n' "$NAME"
printf '# Do not edit this file directly.\n\n'
_emit_common
} >> "$CONF_FILE" || return 1
[ "$_TOML_ERR" = "0" ] || return 1
if [ -n "$init_cfg" ]; then
config_list_foreach "$init_cfg" conf_inc _append_conf_file
config_get_bool stdout "$init_cfg" stdout 1
config_get_bool stderr "$init_cfg" stderr 1
config_get_bool respawn "$init_cfg" respawn 1
config_get run_user "$init_cfg" user
config_get run_group "$init_cfg" group
fi
{
config_foreach _emit_http_plugin http_plugin
} >> "$CONF_FILE" || return 1
[ "$_TOML_ERR" = "0" ] || return 1
if [ -n "$run_user" ]; then
chown "$run_user${run_group:+:$run_group}" "$CONF_FILE" 2>/dev/null || {
_err "failed to chown $CONF_FILE to $run_user${run_group:+:$run_group}"
return 1
}
fi
chmod 600 "$CONF_FILE" || {
_err "failed to chmod $CONF_FILE"
return 1
}
procd_open_instance
if [ "$_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC" = "1" ]; then
procd_set_param command "$PROG" -c "$CONF_FILE" --allow-unsafe=TokenSourceExec
else
procd_set_param command "$PROG" -c "$CONF_FILE"
fi
procd_set_param file "$CONF_FILE"
procd_set_param file "/etc/config/$NAME"
if [ -n "$init_cfg" ]; then
config_list_foreach "$init_cfg" conf_inc _watch_conf_file
fi
procd_set_param stdout "$stdout"
procd_set_param stderr "$stderr"
[ -n "$run_user" ] && procd_set_param user "$run_user"
[ -n "$run_group" ] && procd_set_param group "$run_group"
[ "$respawn" -eq 1 ] && procd_set_param respawn
if [ -n "$init_cfg" ]; then
config_list_foreach "$init_cfg" env _append_env
fi
procd_close_instance
}