diff --git a/net/frp/Makefile b/net/frp/Makefile index 07371ce6840a9..8232f3becbb37 100644 --- a/net/frp/Makefile +++ b/net/frp/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=frp PKG_VERSION:=0.69.1 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/fatedier/frp/tar.gz/v$(PKG_VERSION)? @@ -36,6 +36,7 @@ endef define Package/frp/install $(INSTALL_DIR) $(1)/usr/bin/ $(INSTALL_DIR) $(1)/etc/config/ + $(INSTALL_DIR) $(1)/etc/frp/$(2).d/ $(INSTALL_DIR) $(1)/etc/init.d/ $(INSTALL_BIN) $(GO_PKG_BUILD_BIN_DIR)/$(2) $(1)/usr/bin/ @@ -65,6 +66,7 @@ define Package/frp/template define Package/$(1)/conffiles /etc/config/$(1) +/etc/frp/$(1).d/ endef define Package/$(1)/install diff --git a/net/frp/files/frpc.config b/net/frp/files/frpc.config index 3d305f41c7beb..333caa10142b2 100644 --- a/net/frp/files/frpc.config +++ b/net/frp/files/frpc.config @@ -1,20 +1,64 @@ config init - option stdout 1 - option stderr 1 - option user frpc - option group frpc - option respawn 1 + option stdout '1' + option stderr '1' +# Uncomment to run frpc as an existing user/group. Keep disabled by +# default to avoid permission issues with certificate, log and included +# config files. +# option user 'nobody' +# option group 'nogroup' + option respawn '1' # For full configuration options, see: -# https://github.com/fatedier/frp/blob/master/conf/legacy/frpc_legacy_full.ini +# https://github.com/fatedier/frp/blob/master/conf/frpc_full_example.toml +# +# Additional config files should be readable root-level TOML fragments. +# The service will refuse to start if a listed fragment is missing or outside /etc/frp/frpc.d/. +# Use frp's own includes option or per-proxy raw settings for proxy/visitor tables. +# list conf_inc '/etc/frp/frpc.d/frpc_extra.toml' config conf 'common' - option server_addr 127.0.0.1 - option server_port 7000 -# List options with name="_" will be directly appended to config file -# list _ '# Key-A=Value-A' + option server_addr '127.0.0.1' + option server_port '7000' + option authentication_method 'token' +# option token 'your_token' +# Alternatively, load a token from a file or command. Exec token sources +# require frpc to run with --allow-unsafe=TokenSourceExec; the init script +# adds that flag automatically when token_source_type is exec. +# option token_source_type 'file' +# option token_source_file_path '/etc/frp/client_token' +# option token_source_type 'exec' +# option token_source_exec_command '/usr/bin/get-frpc-token' +# list token_source_exec_args '--format' +# list token_source_exec_args 'raw' +# list token_source_exec_env 'TOKEN_SERVICE=production' + option login_fail_exit 'true' + option protocol 'tcp' + option wire_protocol 'v1' + option tcp_mux 'true' + option tls_enable 'true' + option disable_custom_tls_first_byte 'true' + # Web server is disabled when admin_port is empty or 0. +# option admin_addr '127.0.0.1' +# option admin_port '7400' +# option admin_user 'admin' +# option admin_pwd 'admin' + option admin_tls_enable 'false' + option pprof_enable 'false' +# option admin_tls_cert_file '/etc/ssl/acme/example.com.fullchain.crt' +# option admin_tls_key_file '/etc/ssl/acme/example.com.key' + option log_file 'console' + option log_level 'info' + option log_max_days '3' +# Uncomment to enable runtime proxy/visitor persistence via frpc web UI or API. +# option store_path '/etc/frp/frpc_store.json' +# List options with name "_" will be directly appended as raw TOML lines. +# Use this only for options not covered by UCI options above. +# Do not duplicate keys generated by UCI options above. +# list _ 'uncovered.option = "value"' config conf 'ssh' - option type tcp - option local_ip 127.0.0.1 - option local_port 22 - option remote_port 6000 + option name 'ssh' + option type 'tcp' + option local_ip '127.0.0.1' + option local_port '22' + option remote_port '6000' + option enabled 'true' diff --git a/net/frp/files/frpc.init b/net/frp/files/frpc.init index 68fe43c4e022a..62c2062ff8354 100644 --- a/net/frp/files/frpc.init +++ b/net/frp/files/frpc.init @@ -5,76 +5,999 @@ USE_PROCD=1 NAME=frpc PROG=/usr/bin/$NAME +CONF_FILE=/var/etc/$NAME.toml _err() { echo "$*" >&2 logger -p daemon.err -t "$NAME" "$*" } -config_cb() { - [ $# -eq 0 ] && return +_trim() { + printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' +} - local type="$1" - local name="$2" - if [ "$type" = "conf" ]; then - echo "[$name]" >> "$conf_file" - option_cb() { - local option="$1" - local value="$2" - [ "$option" = "name" ] && \ - sed -i "s/$CONFIG_SECTION/$value/g" "$conf_file" || \ - echo "$option = $value" >> "$conf_file"; - } - list_cb() { - local name="$1" - local value="$2" - [ "$name" = "_" ] && echo "$value" >> "$conf_file" - } - else - [ "$type" = "init" ] && init_cfg="$name" - option_cb() { return 0; } - list_cb() { return 0; } +_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 +} + +_is_negative_integer() { + local value + + case "$1" in + -*) + value="${1#-}" + _is_uinteger "$value" && [ "$value" -gt 0 ] 2>/dev/null + ;; + *) + return 1 + ;; + esac +} + +_is_visitor_bind_port_value() { + _is_negative_integer "$1" && return 0 + _is_port_value "$1" +} + +_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" + ;; + visitor_bind_port) + if ! _is_visitor_bind_port_value "$value"; then + _err "invalid visitor bind 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" } -service_triggers() -{ +_emit_kv_pair() { + local prefix="$1" + local line="$2" + local value_type="$3" + local key value toml_key + + case "$line" in + *=*) + key="${line%%=*}" + value="${line#*=}" + ;; + *) + return 0 + ;; + esac + + key="$(_trim "$key")" + value="$(_trim "$value")" + + [ -z "$key" ] && return 0 + + toml_key="${prefix}.$(_toml_key_quote "$key")" + + case "$value_type" in + bool) + printf '%s = %s\n' "$toml_key" "$(_toml_bool "$value")" + ;; + int|integer|number) + printf '%s = %s\n' "$toml_key" "$value" + ;; + *) + printf '%s = %s\n' "$toml_key" "$(_toml_quote "$value")" + ;; + esac +} + +_KV_PREFIX= +_KV_TYPE= +_KV_HAS_LIST=0 + +_emit_kv_list_item() { + _KV_HAS_LIST=1 + _emit_kv_pair "$_KV_PREFIX" "$1" "$_KV_TYPE" +} + +_emit_kv_opt() { + local section="$1" + local option="$2" + local prefix="$3" + local value_type="$4" + local scalar + + _KV_PREFIX="$prefix" + _KV_TYPE="${value_type:-string}" + _KV_HAS_LIST=0 + + config_list_foreach "$section" "$option" _emit_kv_list_item + + if [ "$_KV_HAS_LIST" = "0" ]; then + config_get scalar "$section" "$option" + [ -n "$scalar" ] && _emit_kv_pair "$prefix" "$scalar" "$value_type" + fi + + return 0 +} + +_HEADERS_ARRAY= +_HEADERS_HAS_LIST=0 + +_headers_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")" + + [ -z "$key" ] && return 0 + + item="{ name = $(_toml_quote "$key"), value = $(_toml_quote "$value") }" + + if [ -n "$_HEADERS_ARRAY" ]; then + _HEADERS_ARRAY="${_HEADERS_ARRAY}, " + fi + + _HEADERS_ARRAY="${_HEADERS_ARRAY}${item}" +} + +_collect_header_item() { + _HEADERS_HAS_LIST=1 + _headers_array_add "$1" +} + +_emit_headers_opt() { + local section="$1" + local option="$2" + local toml_key="$3" + local scalar + + _HEADERS_ARRAY= + _HEADERS_HAS_LIST=0 + + config_list_foreach "$section" "$option" _collect_header_item + + if [ "$_HEADERS_HAS_LIST" = "0" ]; then + config_get scalar "$section" "$option" + [ -n "$scalar" ] && _headers_array_add "$scalar" + fi + + [ -n "$_HEADERS_ARRAY" ] || return 0 + printf '%s = [%s]\n' "$toml_key" "$_HEADERS_ARRAY" +} + +_emit_name_value_array_opt() { + _emit_headers_opt "$@" +} + +_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 +} + +_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 + + return 0 +} + +_emit_common() { + local section="common" + local method token token_source_type token_source_file_path + + # Root options + _emit_opt "$section" client_id clientID string + _emit_opt "$section" user user string + _emit_opt "$section" server_addr serverAddr string + _emit_opt "$section" server_port serverPort port + _emit_opt "$section" nat_hole_stun_server natHoleStunServer string + _emit_opt "$section" login_fail_exit loginFailExit bool + _emit_opt "$section" dns_server dnsServer string + _emit_array_opt "$section" start start + _emit_opt "$section" udp_packet_size udpPacketSize int + _emit_array_opt "$section" includes includes + + # 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_client_id auth.oidc.clientID string + _emit_opt "$section" oidc_client_secret auth.oidc.clientSecret string + _emit_opt "$section" oidc_audience auth.oidc.audience string + _emit_opt "$section" oidc_scope auth.oidc.scope string + _emit_opt "$section" oidc_token_endpoint_url auth.oidc.tokenEndpointURL string + _emit_kv_opt "$section" oidc_additional_endpoint_params auth.oidc.additionalEndpointParams string + _emit_opt "$section" oidc_trusted_ca_file auth.oidc.trustedCaFile string + _emit_opt "$section" oidc_insecure_skip_verify auth.oidc.insecureSkipVerify bool + _emit_opt "$section" oidc_proxy_url auth.oidc.proxyURL string + fi + + # Transport + _emit_opt "$section" dial_server_timeout transport.dialServerTimeout int + _emit_opt "$section" dial_server_keepalive transport.dialServerKeepalive int + _emit_opt "$section" http_proxy transport.proxyURL string + _emit_opt "$section" pool_count transport.poolCount int + _emit_opt "$section" tcp_mux transport.tcpMux bool + _emit_opt "$section" tcp_mux_keepalive_interval transport.tcpMuxKeepaliveInterval int + _emit_opt "$section" protocol transport.protocol string + _emit_opt "$section" wire_protocol transport.wireProtocol string + _emit_opt "$section" connect_server_local_ip transport.connectServerLocalIP string + _emit_opt "$section" heartbeat_interval transport.heartbeatInterval 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_enable transport.tls.enable 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 + _emit_opt "$section" tls_server_name transport.tls.serverName string + _emit_opt "$section" disable_custom_tls_first_byte transport.tls.disableCustomTLSFirstByte bool + + # Web admin server + _emit_admin_web "$section" || return 1 + + # Feature gates / virtual net + _emit_kv_opt "$section" feature_gates featureGates bool + _emit_opt "$section" virtual_net_address virtualNet.address string + _emit_opt "$section" store_path store.path string + + # Client metadata + _emit_kv_opt "$section" metadatas metadatas 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_plugin() { + local section="$1" + local plugin role + + config_get plugin "$section" plugin + config_get role "$section" role + + [ -z "$plugin" ] && return 0 + + _toml_line plugin.type "$plugin" string + + case "$plugin" in + http_proxy) + _emit_opt "$section" plugin_http_user plugin.httpUser string + _emit_opt "$section" plugin_http_passwd plugin.httpPassword string + ;; + + socks5) + _emit_opt "$section" plugin_user plugin.username string + _emit_opt "$section" plugin_passwd plugin.password string + ;; + + unix_domain_socket) + _emit_opt "$section" plugin_unix_path plugin.unixPath string + ;; + + static_file) + _emit_opt "$section" plugin_local_path plugin.localPath string + _emit_opt "$section" plugin_strip_prefix plugin.stripPrefix string + _emit_opt "$section" plugin_http_user plugin.httpUser string + _emit_opt "$section" plugin_http_passwd plugin.httpPassword string + ;; + + https2http|https2https) + _emit_opt "$section" plugin_local_addr plugin.localAddr string + _emit_opt "$section" plugin_crt_path plugin.crtPath string + _emit_opt "$section" plugin_key_path plugin.keyPath string + _emit_opt "$section" plugin_host_header_rewrite plugin.hostHeaderRewrite string + _emit_opt "$section" plugin_enable_http2 plugin.enableHTTP2 bool + _emit_kv_opt "$section" plugin_request_headers plugin.requestHeaders.set string + ;; + + http2https|http2http) + _emit_opt "$section" plugin_local_addr plugin.localAddr string + _emit_opt "$section" plugin_host_header_rewrite plugin.hostHeaderRewrite string + _emit_kv_opt "$section" plugin_request_headers plugin.requestHeaders.set string + ;; + + tls2raw) + _emit_opt "$section" plugin_local_addr plugin.localAddr string + _emit_opt "$section" plugin_crt_path plugin.crtPath string + _emit_opt "$section" plugin_key_path plugin.keyPath string + ;; + + virtual_net) + [ "$role" = "visitor" ] && \ + _emit_opt "$section" plugin_destination_ip plugin.destinationIP string + ;; + esac + + return 0 +} + +_emit_proxy() { + local section="$1" + local pname ptype plugin hct + + [ "$section" = "common" ] && return 0 + + config_get pname "$section" name "$section" + config_get ptype "$section" type + config_get plugin "$section" plugin + + [ -z "$ptype" ] && ptype="tcp" + + printf '\n[[proxies]]\n' + _toml_line name "$pname" string + _toml_line type "$ptype" string + + _emit_opt "$section" enabled enabled bool + + # localIP/localPort are useful for normal local-service proxies. + # If plugin is enabled, plugin usually handles the local service itself. + if [ -z "$plugin" ]; then + case "$ptype" in + tcp|udp|http|https|stcp|xtcp|sudp|tcpmux) + _emit_opt "$section" local_ip localIP string + _emit_opt "$section" local_port localPort port + ;; + esac + fi + + # remotePort is mainly used by tcp/udp-style listeners. + # Plugin proxies with type=tcp also need remotePort. + case "$ptype" in + tcp|udp) + _emit_opt "$section" remote_port remotePort port0 + ;; + esac + + # Common proxy transport options. + _emit_opt "$section" bandwidth_limit transport.bandwidthLimit string + _emit_opt "$section" bandwidth_limit_mode transport.bandwidthLimitMode string + _emit_opt "$section" use_encryption transport.useEncryption bool + _emit_opt "$section" use_compression transport.useCompression bool + _emit_opt "$section" proxy_protocol_version transport.proxyProtocolVersion string + + # HTTP / HTTPS domain options. + case "$ptype" in + http|https) + _emit_array_opt "$section" custom_domains customDomains + _emit_opt "$section" subdomain subdomain string + ;; + esac + + # HTTP-only options. Skip these when plugin is enabled to avoid stale normal HTTP proxy fields. + case "$ptype" in + http) + _emit_opt "$section" route_by_http_user routeByHTTPUser string + if [ -z "$plugin" ]; then + _emit_array_opt "$section" locations locations + _emit_opt "$section" http_user httpUser string + _emit_opt "$section" http_pwd httpPassword string + _emit_opt "$section" host_header_rewrite hostHeaderRewrite string + _emit_kv_opt "$section" request_headers requestHeaders.set string + _emit_kv_opt "$section" response_headers responseHeaders.set string + fi + ;; + esac + + # TCPMUX options. + case "$ptype" in + tcpmux) + _emit_array_opt "$section" custom_domains customDomains + _emit_opt "$section" http_user httpUser string + _emit_opt "$section" http_pwd httpPassword string + _emit_opt "$section" multiplexer multiplexer string + _emit_opt "$section" route_by_http_user routeByHTTPUser string + ;; + esac + + # Load balancer options. + _emit_opt "$section" group loadBalancer.group string + _emit_opt "$section" group_key loadBalancer.groupKey string + + # Health check options. + config_get hct "$section" health_check_type + if [ -n "$hct" ] && [ -z "$plugin" ]; then + case "$ptype" in + tcp|http|https|tcpmux) + _emit_opt "$section" health_check_type healthCheck.type string + _emit_opt "$section" health_check_timeout_s healthCheck.timeoutSeconds int + _emit_opt "$section" health_check_max_failed healthCheck.maxFailed int + _emit_opt "$section" health_check_interval_s healthCheck.intervalSeconds int + + if [ "$hct" = "http" ]; then + _emit_opt "$section" health_check_url healthCheck.path string + _emit_headers_opt "$section" health_check_headers healthCheck.httpHeaders + fi + ;; + esac + fi + + # STCP / XTCP / SUDP server options. + case "$ptype" in + stcp|xtcp|sudp) + _emit_opt "$section" sk secretKey string + _emit_array_opt "$section" allow_users allowUsers + ;; + esac + + # NAT traversal options are mainly useful for XTCP. + case "$ptype" in + xtcp) + _emit_opt "$section" nat_disable_assisted_addrs natTraversal.disableAssistedAddrs bool + ;; + esac + + # Metadata / annotations. + _emit_kv_opt "$section" metadatas metadatas string + _emit_kv_opt "$section" annotations annotations string + + # Plugin options are filtered by plugin type in _emit_plugin(). + _emit_plugin "$section" + + # Raw extra TOML lines. + _emit_raw_opt "$section" _ +} + +_emit_visitor() { + local section="$1" + local vname vtype + + config_get vname "$section" name "$section" + config_get vtype "$section" type + + [ -z "$vtype" ] && vtype="stcp" + + printf '\n[[visitors]]\n' + _toml_line name "$vname" string + _toml_line type "$vtype" string + + _emit_opt "$section" server_user serverUser string + _emit_opt "$section" server_name serverName string + _emit_opt "$section" sk secretKey string + _emit_opt "$section" bind_addr bindAddr string + _emit_opt "$section" bind_port bindPort visitor_bind_port + _emit_opt "$section" enabled enabled bool + _emit_opt "$section" use_encryption transport.useEncryption bool + _emit_opt "$section" use_compression transport.useCompression bool + + # XTCP visitor-specific options. + case "$vtype" in + xtcp) + _emit_opt "$section" visitor_protocol protocol string + _emit_opt "$section" keep_tunnel_open keepTunnelOpen bool + _emit_opt "$section" max_retries_an_hour maxRetriesAnHour int + _emit_opt "$section" min_retry_interval minRetryInterval int + _emit_opt "$section" fallback_to fallbackTo string + _emit_opt "$section" fallback_timeout_ms fallbackTimeoutMs int + _emit_opt "$section" nat_disable_assisted_addrs natTraversal.disableAssistedAddrs bool + ;; + esac + + # Visitor plugin, for example virtual_net. + _emit_plugin "$section" + + # Raw extra TOML lines. + _emit_raw_opt "$section" _ +} + +_emit_conf_section() { + local section="$1" + local role ptype + + [ "$section" = "common" ] && return 0 + + config_get role "$section" role + config_get ptype "$section" type + + if [ "$role" = "visitor" ]; then + case "$ptype" in + ''|stcp|xtcp|sudp) + _emit_visitor "$section" + return $? + ;; + *) + _err "visitor section $section has unsupported type: $ptype" + _TOML_ERR=1 + return 1 + ;; + esac + fi + + _emit_proxy "$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 conf_file="/var/etc/$NAME.ini" + 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 - > "$conf_file" config_load "$NAME" - local stdout stderr user group respawn env conf_inc - uci_validate_section "$NAME" init "$init_cfg" \ - 'stdout:bool:1' \ - 'stderr:bool:1' \ - 'user:string' \ - 'group:string' \ - 'respawn:bool:1' \ - 'env:list(string)' \ - 'conf_inc:list(string)' - - local err=$? - [ $err -ne 0 ] && { - _err "uci_validate_section returned $err" + 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 } - [ -n "$conf_inc" ] && config_list_foreach "$init_cfg" conf_inc cat >> "$conf_file" + { + 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_conf_section conf + } >> "$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 - procd_set_param command "$PROG" -c "$conf_file" - procd_set_param file $conf_file - procd_set_param stdout $stdout - procd_set_param stderr $stderr - [ -n "$user" ] && procd_set_param user "$user" - [ -n "$group" ] && procd_set_param group "$group" - [ $respawn -eq 1 ] && procd_set_param respawn - [ -n "$env" ] && config_list_foreach "$init_cfg" env "procd_append_param env" + 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 } diff --git a/net/frp/files/frpc.uci-defaults b/net/frp/files/frpc.uci-defaults index 4883a2d8c6a1e..4887acc90617b 100644 --- a/net/frp/files/frpc.uci-defaults +++ b/net/frp/files/frpc.uci-defaults @@ -2,18 +2,108 @@ . /lib/functions.sh -upgrade() { - local section=$1 +changed=0 +init_section= +package=frpc + +find_init_section() { + [ -z "$init_section" ] && init_section="$1" + return 0 +} + +set_if_empty() { + local section="$1" + local option="$2" + local value="$3" + local cur + + config_get cur "$section" "$option" + + if [ -z "$cur" ]; then + uci_set "$package" "$section" "$option" "$value" + changed=1 + fi +} + +copy_if_empty() { + local section="$1" + local old_option="$2" + local new_option="$3" + local value + + config_get value "$section" "$old_option" + [ -n "$value" ] || return 0 + + set_if_empty "$section" "$new_option" "$value" +} + +upgrade_common() { + local section="$1" + local dashboard_tls_mode + + [ "$section" = "common" ] || return 0 + + set_if_empty "$section" server_addr 127.0.0.1 + set_if_empty "$section" server_port 7000 + set_if_empty "$section" authentication_method token + set_if_empty "$section" login_fail_exit true + set_if_empty "$section" protocol tcp + set_if_empty "$section" wire_protocol v1 + set_if_empty "$section" tcp_mux true + set_if_empty "$section" tls_enable true + set_if_empty "$section" disable_custom_tls_first_byte true + + config_get dashboard_tls_mode "$section" dashboard_tls_mode + if [ -n "$dashboard_tls_mode" ]; then + set_if_empty "$section" admin_tls_enable "$dashboard_tls_mode" + else + set_if_empty "$section" admin_tls_enable false + fi + + copy_if_empty "$section" dashboard_addr admin_addr + copy_if_empty "$section" dashboard_port admin_port + copy_if_empty "$section" dashboard_user admin_user + copy_if_empty "$section" dashboard_pwd admin_pwd + copy_if_empty "$section" dashboard_tls_cert_file admin_tls_cert_file + copy_if_empty "$section" dashboard_tls_key_file admin_tls_key_file + + set_if_empty "$section" pprof_enable false + set_if_empty "$section" log_file console + set_if_empty "$section" log_level info + set_if_empty "$section" log_max_days 3 +} + +upgrade_init() { + if [ -z "$init_section" ]; then + init_section="$(uci add "$package" init)" || return 0 + changed=1 + fi + + set_if_empty "$init_section" stdout 1 + set_if_empty "$init_section" stderr 1 + set_if_empty "$init_section" respawn 1 +} + +upgrade_proxy_name() { + local section="$1" local name + [ "$section" != "common" ] || return 0 - config_get name $section name + + config_get name "$section" name + if [ -z "$name" ]; then - uci_set frpc "$section" name "$section" - uci_commit frpc + uci_set "$package" "$section" name "$section" + changed=1 fi } -config_load frpc -config_foreach upgrade conf +config_load "$package" +config_foreach find_init_section init +upgrade_init +config_foreach upgrade_common conf +config_foreach upgrade_proxy_name conf + +[ "$changed" -eq 1 ] && uci_commit "$package" exit 0 diff --git a/net/frp/files/frps.config b/net/frp/files/frps.config index 8957604ac49f2..1cd8bfa39cd98 100644 --- a/net/frp/files/frps.config +++ b/net/frp/files/frps.config @@ -1,13 +1,77 @@ config init - option stdout 1 - option stderr 1 - option user frps - option group frps - option respawn 1 + option stdout '1' + option stderr '1' +# Uncomment to run frps as an existing user/group. Keep disabled by +# default to avoid permission issues with certificate, log and included +# config files. +# option user 'nobody' +# option group 'nogroup' + option respawn '1' # For full configuration options, see: -# https://github.com/fatedier/frp/blob/master/conf/legacy/frps_legacy_full.ini +# https://github.com/fatedier/frp/blob/master/conf/frps_full_example.toml +# +# Additional config files should be readable root-level TOML fragments. +# The service will refuse to start if a listed fragment is missing or outside /etc/frp/frps.d/. +# Use raw settings below for table-specific extra options such as HTTP plugins. +# list conf_inc '/etc/frp/frps.d/frps_extra.toml' config conf 'common' - option bind_port 7000 -# List options with name="_" will be directly appended to config file -# list _ '# Key-A=Value-A' + option bind_addr '0.0.0.0' + option bind_port '7000' + + option authentication_method 'token' +# option token 'your_token' +# Alternatively, load a token from a file or command. Exec token sources +# require frps to run with --allow-unsafe=TokenSourceExec; the init script +# adds that flag automatically when token_source_type is exec. +# option token_source_type 'file' +# option token_source_file_path '/etc/frp/server_token' +# option token_source_type 'exec' +# option token_source_exec_command '/usr/bin/get-frps-token' +# list token_source_exec_args '--format' +# list token_source_exec_args 'raw' +# list token_source_exec_env 'TOKEN_SERVICE=production' + + option max_pool_count '5' + option tcp_mux 'true' + option tls_force 'false' + option detailed_errors_to_client 'true' + + # Web server is disabled when admin_port is empty or 0. +# option admin_addr '127.0.0.1' +# option admin_port '7500' +# option admin_user 'admin' +# option admin_pwd 'admin' + option admin_tls_enable 'false' + option pprof_enable 'false' + option enable_prometheus 'false' +# option admin_tls_cert_file '/etc/ssl/acme/example.com.fullchain.crt' +# option admin_tls_key_file '/etc/ssl/acme/example.com.key' + +# Allow ports can be single ports or ranges. +# list allow_ports '2000-3000' +# list allow_ports '3001' + + option log_file 'console' + option log_level 'info' + option log_max_days '3' + +# List options with name "_" will be directly appended as raw TOML lines. +# Use this only for options not covered by UCI options above. +# Do not duplicate keys generated by UCI options above. +# list _ 'uncovered.option = "value"' + +# HTTP plugin hooks for frps. This emits [[httpPlugins]] TOML tables. +#config http_plugin 'user_manager' +# option name 'user-manager' +# option addr '127.0.0.1:9000' +# option path '/handler' +# list ops 'Login' +# option tls_verify 'false' + +#config http_plugin 'port_manager' +# option name 'port-manager' +# option addr '127.0.0.1:9001' +# option path '/handler' +# list ops 'NewProxy' +# option tls_verify 'false' diff --git a/net/frp/files/frps.init b/net/frp/files/frps.init index 38f714fb1bed6..86c4fd0d55668 100644 --- a/net/frp/files/frps.init +++ b/net/frp/files/frps.init @@ -5,74 +5,776 @@ USE_PROCD=1 NAME=frps PROG=/usr/bin/$NAME +CONF_FILE=/var/etc/$NAME.toml _err() { echo "$*" >&2 logger -p daemon.err -t "$NAME" "$*" } -config_cb() { - [ $# -eq 0 ] && return +_trim() { + printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' +} - local type="$1" - local name="$2" - if [ "$type" = "conf" ]; then - echo "[$name]" >> "$conf_file" - option_cb() { - local option="$1" - local value="$2" - echo "$option = $value" >> "$conf_file" - } - list_cb() { - local name="$1" - local value="$2" - [ "$name" = "_" ] && echo "$value" >> "$conf_file" - } - else - [ "$type" = "init" ] && init_cfg="$name" - option_cb() { return 0; } - list_cb() { return 0; } +_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" } -service_triggers() -{ +_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 conf_file="/var/etc/$NAME.ini" + 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 - > "$conf_file" config_load "$NAME" - local stdout stderr user group respawn env conf_inc - uci_validate_section "$NAME" init "$init_cfg" \ - 'stdout:bool:1' \ - 'stderr:bool:1' \ - 'user:string' \ - 'group:string' \ - 'respawn:bool:1' \ - 'env:list(string)' \ - 'conf_inc:list(string)' - - local err=$? - [ $err -ne 0 ] && { - _err "uci_validate_section returned $err" + 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 } - [ -n "$conf_inc" ] && config_list_foreach "$init_cfg" conf_inc cat >> "$conf_file" + { + 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 - procd_set_param command "$PROG" -c "$conf_file" - procd_set_param file $conf_file - procd_set_param stdout $stdout - procd_set_param stderr $stderr - [ -n "$user" ] && procd_set_param user "$user" - [ -n "$group" ] && procd_set_param group "$group" - [ $respawn -eq 1 ] && procd_set_param respawn - [ -n "$env" ] && config_list_foreach "$init_cfg" env "procd_append_param env" + 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 } diff --git a/net/frp/files/frps.uci-defaults b/net/frp/files/frps.uci-defaults new file mode 100644 index 0000000000000..2e3133a3b613d --- /dev/null +++ b/net/frp/files/frps.uci-defaults @@ -0,0 +1,105 @@ +#!/bin/sh + +. /lib/functions.sh + +changed=0 +init_section= +package=frps + +find_init_section() { + [ -z "$init_section" ] && init_section="$1" + return 0 +} + +set_if_empty() { + local section="$1" + local option="$2" + local value="$3" + local cur + + config_get cur "$section" "$option" + + if [ -z "$cur" ]; then + uci_set "$package" "$section" "$option" "$value" + changed=1 + fi +} + +copy_if_empty() { + local section="$1" + local old_option="$2" + local new_option="$3" + local value + + config_get value "$section" "$old_option" + [ -n "$value" ] || return 0 + + set_if_empty "$section" "$new_option" "$value" +} + +upgrade_common() { + local section="$1" + local dashboard_tls_mode + local tls_only + local nat_hole_hours + + [ "$section" = "common" ] || return 0 + + set_if_empty "$section" bind_addr 0.0.0.0 + set_if_empty "$section" bind_port 7000 + set_if_empty "$section" authentication_method token + set_if_empty "$section" max_pool_count 5 + set_if_empty "$section" tcp_mux true + set_if_empty "$section" detailed_errors_to_client true + set_if_empty "$section" pprof_enable false + set_if_empty "$section" enable_prometheus false + + config_get tls_only "$section" tls_only + if [ -n "$tls_only" ]; then + set_if_empty "$section" tls_force "$tls_only" + else + set_if_empty "$section" tls_force false + fi + + config_get dashboard_tls_mode "$section" dashboard_tls_mode + if [ -n "$dashboard_tls_mode" ]; then + set_if_empty "$section" admin_tls_enable "$dashboard_tls_mode" + else + set_if_empty "$section" admin_tls_enable false + fi + + copy_if_empty "$section" dashboard_addr admin_addr + copy_if_empty "$section" dashboard_port admin_port + copy_if_empty "$section" dashboard_user admin_user + copy_if_empty "$section" dashboard_pwd admin_pwd + copy_if_empty "$section" dashboard_tls_cert_file admin_tls_cert_file + copy_if_empty "$section" dashboard_tls_key_file admin_tls_key_file + + config_get nat_hole_hours "$section" nat_hole_analysis_data_reserve_hours + [ -n "$nat_hole_hours" ] && \ + set_if_empty "$section" nathole_analysis_data_reserve_hours "$nat_hole_hours" + + set_if_empty "$section" log_file console + set_if_empty "$section" log_level info + set_if_empty "$section" log_max_days 3 +} + +upgrade_init() { + if [ -z "$init_section" ]; then + init_section="$(uci add "$package" init)" || return 0 + changed=1 + fi + + set_if_empty "$init_section" stdout 1 + set_if_empty "$init_section" stderr 1 + set_if_empty "$init_section" respawn 1 +} + +config_load "$package" +config_foreach find_init_section init +upgrade_init +config_foreach upgrade_common conf + +[ "$changed" -eq 1 ] && uci_commit "$package" + +exit 0