diff --git a/frp/Makefile b/frp/Makefile new file mode 100644 index 00000000..647ac57f --- /dev/null +++ b/frp/Makefile @@ -0,0 +1,80 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=frp +PKG_VERSION:=0.70.1 +PKG_RELEASE:=5 + +PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz +PKG_SOURCE_URL:=https://codeload.github.com/fatedier/frp/tar.gz/v$(PKG_VERSION)? +PKG_HASH:=skip + +PKG_MAINTAINER:=Alexandru Ardelean +PKG_LICENSE:=Apache-2.0 +PKG_LICENSE_FILES:=LICENSE + +PKG_BUILD_DEPENDS:=golang/host node/host +PKG_BUILD_PARALLEL:=1 +PKG_BUILD_FLAGS:=no-mips16 + +GO_PKG:=github.com/fatedier/frp +GO_PKG_BUILD_PKG:=github.com/fatedier/frp/cmd/... +GO_PKG_LDFLAGS:=-s -w +GO_PKG_LDFLAGS_X:=github.com/fatedier/frp/pkg/util/version.version=$(PKG_VERSION) + +include $(INCLUDE_DIR)/package.mk +include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk + +define Build/Compile + ( \ + $(MAKE) -C $(PKG_BUILD_DIR)/web/frpc install ; \ + $(MAKE) -C $(PKG_BUILD_DIR)/web/frps install ; \ + $(MAKE) -C $(PKG_BUILD_DIR) web ; \ + $(call GoPackage/Build/Compile) ; \ + ) +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/ + $(INSTALL_CONF) ./files/$(2).config $(1)/etc/config/$(2) + $(INSTALL_BIN) ./files/$(2).init $(1)/etc/init.d/$(2) + + if [ -r ./files/$(2).uci-defaults ]; then \ + $(INSTALL_DIR) $(1)/etc/uci-defaults; \ + $(INSTALL_DATA) ./files/$(2).uci-defaults $(1)/etc/uci-defaults/$(2); \ + fi +endef + +define Package/frp/template + define Package/$(1) + SECTION:=net + CATEGORY:=Network + SUBMENU:=Web Servers/Proxies + TITLE:=$(1) - fast reverse proxy $(2) + URL:=https://github.com/fatedier/frp + DEPENDS:=$(GO_ARCH_DEPENDS) + endef + + define Package/$(1)/description + $(1) is a fast reverse proxy $(2) that helps you expose a local server + behind a NAT or firewall to the internet. + endef + + define Package/$(1)/conffiles +/etc/config/$(1) +/etc/frp/$(1).d/ + endef + + define Package/$(1)/install + $(call Package/frp/install,$$(1),$(1)) + endef +endef + +$(eval $(call Package/frp/template,frpc,client)) +$(eval $(call Package/frp/template,frps,server)) +$(eval $(call BuildPackage,frpc)) +$(eval $(call BuildPackage,frps)) diff --git a/frp/files/frpc.config b/frp/files/frpc.config new file mode 100644 index 00000000..333caa10 --- /dev/null +++ b/frp/files/frpc.config @@ -0,0 +1,64 @@ +config init + 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/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' + 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 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/frp/files/frpc.init b/frp/files/frpc.init new file mode 100644 index 00000000..62c2062f --- /dev/null +++ b/frp/files/frpc.init @@ -0,0 +1,1003 @@ +#!/bin/sh /etc/rc.common + +START=99 +USE_PROCD=1 + +NAME=frpc +PROG=/usr/bin/$NAME +CONF_FILE=/var/etc/$NAME.toml + +_err() { + echo "$*" >&2 + logger -p daemon.err -t "$NAME" "$*" +} + +_trim() { + printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' +} + +_toml_escape() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +_toml_quote() { + printf '"%s"' "$(_toml_escape "$1")" +} + +_toml_key_quote() { + _toml_quote "$1" +} + +_toml_bool() { + local v + v="$(printf '%s' "$1" | tr 'A-Z' 'a-z')" + + case "$v" in + 1|true|yes|on|enabled) + printf 'true' + ;; + 0|false|no|off|disabled) + printf 'false' + ;; + *) + printf 'false' + ;; + esac +} + +_TOML_ERR=0 +_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0 + +_is_uinteger() { + case "$1" in + ''|*[!0-9]*) + return 1 + ;; + esac + + return 0 +} + +_is_integer() { + local value="$1" + + case "$value" in + +*|-*) + value="${value#?}" + ;; + esac + + _is_uinteger "$value" +} + +_is_port_value() { + _is_uinteger "$1" || return 1 + [ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null +} + +_is_port_or_zero_value() { + _is_uinteger "$1" || return 1 + [ "$1" -ge 0 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null +} + +_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" +} + +_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 stdout=1 + local stderr=1 + local respawn=1 + local run_user= + local run_group= + local old_umask + + mkdir -p /var/etc + _TOML_ERR=0 + _ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0 + + config_load "$NAME" + + config_foreach _find_init_section init + + old_umask="$(umask)" + umask 077 + : > "$CONF_FILE" || { + umask "$old_umask" + _err "failed to create $CONF_FILE" + return 1 + } + umask "$old_umask" + chmod 600 "$CONF_FILE" || { + _err "failed to chmod $CONF_FILE" + return 1 + } + + { + printf '# This file is automatically generated from /etc/config/%s.\n' "$NAME" + printf '# Do not edit this file directly.\n\n' + + _emit_common + } >> "$CONF_FILE" || return 1 + [ "$_TOML_ERR" = "0" ] || return 1 + + if [ -n "$init_cfg" ]; then + config_list_foreach "$init_cfg" conf_inc _append_conf_file + + config_get_bool stdout "$init_cfg" stdout 1 + config_get_bool stderr "$init_cfg" stderr 1 + config_get_bool respawn "$init_cfg" respawn 1 + config_get run_user "$init_cfg" user + config_get run_group "$init_cfg" group + fi + + { + config_foreach _emit_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 + 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/frp/files/frpc.uci-defaults b/frp/files/frpc.uci-defaults new file mode 100644 index 00000000..4887acc9 --- /dev/null +++ b/frp/files/frpc.uci-defaults @@ -0,0 +1,109 @@ +#!/bin/sh + +. /lib/functions.sh + +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 + + if [ -z "$name" ]; then + uci_set "$package" "$section" name "$section" + changed=1 + fi +} + +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/frp/files/frps.config b/frp/files/frps.config new file mode 100644 index 00000000..1cd8bfa3 --- /dev/null +++ b/frp/files/frps.config @@ -0,0 +1,77 @@ +config init + 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/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_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/frp/files/frps.init b/frp/files/frps.init new file mode 100644 index 00000000..86c4fd0d --- /dev/null +++ b/frp/files/frps.init @@ -0,0 +1,780 @@ +#!/bin/sh /etc/rc.common + +START=99 +USE_PROCD=1 + +NAME=frps +PROG=/usr/bin/$NAME +CONF_FILE=/var/etc/$NAME.toml + +_err() { + echo "$*" >&2 + logger -p daemon.err -t "$NAME" "$*" +} + +_trim() { + printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' +} + +_toml_escape() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +_toml_quote() { + printf '"%s"' "$(_toml_escape "$1")" +} + +_toml_key_quote() { + _toml_quote "$1" +} + +_toml_bool() { + local v + v="$(printf '%s' "$1" | tr 'A-Z' 'a-z')" + + case "$v" in + 1|true|yes|on|enabled) + printf 'true' + ;; + 0|false|no|off|disabled) + printf 'false' + ;; + *) + printf 'false' + ;; + esac +} + +_TOML_ERR=0 +_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0 + +_is_uinteger() { + case "$1" in + ''|*[!0-9]*) + return 1 + ;; + esac + + return 0 +} + +_is_integer() { + local value="$1" + + case "$value" in + +*|-*) + value="${value#?}" + ;; + esac + + _is_uinteger "$value" +} + +_is_port_value() { + _is_uinteger "$1" || return 1 + [ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null +} + +_is_port_or_zero_value() { + _is_uinteger "$1" || return 1 + [ "$1" -ge 0 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null +} + +_toml_line() { + local key="$1" + local value="$2" + local type="$3" + + [ -z "$value" ] && return 0 + + case "$type" in + bool) + printf '%s = %s\n' "$key" "$(_toml_bool "$value")" + ;; + int|integer|number) + if ! _is_integer "$value"; then + _err "invalid integer for $key: $value" + _TOML_ERR=1 + return 1 + fi + + printf '%s = %s\n' "$key" "$value" + ;; + port) + if ! _is_port_value "$value"; then + _err "invalid port for $key: $value" + _TOML_ERR=1 + return 1 + fi + + printf '%s = %s\n' "$key" "$value" + ;; + port0) + if ! _is_port_or_zero_value "$value"; then + _err "invalid port for $key: $value" + _TOML_ERR=1 + return 1 + fi + + printf '%s = %s\n' "$key" "$value" + ;; + *) + printf '%s = %s\n' "$key" "$(_toml_quote "$value")" + ;; + esac +} + +_emit_opt() { + local section="$1" + local option="$2" + local toml_key="$3" + local type="$4" + local value + + config_get value "$section" "$option" + _toml_line "$toml_key" "$value" "$type" +} + +_TOML_ARRAY= +_TOML_HAS_LIST=0 + +_toml_array_add() { + local item="$1" + + item="$(_trim "$item")" + [ -z "$item" ] && return 0 + + if [ -n "$_TOML_ARRAY" ]; then + _TOML_ARRAY="${_TOML_ARRAY}, " + fi + + _TOML_ARRAY="${_TOML_ARRAY}$(_toml_quote "$item")" +} + +_collect_array_item() { + _TOML_HAS_LIST=1 + _toml_array_add "$1" +} + +_collect_array_option() { + local section="$1" + local option="$2" + local scalar item + + _TOML_ARRAY= + _TOML_HAS_LIST=0 + + config_list_foreach "$section" "$option" _collect_array_item + + if [ "$_TOML_HAS_LIST" = "0" ]; then + config_get scalar "$section" "$option" + + while [ -n "$scalar" ]; do + case "$scalar" in + *,*) + item="${scalar%%,*}" + scalar="${scalar#*,}" + ;; + *) + item="$scalar" + scalar= + ;; + esac + + _toml_array_add "$item" + done + fi + + return 0 +} + +_emit_array_opt() { + local section="$1" + local option="$2" + local toml_key="$3" + + _collect_array_option "$section" "$option" + + [ -n "$_TOML_ARRAY" ] || return 0 + printf '%s = [%s]\n' "$toml_key" "$_TOML_ARRAY" +} + +_NAME_VALUE_ARRAY= +_NAME_VALUE_HAS_LIST=0 + +_name_value_array_add() { + local line="$1" + local key value item + + case "$line" in + *=*) + key="${line%%=*}" + value="${line#*=}" + ;; + *) + return 0 + ;; + esac + + key="$(_trim "$key")" + value="$(_trim "$value")" + + [ -n "$key" ] || return 0 + + item="{ name = $(_toml_quote "$key"), value = $(_toml_quote "$value") }" + + if [ -n "$_NAME_VALUE_ARRAY" ]; then + _NAME_VALUE_ARRAY="${_NAME_VALUE_ARRAY}, " + fi + + _NAME_VALUE_ARRAY="${_NAME_VALUE_ARRAY}${item}" +} + +_collect_name_value_item() { + _NAME_VALUE_HAS_LIST=1 + _name_value_array_add "$1" +} + +_emit_name_value_array_opt() { + local section="$1" + local option="$2" + local toml_key="$3" + local scalar + + _NAME_VALUE_ARRAY= + _NAME_VALUE_HAS_LIST=0 + + config_list_foreach "$section" "$option" _collect_name_value_item + + if [ "$_NAME_VALUE_HAS_LIST" = "0" ]; then + config_get scalar "$section" "$option" + [ -n "$scalar" ] && _name_value_array_add "$scalar" + fi + + [ -n "$_NAME_VALUE_ARRAY" ] || return 0 + printf '%s = [%s]\n' "$toml_key" "$_NAME_VALUE_ARRAY" +} + +_RAW_HAS_LIST=0 + +_emit_raw_item() { + _RAW_HAS_LIST=1 + [ -n "$1" ] || return 0 + printf '%s\n' "$1" +} + +_emit_raw_opt() { + local section="$1" + local option="$2" + local scalar + + _RAW_HAS_LIST=0 + config_list_foreach "$section" "$option" _emit_raw_item + + if [ "$_RAW_HAS_LIST" = "0" ]; then + config_get scalar "$section" "$option" + [ -n "$scalar" ] || return 0 + printf '%s\n' "$scalar" + return $? + fi + + return 0 +} + +_is_port() { + _is_port_value "$1" +} + +_ALLOW_PORTS_ARRAY= +_ALLOW_PORTS_HAS_LIST=0 +_ALLOW_PORTS_ERR=0 + +_allow_ports_array_add() { + local item="$1" + local start end table + + item="$(_trim "$item")" + [ -z "$item" ] && return 0 + + case "$item" in + \{*\}) + table="$item" + ;; + + *-*) + start="${item%%-*}" + end="${item#*-}" + + start="$(_trim "$start")" + end="$(_trim "$end")" + + if ! _is_port "$start" || ! _is_port "$end"; then + _err "invalid allow_ports range: $item" + _ALLOW_PORTS_ERR=1 + return 0 + fi + + if [ "$start" -gt "$end" ]; then + _err "invalid allow_ports range, start is greater than end: $item" + _ALLOW_PORTS_ERR=1 + return 0 + fi + + if [ "$start" = "$end" ]; then + table="{ single = $start }" + else + table="{ start = $start, end = $end }" + fi + ;; + + *) + if ! _is_port "$item"; then + _err "invalid allow_ports value: $item" + _ALLOW_PORTS_ERR=1 + return 0 + fi + + table="{ single = $item }" + ;; + esac + + if [ -n "$_ALLOW_PORTS_ARRAY" ]; then + _ALLOW_PORTS_ARRAY="${_ALLOW_PORTS_ARRAY}, " + fi + + _ALLOW_PORTS_ARRAY="${_ALLOW_PORTS_ARRAY}${table}" +} + +_collect_allow_port_item() { + _ALLOW_PORTS_HAS_LIST=1 + _allow_ports_array_add "$1" +} + +_emit_allow_ports() { + local section="$1" + local option="$2" + local scalar item + + _ALLOW_PORTS_ARRAY= + _ALLOW_PORTS_HAS_LIST=0 + _ALLOW_PORTS_ERR=0 + + config_list_foreach "$section" "$option" _collect_allow_port_item + + if [ "$_ALLOW_PORTS_HAS_LIST" = "0" ]; then + config_get scalar "$section" "$option" + + while [ -n "$scalar" ]; do + case "$scalar" in + *,*) + item="${scalar%%,*}" + scalar="${scalar#*,}" + ;; + *) + item="$scalar" + scalar= + ;; + esac + + _allow_ports_array_add "$item" + done + fi + + [ "$_ALLOW_PORTS_ERR" = "0" ] || return 1 + [ -n "$_ALLOW_PORTS_ARRAY" ] || return 0 + + printf 'allowPorts = [%s]\n' "$_ALLOW_PORTS_ARRAY" +} + +_emit_auth_scopes() { + local section="$1" + local hb nwc v + + _collect_array_option "$section" auth_additional_scopes + + if [ -z "$_TOML_ARRAY" ]; then + config_get hb "$section" authenticate_heartbeats + config_get nwc "$section" authenticate_new_work_conns + + v="$(_toml_bool "$hb")" + [ "$v" = "true" ] && _toml_array_add "HeartBeats" + + v="$(_toml_bool "$nwc")" + [ "$v" = "true" ] && _toml_array_add "NewWorkConns" + fi + + [ -n "$_TOML_ARRAY" ] || return 0 + printf 'auth.additionalScopes = [%s]\n' "$_TOML_ARRAY" +} + +_emit_admin_web_tls() { + local section="$1" + local enabled cert key + + config_get enabled "$section" admin_tls_enable + + [ "$(_toml_bool "$enabled")" = "true" ] || return 0 + + config_get cert "$section" admin_tls_cert_file + config_get key "$section" admin_tls_key_file + + if [ -z "$cert" ] || [ -z "$key" ]; then + _err "admin_tls_cert_file and admin_tls_key_file are required when admin_tls_enable is enabled" + return 1 + fi + + if [ ! -r "$cert" ]; then + _err "admin TLS certificate file is not readable: $cert" + return 1 + fi + + if [ ! -r "$key" ]; then + _err "admin TLS private key file is not readable: $key" + return 1 + fi + + _toml_line webServer.tls.certFile "$cert" string + _toml_line webServer.tls.keyFile "$key" string + + return 0 +} + +_emit_admin_web() { + local section="$1" + local port addr + + config_get port "$section" admin_port + + # Empty or 0 means web server is disabled. + [ -n "$port" ] && [ "$port" != "0" ] || return 0 + + config_get addr "$section" admin_addr + _toml_line webServer.addr "${addr:-127.0.0.1}" string + _toml_line webServer.port "$port" port + _emit_opt "$section" admin_user webServer.user string + _emit_opt "$section" admin_pwd webServer.password string + _emit_admin_web_tls "$section" || return 1 + _emit_opt "$section" assets_dir webServer.assetsDir string + _emit_opt "$section" pprof_enable webServer.pprofEnable bool + _emit_opt "$section" enable_prometheus enablePrometheus bool + + return 0 +} + +_emit_common() { + local section="common" + local method token token_source_type token_source_file_path + + # Root options + _emit_opt "$section" bind_addr bindAddr string + _emit_opt "$section" bind_port bindPort port + _emit_opt "$section" kcp_bind_port kcpBindPort port0 + _emit_opt "$section" quic_bind_port quicBindPort port0 + _emit_opt "$section" proxy_bind_addr proxyBindAddr string + _emit_opt "$section" vhost_http_port vhostHTTPPort port0 + _emit_opt "$section" vhost_https_port vhostHTTPSPort port0 + _emit_opt "$section" vhost_http_timeout vhostHTTPTimeout int + _emit_opt "$section" tcpmux_httpconnect_port tcpmuxHTTPConnectPort port0 + _emit_opt "$section" tcpmux_passthrough tcpmuxPassthrough bool + _emit_opt "$section" subdomain_host subDomainHost string + _emit_opt "$section" custom_404_page custom404Page string + _emit_opt "$section" udp_packet_size udpPacketSize int + _emit_opt "$section" detailed_errors_to_client detailedErrorsToClient bool + _emit_opt "$section" user_conn_timeout userConnTimeout int + _emit_opt "$section" nathole_analysis_data_reserve_hours natholeAnalysisDataReserveHours int + + # Auth + config_get method "$section" authentication_method + config_get token "$section" token + config_get token_source_type "$section" token_source_type + config_get token_source_file_path "$section" token_source_file_path + + [ -z "$method" ] && { [ -n "$token" ] || [ -n "$token_source_type" ]; } && method="token" + + _toml_line auth.method "$method" string + + if [ "$method" = "token" ] || [ -z "$method" ]; then + if [ -n "$token_source_type" ]; then + if [ -n "$token" ]; then + _err "token and token_source_type are mutually exclusive" + return 1 + fi + + case "$token_source_type" in + file) + if [ -z "$token_source_file_path" ]; then + _err "token_source_file_path is required when token_source_type=file" + return 1 + fi + + _toml_line auth.tokenSource.type "$token_source_type" string + _toml_line auth.tokenSource.file.path "$token_source_file_path" string + ;; + + exec) + local token_source_exec_command + + config_get token_source_exec_command "$section" token_source_exec_command + if [ -z "$token_source_exec_command" ]; then + _err "token_source_exec_command is required when token_source_type=exec" + return 1 + fi + + _ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=1 + _toml_line auth.tokenSource.type "$token_source_type" string + _toml_line auth.tokenSource.exec.command "$token_source_exec_command" string + _emit_array_opt "$section" token_source_exec_args auth.tokenSource.exec.args + _emit_name_value_array_opt "$section" token_source_exec_env auth.tokenSource.exec.env + ;; + + *) + _err "unsupported token_source_type: $token_source_type" + return 1 + ;; + esac + else + _toml_line auth.token "$token" string + fi + fi + + _emit_auth_scopes "$section" + + if [ "$method" = "oidc" ]; then + _emit_opt "$section" oidc_issuer auth.oidc.issuer string + _emit_opt "$section" oidc_audience auth.oidc.audience string + _emit_opt "$section" oidc_skip_expiry_check auth.oidc.skipExpiryCheck bool + _emit_opt "$section" oidc_skip_issuer_check auth.oidc.skipIssuerCheck bool + fi + + # Transport + _emit_opt "$section" max_pool_count transport.maxPoolCount int + _emit_opt "$section" tcp_mux transport.tcpMux bool + _emit_opt "$section" tcp_mux_keepalive_interval transport.tcpMuxKeepaliveInterval int + _emit_opt "$section" tcp_keepalive transport.tcpKeepalive int + _emit_opt "$section" heartbeat_timeout transport.heartbeatTimeout int + + # QUIC + _emit_opt "$section" quic_keepalive_period transport.quic.keepalivePeriod int + _emit_opt "$section" quic_max_idle_timeout transport.quic.maxIdleTimeout int + _emit_opt "$section" quic_max_incoming_streams transport.quic.maxIncomingStreams int + + # TLS + _emit_opt "$section" tls_force transport.tls.force bool + _emit_opt "$section" tls_cert_file transport.tls.certFile string + _emit_opt "$section" tls_key_file transport.tls.keyFile string + _emit_opt "$section" tls_trusted_ca_file transport.tls.trustedCaFile string + + # Web dashboard server + _emit_admin_web "$section" || return 1 + + # Access control + _emit_allow_ports "$section" allow_ports || return 1 + _emit_opt "$section" max_ports_per_client maxPortsPerClient int + + # SSH tunnel gateway + _emit_opt "$section" ssh_tunnel_bind_port sshTunnelGateway.bindPort port0 + _emit_opt "$section" ssh_tunnel_private_key_file sshTunnelGateway.privateKeyFile string + _emit_opt "$section" ssh_tunnel_auto_gen_private_key_path sshTunnelGateway.autoGenPrivateKeyPath string + _emit_opt "$section" ssh_tunnel_authorized_keys_file sshTunnelGateway.authorizedKeysFile string + + # Log + _emit_opt "$section" log_file log.to string + _emit_opt "$section" log_level log.level string + _emit_opt "$section" log_max_days log.maxDays int + _emit_opt "$section" disable_log_color log.disablePrintColor bool + + # Raw extra TOML lines kept for manual UCI usage; LuCI intentionally hides this. + _emit_raw_opt "$section" _ +} + +_emit_http_plugin() { + local section="$1" + local name addr path + + config_get name "$section" name "$section" + config_get addr "$section" addr + config_get path "$section" path + + if [ -z "$addr" ] || [ -z "$path" ]; then + _err "http plugin $name requires addr and path" + return 1 + fi + + _collect_array_option "$section" ops + + if [ -z "$_TOML_ARRAY" ]; then + _err "http plugin $name requires at least one operation" + return 1 + fi + + printf '\n[[httpPlugins]]\n' + _toml_line name "$name" string + _toml_line addr "$addr" string + _toml_line path "$path" string + printf 'ops = [%s]\n' "$_TOML_ARRAY" + _emit_opt "$section" tls_verify tlsVerify bool + + # Raw extra TOML lines for plugin options not covered by UCI options above. + _emit_raw_opt "$section" _ +} + +_find_init_section() { + [ -z "$init_cfg" ] && init_cfg="$1" + return 0 +} + +_append_conf_file() { + local file="$1" + local dir="/etc/frp/$NAME.d/" + local real_file + + case "$file" in + "$dir"*.toml) + case "$file" in + *../*) + _err "additional config file outside $dir is not allowed: $file" + _TOML_ERR=1 + return 1 + ;; + esac + ;; + *) + _err "additional config file must be under $dir and end with .toml: $file" + _TOML_ERR=1 + return 1 + ;; + esac + + [ -r "$file" ] || { + _err "additional config file not readable: $file" + _TOML_ERR=1 + return 1 + } + + real_file="$(readlink -f "$file")" || { + _err "additional config file path cannot be resolved: $file" + _TOML_ERR=1 + return 1 + } + + case "$real_file" in + "$dir"*.toml) + ;; + *) + _err "additional config file resolves outside $dir or is not .toml: $file" + _TOML_ERR=1 + return 1 + ;; + esac + + printf '\n' >> "$CONF_FILE" + cat "$real_file" >> "$CONF_FILE" + printf '\n' >> "$CONF_FILE" +} + +_watch_conf_file() { + local file="$1" + + [ -r "$file" ] && procd_set_param file "$file" + return 0 +} + +_append_env() { + procd_append_param env "$1" +} + +service_triggers() { + procd_add_reload_trigger "$NAME" +} + +start_service() { + local init_cfg= + local stdout=1 + local stderr=1 + local respawn=1 + local run_user= + local run_group= + local old_umask + + mkdir -p /var/etc + _TOML_ERR=0 + _ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0 + + config_load "$NAME" + + config_foreach _find_init_section init + + old_umask="$(umask)" + umask 077 + : > "$CONF_FILE" || { + umask "$old_umask" + _err "failed to create $CONF_FILE" + return 1 + } + umask "$old_umask" + chmod 600 "$CONF_FILE" || { + _err "failed to chmod $CONF_FILE" + return 1 + } + + { + printf '# This file is automatically generated from /etc/config/%s.\n' "$NAME" + printf '# Do not edit this file directly.\n\n' + + _emit_common + } >> "$CONF_FILE" || return 1 + [ "$_TOML_ERR" = "0" ] || return 1 + + if [ -n "$init_cfg" ]; then + config_list_foreach "$init_cfg" conf_inc _append_conf_file + + config_get_bool stdout "$init_cfg" stdout 1 + config_get_bool stderr "$init_cfg" stderr 1 + config_get_bool respawn "$init_cfg" respawn 1 + config_get run_user "$init_cfg" user + config_get run_group "$init_cfg" group + fi + + { + config_foreach _emit_http_plugin http_plugin + } >> "$CONF_FILE" || return 1 + [ "$_TOML_ERR" = "0" ] || return 1 + + if [ -n "$run_user" ]; then + chown "$run_user${run_group:+:$run_group}" "$CONF_FILE" 2>/dev/null || { + _err "failed to chown $CONF_FILE to $run_user${run_group:+:$run_group}" + return 1 + } + fi + + chmod 600 "$CONF_FILE" || { + _err "failed to chmod $CONF_FILE" + return 1 + } + + procd_open_instance + if [ "$_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC" = "1" ]; then + procd_set_param command "$PROG" -c "$CONF_FILE" --allow-unsafe=TokenSourceExec + else + procd_set_param command "$PROG" -c "$CONF_FILE" + fi + procd_set_param file "$CONF_FILE" + procd_set_param file "/etc/config/$NAME" + + if [ -n "$init_cfg" ]; then + config_list_foreach "$init_cfg" conf_inc _watch_conf_file + fi + + procd_set_param stdout "$stdout" + procd_set_param stderr "$stderr" + + [ -n "$run_user" ] && procd_set_param user "$run_user" + [ -n "$run_group" ] && procd_set_param group "$run_group" + [ "$respawn" -eq 1 ] && procd_set_param respawn + + if [ -n "$init_cfg" ]; then + config_list_foreach "$init_cfg" env _append_env + fi + + procd_close_instance +} diff --git a/frp/files/frps.uci-defaults b/frp/files/frps.uci-defaults new file mode 100644 index 00000000..2e3133a3 --- /dev/null +++ b/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 diff --git a/frp/test.sh b/frp/test.sh new file mode 100755 index 00000000..fd53475e --- /dev/null +++ b/frp/test.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# frpc/frps both accept "verify -c " to validate a config without +# connecting, which exercises the TOML config loader end-to-end. +case "$1" in +frpc) + conf="/tmp/frpc.$$.toml" + printf 'serverAddr = "127.0.0.1"\nserverPort = 7000\n' > "$conf" + frpc verify -c "$conf" || { echo "FAIL: frpc rejected a valid config"; rm -f "$conf"; exit 1; } + rm -f "$conf" + ;; +frps) + conf="/tmp/frps.$$.toml" + printf 'bindPort = 7000\n' > "$conf" + frps verify -c "$conf" || { echo "FAIL: frps rejected a valid config"; rm -f "$conf"; exit 1; } + rm -f "$conf" + ;; +*) + exit 0 + ;; +esac diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index a890f201..fe304a7b 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.8.19 -PKG_RELEASE:=241 +PKG_RELEASE:=242 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua index 683e41e8..dbacc653 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua @@ -33,15 +33,17 @@ for _, v in pairs(nodes_table) do end local socks_list = {} -m:foreach("socks", function(s) - if s.enabled == "1" and s.node then - socks_list[#socks_list + 1] = { - id = s[".name"], - remark = translate("Socks Config") .. " " .. string.format("[%s %s]", s.port, translate("Port")), - group = "Socks" - } - end -end) +if has_singbox or has_xray then + m:foreach("socks", function(s) + if s.enabled == "1" and s.node then + socks_list[#socks_list + 1] = { + id = s[".name"], + remark = translate("Socks Config") .. " " .. string.format("[%s %s]", s.port, translate("Port")), + group = "Socks" + } + end + end) +end -- [[ ACLs Settings ]]-- s = m:section(NamedSection, arg[1], translate("ACLs"), translate("ACLs")) diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index b270d583..5df9a7c5 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -62,11 +62,13 @@ m:foreach("socks", function(s) id = id, remark = id .. " - " .. (remark or translate("Misconfigured")) } - socks_list[#socks_list + 1] = { - id = s[".name"], - remark = translate("Socks Config") .. " " .. string.format("[%s %s]", s.port, translate("Port")), - group = "Socks" - } + if has_singbox or has_xray then + socks_list[#socks_list + 1] = { + id = s[".name"], + remark = translate("Socks Config") .. " " .. string.format("[%s %s]", s.port, translate("Port")), + group = "Socks" + } + end end end) diff --git a/luci-app-passwall/root/usr/share/passwall/app.sh b/luci-app-passwall/root/usr/share/passwall/app.sh index 20c56566..d586d056 100755 --- a/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/luci-app-passwall/root/usr/share/passwall/app.sh @@ -325,20 +325,14 @@ run_socks() { log_file="/dev/null" fi - local node2socks_port=0 - local type remarks server_host server_port + local type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') + local remarks=$(config_n_get $node remarks) + local server_host=$(config_n_get $node address) + local server_port=$(config_n_get $node port) + if [ "$(config_get_type $node)" = "socks" ]; then - node2socks_port=$(config_n_get $node port 0) - fi - if [ "$node2socks_port" = "0" ]; then - type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') - remarks=$(config_n_get $node remarks) - server_host=$(config_n_get $node address) - server_port=$(config_n_get $node port) - else type="socks" server_host="127.0.0.1" - server_port=$node2socks_port remarks="Socks 配置($server_port 端口)" fi @@ -382,16 +376,8 @@ run_socks() { json_add_string "server_port" "${server_port}" case "$type" in socks) - local _socks_address _socks_port _socks_username _socks_password - if [ "$node2socks_port" = "0" ]; then - _socks_address=$(config_n_get $node address) - _socks_port=$(config_n_get $node port) - _socks_username=$(config_n_get $node username) - _socks_password=$(config_n_get $node password) - else - _socks_address="127.0.0.1" - _socks_port=$node2socks_port - fi + local socks_username=$(config_n_get $node username) + local socks_password=$(config_n_get $node password) [ "$http_port" != "0" ] && { http_flag=1 config_file="${config_file%%.*}+http${config_file#${config_file%%.*}}" @@ -403,10 +389,10 @@ run_socks() { json_add_string "local_socks_address" "$bind" json_add_string "local_socks_port" "$socks_port" json_add_string "server_proto" "socks" - json_add_string "server_address" "${_socks_address}" - json_add_string "server_port" "${_socks_port}" - json_add_string "server_username" "${_socks_username}" - json_add_string "server_password" "${_socks_password}" + json_add_string "server_address" "${server_host}" + json_add_string "server_port" "${server_port}" + json_add_string "server_username" "${socks_username}" + json_add_string "server_password" "${socks_password}" if [ -n "${SINGBOX_BIN}" ]; then type="sing-box" local bin="${SINGBOX_BIN}" @@ -519,32 +505,35 @@ run_socks() { start_global() { [ -z "$NODE" ] && return 1 - - local type=$(echo $(config_n_get $NODE type) | tr 'A-Z' 'a-z') - local config_file=${GLOBAL_ACL_PATH}/global.json local log_file=${GLOBAL_ACL_PATH}/global.log - local node2socks_port=0 - local remarks server_host port - if [ "$(config_get_type $NODE)" = "socks" ]; then - node2socks_port=$(config_n_get $NODE port 0) - fi - if [ "$node2socks_port" = "0" ]; then - remarks=$(config_n_get $NODE remarks) - server_host=$(config_n_get $NODE address) - port=$(config_n_get $NODE port) - else + + local remarks=$(config_n_get $NODE remarks) + local server_host=$(config_n_get $NODE address) + local port=$(config_n_get $NODE port) + local type=$(echo $(config_n_get $NODE type) | tr 'A-Z' 'a-z') + + local is_socks_cfg=0 + [ "$(config_get_type $NODE)" = "socks" ] && is_socks_cfg=1 + + if [ "$type" = "socks" ] || [ "$is_socks_cfg" = "1" ] ; then if [ "${DNS_MODE}" = "xray" ]; then type="xray" elif [ "${DNS_MODE}" = "sing-box" ]; then type="sing-box" + elif [ -n "${SINGBOX_BIN}" ]; then + type="sing-box" + elif [ -n "${XRAY_BIN}" ]; then + type="xray" else type="socks" fi - server_host="127.0.0.1" - port=$node2socks_port - remarks="Socks 配置($port 端口)" + if [ "$is_socks_cfg" = "1" ] ; then + server_host="127.0.0.1" + remarks="Socks 配置($port 端口)" + fi fi + local enable_log=$(config_n_get @global[0] log_node 1) [ "$enable_log" != "1" ] && log_file="/dev/null" [ -n "$server_host" ] && [ -n "$port" ] && { @@ -583,22 +572,15 @@ start_global() { case "$type" in socks) _socks_flag=1 - if [ "$node2socks_port" = "0" ]; then - _socks_address=$(config_n_get $NODE address) - _socks_port=$(config_n_get $NODE port) - _socks_username=$(config_n_get $NODE username) - _socks_password=$(config_n_get $NODE password) - else - _socks_address="127.0.0.1" - _socks_port=$node2socks_port - fi + _socks_address=$server_host + _socks_port=$port + _socks_username=$(config_n_get $NODE username) + _socks_password=$(config_n_get $NODE password) [ -z "$can_ipt" ] && { - local _config_file=$config_file - _config_file="Global_SOCKS_${NODE}.json" - local _port=$(get_new_port 3001) - run_socks flag="global" node=$NODE bind=127.0.0.1 socks_port=${_port} config_file=${_config_file} + local _config_file="global_${NODE}_socks.json" _socks_address="127.0.0.1" - _socks_port=${_port} + _socks_port=$GLOBAL_SOCKS_port + run_socks flag="global" node=$NODE bind=${node_socks_bind} socks_port=${_socks_port} config_file=${_config_file} unset _socks_username unset _socks_password } @@ -1463,6 +1445,10 @@ acl_app() { type="xray" elif [ "${dns_mode}" = "sing-box" ]; then type="sing-box" + elif [ -n "${SINGBOX_BIN}" ]; then + type="sing-box" + elif [ -n "${XRAY_BIN}" ]; then + type="xray" fi else type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') diff --git a/luci-app-wwand/Makefile b/luci-app-wwand/Makefile index 91653c42..3a712056 100644 --- a/luci-app-wwand/Makefile +++ b/luci-app-wwand/Makefile @@ -15,12 +15,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-wwand -PKG_RELEASE:=3 +PKG_RELEASE:=4 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git -PKG_SOURCE_VERSION:=c9c2e1870b094cdc7bdd9013a7e4862e030b9500 -PKG_SOURCE_DATE:=2026-08-23 +PKG_SOURCE_VERSION:=7f0c58f56cc060bd3355fed7fd86a68ad7003e9e +PKG_SOURCE_DATE:=2026-08-24 PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL-2.0-only