]> git.99rst.org Git - openwrt-packages.git/commitdiff
frp: migrate generated configuration from INI to TOML
authorRoc Lai <redacted>
Tue, 14 Jul 2026 23:04:33 +0000 (07:04 +0800)
committerAlexandru Ardelean <redacted>
Wed, 19 Aug 2026 19:28:50 +0000 (22:28 +0300)
Migrate frpc and frps configuration generation to the TOML format used by
current frp releases.

- generate TOML configuration from UCI options
- add UCI defaults migration for existing installations
- install additional fragments under /etc/frp/frpc.d and /etc/frp/frps.d
- preserve manual raw TOML fragment support without exposing it in LuCI
- keep client and server TCP mux settings aligned
- update the package metadata for the current frp release artifacts

Signed-off-by: Roc Lai <redacted>
net/frp/Makefile
net/frp/files/frpc.config
net/frp/files/frpc.init
net/frp/files/frpc.uci-defaults
net/frp/files/frps.config
net/frp/files/frps.init
net/frp/files/frps.uci-defaults [new file with mode: 0644]

index ded82033ed940e2f9acd9aed66295a9d52657757..2cd3f814835d186329aecd4cc1ea05fe10490a47 100644 (file)
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
 
 PKG_NAME:=frp
 PKG_VERSION:=0.70.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
index 3d305f41c7beb6f0949d44c109ca3abab6013fc1..333caa10142b277c3925cec0bef8f9c94102e276 100644 (file)
@@ -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'
index 68fe43c4e022a49e3e88203f888b05cd566469ee..62c2062ff835445d3adf1ccb6db56b06c1418361 100644 (file)
@@ -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
 }
index 4883a2d8c6a1e8cf9eda4ee6123662f6034f9200..4887acc90617bf3ebb771b9ffd19ff4c64a3742f 100644 (file)
 
 . /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
index 8957604ac49f2094d8fe4bf5a3ffc38faf5b1c0d..1cd8bfa39cd988ec7ed88fc86ab254602f338ae8 100644 (file)
@@ -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'
index 38f714fb1bed6a69cac1a389f3a70d032a8a9c21..86c4fd0d556685abc60910d0134de6299a8dfeb3 100644 (file)
@@ -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 (file)
index 0000000..2e3133a
--- /dev/null
@@ -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
git clone https://git.99rst.org/PROJECT