diff --git a/gecoosac/Makefile b/gecoosac/Makefile index e474ee481..94095f39e 100644 --- a/gecoosac/Makefile +++ b/gecoosac/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=gecoosac PKG_VERSION:=2.2.20251015 -PKG_RELEASE:=20 +PKG_RELEASE:=21 PKG_MAINTAINER:=Roc Lai PKG_LICENSE:=AGPL-3.0-only @@ -43,8 +43,8 @@ define Package/$(PKG_NAME) CATEGORY:=Network TITLE:=gecoosac server (version $(PKG_VERSION)) URL:=http://www.cnrouter.com/ - DEPENDS:=@(aarch64||arm||i386||mips||mipsel||x86_64) +openssl-util - PROVIDES:=gecoosac-files + DEPENDS:=@(aarch64||arm||i386||mips||mipsel||x86_64) +openssl-util +coreutils-timeout + PROVIDES:=gecoosac-files gecoosac-common endef define Package/$(PKG_NAME)/conffiles @@ -66,11 +66,13 @@ define Package/$(PKG_NAME)/install $(INSTALL_DIR) $(1)/etc/gecoosac/tls $(INSTALL_DIR) $(1)/etc/init.d $(INSTALL_DIR) $(1)/etc/uci-defaults + $(INSTALL_DIR) $(1)/usr/share/gecoosac $(INSTALL_BIN) $(PKG_BUILD_DIR)/$(PKG_NAME) $(1)/usr/bin $(INSTALL_BIN) ./files/etc/init.d/gecoosac $(1)/etc/init.d/gecoosac $(INSTALL_BIN) ./files/etc/uci-defaults/gecoosac $(1)/etc/uci-defaults/gecoosac $(INSTALL_CONF) ./files/etc/config/gecoosac $(1)/etc/config/gecoosac + $(INSTALL_DATA) ./files/usr/share/gecoosac/common.sh $(1)/usr/share/gecoosac/common.sh endef $(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/gecoosac/files/etc/init.d/gecoosac b/gecoosac/files/etc/init.d/gecoosac index b2bcd51e7..db1dea1b5 100644 --- a/gecoosac/files/etc/init.d/gecoosac +++ b/gecoosac/files/etc/init.d/gecoosac @@ -3,6 +3,7 @@ . /lib/functions.sh . /lib/functions/procd.sh +. /usr/share/gecoosac/common.sh USE_PROCD=1 @@ -10,13 +11,9 @@ START=90 STOP=10 PROG=/usr/bin/gecoosac -DEFAULT_DB_DIR=/etc/gecoosac -DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload -DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt -DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key -DEFAULT_PID_DIR=/var/run DEFAULT_LANG=zh CERT_TIMEOUT=60 +CERT_RENEW_BEFORE=2592000 init_conf() { local section_type @@ -47,193 +44,6 @@ init_conf() { config_get "log" "config" "log" "0" } -is_abs_path() { - case "$1" in - /*) return 0 ;; - *) return 1 ;; - esac -} - -normalize_path() { - local path="$1" - local part normalized parent - - is_abs_path "$path" || return 1 - normalized="/" - path="${path#/}" - - while [ -n "$path" ]; do - part="${path%%/*}" - if [ "$part" = "$path" ]; then - path="" - else - path="${path#*/}" - fi - - case "$part" in - ""|.) ;; - ..) - if [ "$normalized" != "/" ]; then - parent="${normalized%/*}" - [ -n "$parent" ] || parent="/" - normalized="$parent" - fi - ;; - *) normalized="${normalized%/}/$part" ;; - esac - done - - printf '%s\n' "$normalized" -} - -path_has_clear_stage_component() { - local path="$1" part rest - - path="$(normalize_path "$path")" || return 1 - rest="${path#/}" - while [ -n "$rest" ]; do - part="${rest%%/*}" - if [ "$part" = "$rest" ]; then - rest="" - else - rest="${rest#*/}" - fi - case "$part" in - .gecoosac-clear.*) return 0 ;; - esac - done - - return 1 -} - -path_uses_clear_stage() { - local path="$1" resolved - - path_has_clear_stage_component "$path" && return 0 - if [ -e "$path" ] || [ -L "$path" ]; then - resolved="$(readlink -f "$path" 2>/dev/null)" || return 1 - path_has_clear_stage_component "$resolved" && return 0 - fi - - return 1 -} - -is_supported_upload_path() { - local path="$1" storage - - path="$(normalize_path "$path")" || return 1 - path_uses_clear_stage "$path" && return 1 - [ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0 - - case "$path" in - /mnt/*/gecoosac/upload) - storage="${path#/mnt/}" - storage="${storage%/gecoosac/upload}" - [ -n "$storage" ] && [ "${storage#*/}" = "$storage" ] - ;; - *) return 1 ;; - esac -} - -is_safe_upload_dir() { - local path physical - - path="$(normalize_path "$1")" || return 1 - physical="$(managed_dir_path upload "$path")" || return 1 - is_supported_upload_path "$path" || return 1 - is_supported_upload_path "$physical" -} - -is_path_in_dir() { - local path root - - path="$(normalize_path "$1")" || return 1 - root="$(normalize_path "$2")" || return 1 - - [ "$root" != "/" ] || return 1 - [ "$path" = "$root" ] && return 0 - [ "${path#"$root"/}" != "$path" ] -} - -is_secure_dir() { - local allow_sticky="$2" owner permissions metadata - - [ -d "$1" ] && [ ! -L "$1" ] || return 1 - metadata="$(ls -ldn "$1" 2>/dev/null)" || return 1 - set -- $metadata - permissions="$1" - owner="$3" - [ "$owner" = "0" ] || return 1 - case "$permissions" in - d?????????) ;; - *) return 1 ;; - esac - - if [ "$(printf '%s' "$permissions" | cut -c6)" = "w" ] || \ - [ "$(printf '%s' "$permissions" | cut -c9)" = "w" ]; then - [ "$allow_sticky" = "1" ] && [ "$(printf '%s' "$permissions" | cut -c10)" = "t" ] || return 1 - fi -} - -is_secure_upload_dir() { - local path current part rest - - path="$(normalize_path "$1")" || return 1 - [ -d "$path" ] && [ ! -L "$path" ] || return 1 - current="/" - rest="${path#/}" - - while [ -n "$rest" ]; do - part="${rest%%/*}" - if [ "$part" = "$rest" ]; then - rest="" - else - rest="${rest#*/}" - fi - current="${current%/}/$part" - case "$current" in - /tmp) is_secure_dir "$current" 1 || return 1 ;; - *) is_secure_dir "$current" || return 1 ;; - esac - done -} - -is_safe_db_dir() { - local path upload_root physical physical_upload_root - - path="$(normalize_path "$1")" || return 1 - upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1 - - case "$path" in - /etc/gecoosac|/etc/gecoosac/*|/tmp/gecoosac|/tmp/gecoosac/*|/var/lib/gecoosac|/var/lib/gecoosac/*) ;; - *) return 1 ;; - esac - is_path_in_dir "$path" "$upload_root" && return 1 - physical="$(managed_dir_path db "$path")" || return 1 - physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1 - is_path_in_dir "$physical" "$physical_upload_root" && return 1 - - return 0 -} - -is_safe_pid_dir() { - local path upload_root physical physical_upload_root - - path="$(normalize_path "$1")" || return 1 - upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1 - - case "$path" in - /var/run|/var/run/*|/tmp/gecoosac|/tmp/gecoosac/*) ;; - *) return 1 ;; - esac - is_path_in_dir "$path" "$upload_root" && return 1 - physical="$(managed_dir_path pid "$path")" || return 1 - physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1 - is_path_in_dir "$physical" "$physical_upload_root" && return 1 - - return 0 -} - is_port() { case "$1" in ""|*[!0-9]*) return 1 ;; @@ -242,66 +52,6 @@ is_port() { [ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ] } -run_with_timeout() { - local timeout pid i - - timeout="$1" - shift - - "$@" >/dev/null 2>&1 & - pid="$!" - i=0 - - while kill -0 "$pid" >/dev/null 2>&1; do - if [ "$i" -ge "$timeout" ]; then - kill "$pid" >/dev/null 2>&1 - sleep 1 - kill -9 "$pid" >/dev/null 2>&1 - return 1 - fi - sleep 1 - i=$((i + 1)) - done - - wait "$pid" -} - -run_with_timeout_output() { - local timeout output pid i - - timeout="$1" - output="$2" - shift 2 - - "$@" >"$output" 2>/dev/null & - pid="$!" - i=0 - - while kill -0 "$pid" >/dev/null 2>&1; do - if [ "$i" -ge "$timeout" ]; then - kill "$pid" >/dev/null 2>&1 - kill -9 "$pid" >/dev/null 2>&1 - return 1 - fi - sleep 1 - i=$((i + 1)) - done - - wait "$pid" -} - -is_readable_regular_file() { - local path="$1" tmp_dir status - - tmp_dir="$(mktemp -d /tmp/gecoosac-file.XXXXXX)" || return 1 - run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/check" \ - /bin/sh -c '[ -f "$1" ] && [ -r "$1" ] && [ -s "$1" ]' \ - gecoosac-file-check "$path" /dev/null)" - rm -rf "$tmp_dir" - [ -n "$san" ] || return 1 - san_has_entry "$san" "DNS:${cert_host}" || return 1 - [ -z "$cert_ip" ] || san_has_entry "$san" "IP Address:${cert_ip}" || return 1 - - return 0 + # coreutils-timeout waits for completion directly and also bounds commands + # which ignore SIGTERM; no polling delay or orphaned sleep process is needed. + timeout -k 1 "$limit" "$@" >"$output" 2>/dev/null } -cert_matches_key() { - local cert_file="$1" key_file="$2" - local cert_pub key_pub tmp_dir +run_with_timeout() { + local limit="$1" + shift + run_with_timeout_output "$limit" /dev/null "$@" +} + +cert_pair_valid() { + local cert_file="$1" key_file="$2" cert_host="$3" cert_ip="$4" + local min_lifetime="${5:-0}" tmp_dir cert_pub key_pub san status - is_readable_regular_file "$cert_file" || return 1 - is_readable_regular_file "$key_file" || return 1 tmp_dir="$(mktemp -d /tmp/gecoosac-cert.XXXXXX)" || return 1 - if ! run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/cert.pub" \ - openssl x509 -in "$cert_file" -noout -pubkey /dev/null)" - key_pub="$(cat "$tmp_dir/key.pub" 2>/dev/null)" rm -rf "$tmp_dir" - - [ -n "$cert_pub" ] && [ "$cert_pub" = "$key_pub" ] + return "$status" } generate_default_cert() { @@ -389,7 +146,7 @@ generate_default_cert() { cert_ip="$(uci -q get network.lan.ipaddr)" is_ipv4 "$cert_ip" || cert_ip="" - if is_readable_regular_file "$DEFAULT_KEY_FILE" && cert_matches_san "$DEFAULT_CRT_FILE" "$cert_host" "$cert_ip" && cert_matches_key "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE"; then + if cert_pair_valid "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE" "$cert_host" "$cert_ip" "$CERT_RENEW_BEFORE"; then return 0 fi @@ -423,9 +180,7 @@ generate_default_cert() { return 1 fi - if [ ! -s "$tmp_crt" ] || [ ! -s "$tmp_key" ] || \ - ! cert_matches_san "$tmp_crt" "$cert_host" "$cert_ip" || \ - ! cert_matches_key "$tmp_crt" "$tmp_key" || \ + if ! cert_pair_valid "$tmp_crt" "$tmp_key" "$cert_host" "$cert_ip" "$CERT_RENEW_BEFORE" || \ ! chmod 600 "$tmp_key" || ! chmod 644 "$tmp_crt"; then rm -f "$tmp_crt" "$tmp_key" logger -t gecoosac "failed to validate default HTTPS certificate" @@ -468,10 +223,8 @@ generate_default_cert() { old_key=1 fi - if mv "$tmp_key" "$DEFAULT_KEY_FILE" && \ - mv "$tmp_crt" "$DEFAULT_CRT_FILE" && \ - cert_matches_san "$DEFAULT_CRT_FILE" "$cert_host" "$cert_ip" && \ - cert_matches_key "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE"; then + # Both temporary files were validated above; rename preserves their contents. + if mv "$tmp_key" "$DEFAULT_KEY_FILE" && mv "$tmp_crt" "$DEFAULT_CRT_FILE"; then rm -rf "$backup_dir" return 0 fi @@ -601,31 +354,6 @@ ensure_secure_dir_tree() { done } -managed_dir_path() { - local role="$1" path anchor - - case "$role" in - upload|db|pid|file|tls) ;; - *) return 1 ;; - esac - path="$(normalize_path "$2")" || return 1 - case "$path" in - /var/run|/var/run/*) - anchor="$(readlink -f /var/run 2>/dev/null)" || return 1 - [ "$anchor" = "/tmp/run" ] || return 1 - printf '%s%s\n' "$anchor" "${path#/var/run}" - ;; - /var|/var/*) - anchor="$(readlink -f /var 2>/dev/null)" || return 1 - case "$anchor" in - /var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;; - *) return 1 ;; - esac - ;; - *) printf '%s\n' "$path" ;; - esac -} - ensure_upload_storage_root() { local path="$1" root @@ -677,10 +405,8 @@ prepare_service() { if [ "$isonlyoneprot" = "0" ] && [ "$https" = "1" ]; then if [ "$crt_file" = "$DEFAULT_CRT_FILE" ] && [ "$key_file" = "$DEFAULT_KEY_FILE" ]; then generate_default_cert || return 1 - fi - - if ! cert_matches_key "$crt_file" "$key_file"; then - logger -t gecoosac "HTTPS certificate and key do not match or cannot be read" + elif ! cert_pair_valid "$crt_file" "$key_file"; then + logger -t gecoosac "HTTPS certificate is not currently valid, or its private key cannot be read or does not match" return 1 fi fi diff --git a/gecoosac/files/etc/uci-defaults/gecoosac b/gecoosac/files/etc/uci-defaults/gecoosac index 817ecbefb..587e37288 100644 --- a/gecoosac/files/etc/uci-defaults/gecoosac +++ b/gecoosac/files/etc/uci-defaults/gecoosac @@ -1,12 +1,9 @@ #!/bin/sh +. /usr/share/gecoosac/common.sh + changed=0 -DEFAULT_DB_DIR=/etc/gecoosac -DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload LEGACY_UPLOAD_DIR=/etc/gecoosac/upload -DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt -DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key -DEFAULT_PID_DIR=/var/run OLD_CRT_FILE=/etc/gecoosac/tls/1.crt OLD_KEY_FILE=/etc/gecoosac/tls/1.key CONFIG_COMPAT=2 @@ -26,192 +23,6 @@ set_default() { changed=1 } -normalize_path() { - local path="$1" - local part normalized parent - - is_abs_path "$path" || return 1 - normalized="/" - path="${path#/}" - - while [ -n "$path" ]; do - part="${path%%/*}" - if [ "$part" = "$path" ]; then - path="" - else - path="${path#*/}" - fi - - case "$part" in - ""|.) ;; - ..) - if [ "$normalized" != "/" ]; then - parent="${normalized%/*}" - [ -n "$parent" ] || parent="/" - normalized="$parent" - fi - ;; - *) normalized="${normalized%/}/$part" ;; - esac - done - - printf '%s\n' "$normalized" -} - -is_abs_path() { - case "$1" in - /*) return 0 ;; - *) return 1 ;; - esac -} - -managed_dir_path() { - local role="$1" path anchor - - case "$role" in - upload|db|pid|file) ;; - *) return 1 ;; - esac - path="$(normalize_path "$2")" || return 1 - case "$path" in - /var/run|/var/run/*) - anchor="$(readlink -f /var/run 2>/dev/null)" || return 1 - [ "$anchor" = "/tmp/run" ] || return 1 - printf '%s%s\n' "$anchor" "${path#/var/run}" - ;; - /var|/var/*) - anchor="$(readlink -f /var 2>/dev/null)" || return 1 - case "$anchor" in - /var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;; - *) return 1 ;; - esac - ;; - *) printf '%s\n' "$path" ;; - esac -} - -path_has_clear_stage_component() { - local path="$1" part rest - - path="$(normalize_path "$path")" || return 1 - rest="${path#/}" - while [ -n "$rest" ]; do - part="${rest%%/*}" - if [ "$part" = "$rest" ]; then - rest="" - else - rest="${rest#*/}" - fi - case "$part" in - .gecoosac-clear.*) return 0 ;; - esac - done - - return 1 -} - -path_uses_clear_stage() { - local path="$1" resolved - - path_has_clear_stage_component "$path" && return 0 - if [ -e "$path" ] || [ -L "$path" ]; then - resolved="$(readlink -f "$path" 2>/dev/null)" || return 1 - path_has_clear_stage_component "$resolved" && return 0 - fi - - return 1 -} - -is_supported_upload_path() { - local path="$1" storage - - path="$(normalize_path "$path")" || return 1 - path_uses_clear_stage "$path" && return 1 - [ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0 - - case "$path" in - /mnt/*/gecoosac/upload) - storage="${path#/mnt/}" - storage="${storage%/gecoosac/upload}" - [ -n "$storage" ] && [ "${storage#*/}" = "$storage" ] - ;; - *) return 1 ;; - esac -} - -is_safe_upload_dir() { - local path physical - - path="$(normalize_path "$1")" || return 1 - physical="$(managed_dir_path upload "$path")" || return 1 - is_supported_upload_path "$path" || return 1 - is_supported_upload_path "$physical" -} - -is_path_in_dir() { - local path root - - path="$(normalize_path "$1")" || return 1 - root="$(normalize_path "$2")" || return 1 - - [ "$root" != "/" ] || return 1 - [ "$path" = "$root" ] && return 0 - [ "${path#"$root"/}" != "$path" ] -} - -path_has_mount() { - local root line mount_path - - root="$(normalize_path "$1")" || return 2 - [ -r /proc/self/mountinfo ] || return 2 - - while IFS= read -r line; do - mount_path="$(printf '%s\n' "$line" | cut -d ' ' -f 5)" || return 2 - [ -n "$mount_path" ] || return 2 - mount_path="$(printf '%b\n' "$mount_path" 2>/dev/null)" || return 2 - mount_path="$(normalize_path "$mount_path")" || return 2 - is_path_in_dir "$mount_path" "$root" && return 0 - done < /proc/self/mountinfo - - return 1 -} - -is_safe_db_dir() { - local path upload_root physical physical_upload_root - - path="$(normalize_path "$1")" || return 1 - upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1 - - case "$path" in - /etc/gecoosac|/etc/gecoosac/*|/tmp/gecoosac|/tmp/gecoosac/*|/var/lib/gecoosac|/var/lib/gecoosac/*) ;; - *) return 1 ;; - esac - is_path_in_dir "$path" "$upload_root" && return 1 - physical="$(managed_dir_path db "$path")" || return 1 - physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1 - is_path_in_dir "$physical" "$physical_upload_root" && return 1 - - return 0 -} - -is_safe_pid_dir() { - local path upload_root physical physical_upload_root - - path="$(normalize_path "$1")" || return 1 - upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1 - - case "$path" in - /var/run|/var/run/*|/tmp/gecoosac|/tmp/gecoosac/*) ;; - *) return 1 ;; - esac - is_path_in_dir "$path" "$upload_root" && return 1 - physical="$(managed_dir_path pid "$path")" || return 1 - physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1 - is_path_in_dir "$physical" "$physical_upload_root" && return 1 - - return 0 -} - normalize_upload_dir() { local upload_dir normalized @@ -229,38 +40,45 @@ normalize_upload_dir() { } configured_path_in_legacy_upload() { - local option path resolved + local physical="$1" option role path for option in db_dir piddir crt_file key_file; do path="$(uci -q get "gecoosac.config.${option}")" [ -n "$path" ] || continue - path="$(normalize_path "$path")" || return 0 - is_path_in_dir "$path" "$LEGACY_UPLOAD_DIR" && return 0 - - if [ -e "$path" ] || [ -L "$path" ]; then - resolved="$(readlink -f "$path" 2>/dev/null)" || return 0 - resolved="$(normalize_path "$resolved")" || return 0 - is_path_in_dir "$resolved" "$LEGACY_UPLOAD_DIR" && return 0 - fi + case "$option" in + db_dir) role=db ;; + piddir) role=pid ;; + *) role="file" ;; + esac + protected_path_in_dir "$path" "$role" "$physical" "$LEGACY_UPLOAD_DIR" "$physical" + case "$?" in + 1) ;; + *) return 0 ;; + esac done return 1 } cleanup_legacy_upload_dir() { - local mount_state + local physical mount_state [ -e "$LEGACY_UPLOAD_DIR" ] || [ -L "$LEGACY_UPLOAD_DIR" ] || return 0 if [ ! -d "$LEGACY_UPLOAD_DIR" ] || [ -L "$LEGACY_UPLOAD_DIR" ]; then logger -t gecoosac "refusing to remove unsafe legacy upload path: $LEGACY_UPLOAD_DIR" return 0 fi - if configured_path_in_legacy_upload; then + physical="$(readlink -f "$LEGACY_UPLOAD_DIR" 2>/dev/null)" || { + logger -t gecoosac "unable to resolve legacy upload path: $LEGACY_UPLOAD_DIR" + return 0 + } + physical="$(normalize_path "$physical")" || return 0 + if configured_path_in_legacy_upload "$physical"; then logger -t gecoosac "preserving legacy upload path referenced by configuration: $LEGACY_UPLOAD_DIR" return 0 fi - path_has_mount "$LEGACY_UPLOAD_DIR" + path_has_mount "$physical" mount_state="$?" case "$mount_state" in 0) @@ -273,7 +91,13 @@ cleanup_legacy_upload_dir() { ;; esac - rm -rf "$LEGACY_UPLOAD_DIR" || { + # Apply the same ownership and ancestor-symlink policy as RPC cleanup. + # Relocated legacy directories are preserved for manual review. + if ! is_secure_upload_dir "$LEGACY_UPLOAD_DIR" || [ "$physical" != "$LEGACY_UPLOAD_DIR" ]; then + logger -t gecoosac "refusing to remove unsafe legacy upload path: $LEGACY_UPLOAD_DIR" + return 0 + fi + rm -rf "$physical" || { logger -t gecoosac "failed to remove legacy upload path: $LEGACY_UPLOAD_DIR" return 1 } @@ -310,7 +134,7 @@ is_managed_cert_path() { is_regular_file_or_absent() { [ ! -e "$1" ] && [ ! -L "$1" ] && return 0 - [ -f "$1" ] && [ ! -L "$1" ] + [ -f "$1" ] && [ ! -L "$1" ] && is_secure_upload_dir "${1%/*}" } migrate_default_certificates() { diff --git a/gecoosac/files/usr/share/gecoosac/common.sh b/gecoosac/files/usr/share/gecoosac/common.sh new file mode 100644 index 000000000..4e2f82892 --- /dev/null +++ b/gecoosac/files/usr/share/gecoosac/common.sh @@ -0,0 +1,282 @@ +# Shared path policy for the service, configuration migration and LuCI RPC. + +DEFAULT_DB_DIR=/etc/gecoosac +DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload +DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt +DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key +DEFAULT_PID_DIR=/var/run + +is_abs_path() { + case "$1" in + /*) return 0 ;; + *) return 1 ;; + esac +} + +normalize_path() { + local path="$1" + local part normalized parent + + is_abs_path "$path" || return 1 + normalized="/" + path="${path#/}" + + while [ -n "$path" ]; do + part="${path%%/*}" + if [ "$part" = "$path" ]; then + path="" + else + path="${path#*/}" + fi + + case "$part" in + ""|.) ;; + ..) + if [ "$normalized" != "/" ]; then + parent="${normalized%/*}" + [ -n "$parent" ] || parent="/" + normalized="$parent" + fi + ;; + *) normalized="${normalized%/}/$part" ;; + esac + done + + printf '%s\n' "$normalized" +} + +managed_dir_path() { + local role="$1" path anchor + + case "$role" in + upload|db|pid|file|tls) ;; + *) return 1 ;; + esac + path="$(normalize_path "$2")" || return 1 + case "$path" in + /var/run|/var/run/*) + anchor="$(readlink -f /var/run 2>/dev/null)" || return 1 + [ "$anchor" = "/tmp/run" ] || return 1 + printf '%s%s\n' "$anchor" "${path#/var/run}" + ;; + /var|/var/*) + anchor="$(readlink -f /var 2>/dev/null)" || return 1 + case "$anchor" in + /var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;; + *) return 1 ;; + esac + ;; + *) printf '%s\n' "$path" ;; + esac +} + +path_has_clear_stage_component() { + local path="$1" part rest + + path="$(normalize_path "$path")" || return 1 + rest="${path#/}" + while [ -n "$rest" ]; do + part="${rest%%/*}" + if [ "$part" = "$rest" ]; then + rest="" + else + rest="${rest#*/}" + fi + case "$part" in + .gecoosac-clear.*) return 0 ;; + esac + done + + return 1 +} + +path_uses_clear_stage() { + local path="$1" resolved + + path_has_clear_stage_component "$path" && return 0 + if [ -e "$path" ] || [ -L "$path" ]; then + resolved="$(readlink -f "$path" 2>/dev/null)" || return 1 + path_has_clear_stage_component "$resolved" && return 0 + fi + + return 1 +} + +is_supported_upload_path() { + local path="$1" storage + + path="$(normalize_path "$path")" || return 1 + path_uses_clear_stage "$path" && return 1 + [ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0 + + case "$path" in + /mnt/*/gecoosac/upload) + storage="${path#/mnt/}" + storage="${storage%/gecoosac/upload}" + [ -n "$storage" ] && [ "${storage#*/}" = "$storage" ] + ;; + *) return 1 ;; + esac +} + +is_safe_upload_dir() { + local path physical + + path="$(normalize_path "$1")" || return 1 + physical="$(managed_dir_path upload "$path")" || return 1 + is_supported_upload_path "$path" || return 1 + is_supported_upload_path "$physical" +} + +is_path_in_dir() { + local path root + + path="$(normalize_path "$1")" || return 1 + root="$(normalize_path "$2")" || return 1 + + [ "$root" != "/" ] || return 1 + [ "$path" = "$root" ] && return 0 + [ "${path#"$root"/}" != "$path" ] +} + +is_secure_dir() { + local allow_sticky="$2" owner permissions metadata + + [ -d "$1" ] && [ ! -L "$1" ] || return 1 + metadata="$(ls -ldn "$1" 2>/dev/null)" || return 1 + set -- $metadata + permissions="$1" + owner="$3" + [ "$owner" = "0" ] || return 1 + case "$permissions" in + d?????????) ;; + *) return 1 ;; + esac + + if [ "$(printf '%s' "$permissions" | cut -c6)" = "w" ] || \ + [ "$(printf '%s' "$permissions" | cut -c9)" = "w" ]; then + [ "$allow_sticky" = "1" ] && [ "$(printf '%s' "$permissions" | cut -c10)" = "t" ] || return 1 + fi +} + +is_secure_upload_dir() { + local path current part rest + + path="$(normalize_path "$1")" || return 1 + [ -d "$path" ] && [ ! -L "$path" ] || return 1 + current="/" + rest="${path#/}" + + while [ -n "$rest" ]; do + part="${rest%%/*}" + if [ "$part" = "$rest" ]; then + rest="" + else + rest="${rest#*/}" + fi + current="${current%/}/$part" + case "$current" in + /tmp) is_secure_dir "$current" 1 || return 1 ;; + *) is_secure_dir "$current" || return 1 ;; + esac + done +} + +is_safe_db_dir() { + local path upload_root physical physical_upload_root + + path="$(normalize_path "$1")" || return 1 + upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1 + + case "$path" in + /etc/gecoosac|/etc/gecoosac/*|/tmp/gecoosac|/tmp/gecoosac/*|/var/lib/gecoosac|/var/lib/gecoosac/*) ;; + *) return 1 ;; + esac + is_path_in_dir "$path" "$upload_root" && return 1 + physical="$(managed_dir_path db "$path")" || return 1 + physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1 + is_path_in_dir "$physical" "$physical_upload_root" && return 1 + + return 0 +} + +is_safe_pid_dir() { + local path upload_root physical physical_upload_root + + path="$(normalize_path "$1")" || return 1 + upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1 + + case "$path" in + /var/run|/var/run/*|/tmp/gecoosac|/tmp/gecoosac/*) ;; + *) return 1 ;; + esac + is_path_in_dir "$path" "$upload_root" && return 1 + physical="$(managed_dir_path pid "$path")" || return 1 + physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1 + is_path_in_dir "$physical" "$physical_upload_root" && return 1 + + return 0 +} + +path_has_mount() { + local root mount_id parent_id device mount_root mount_path rest + + root="$(normalize_path "$1")" || return 2 + [ -r /proc/self/mountinfo ] || return 2 + + while IFS=' ' read -r mount_id parent_id device mount_root mount_path rest; do + [ -n "$mount_path" ] && [ -n "$rest" ] || return 2 + mount_path="$(printf '%b\n' "$mount_path" 2>/dev/null)" || return 2 + mount_path="$(normalize_path "$mount_path")" || return 2 + is_path_in_dir "$mount_path" "$root" && return 0 + done < /proc/self/mountinfo + + return 1 +} + +# The checked root must be a resolved, validated directory. Return 0 for a +# protected path, 1 for an unrelated path, or 2 when validation is impossible. +# Keep the original spelling when resolving symlinks: link/../file and its +# lexical normalization can refer to different files. +protected_path_in_dir() { + local raw_path="$1" role="$2" checked_root="$3" + local live_logical="${4:-$3}" live_physical="${5:-$3}" + local path="$1" physical real_path suffix mapped + + [ -n "$path" ] || return 1 + + path="$(normalize_path "$path")" || return 2 + physical="$(managed_dir_path "$role" "$path")" || return 2 + checked_root="$(normalize_path "$checked_root")" || return 2 + live_logical="$(normalize_path "$live_logical")" || return 2 + live_physical="$(normalize_path "$live_physical")" || return 2 + + is_path_in_dir "$path" "$checked_root" && return 0 + is_path_in_dir "$physical" "$checked_root" && return 0 + real_path="$(readlink -f "$raw_path" 2>/dev/null)" || return 2 + [ -n "$real_path" ] || return 2 + real_path="$(normalize_path "$real_path")" || return 2 + is_path_in_dir "$real_path" "$checked_root" && return 0 + + [ "$checked_root" != "$live_physical" ] || { + is_path_in_dir "$path" "$live_logical" && return 0 + is_path_in_dir "$physical" "$live_physical" && return 0 + if [ -n "$real_path" ] && is_path_in_dir "$real_path" "$live_physical"; then + return 0 + fi + return 1 + } + + suffix= + if is_path_in_dir "$path" "$live_logical"; then + suffix="${path#"$live_logical"}" + elif is_path_in_dir "$physical" "$live_physical"; then + suffix="${physical#"$live_physical"}" + elif [ -n "$real_path" ] && is_path_in_dir "$real_path" "$live_physical"; then + suffix="${real_path#"$live_physical"}" + else + return 1 + fi + mapped="${checked_root%/}${suffix}" + [ -e "$mapped" ] || [ -L "$mapped" ] +} diff --git a/luci-app-fwlive/Makefile b/luci-app-fwlive/Makefile index 570a2569d..dbd3e0f36 100644 --- a/luci-app-fwlive/Makefile +++ b/luci-app-fwlive/Makefile @@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter LUCI_PKGARCH:=all PKG_VERSION:=0.1.41 -PKG_RELEASE:=13 +PKG_RELEASE:=14 # Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI). ifdef SOURCE_DATE_EPOCH diff --git a/luci-app-fwlive/root/usr/libexec/fwlive-adaptive-cap.sh b/luci-app-fwlive/root/usr/libexec/fwlive-adaptive-cap.sh new file mode 100755 index 000000000..9e78c8be2 --- /dev/null +++ b/luci-app-fwlive/root/usr/libexec/fwlive-adaptive-cap.sh @@ -0,0 +1,443 @@ +#!/bin/sh +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2025-2026 Lucas Albers +# +# Layer 1 adaptive poll cap (#306). Sourced by rpcd/fwlive. +# Always on unless test/triage override (no UCI / no product config): +# FWLIVE_ADAPTIVE=0|false|off|no +# or sentinel ${FWLIVE_ADAPTIVE_OFF_FILE:-/fwlive-adaptive-off} +# (default under /var/run next to state — not world-writable /tmp). +# +# State: ${FWLIVE_ADAPTIVE_STATE_FILE:-/var/run/fwlive-state.json} +# Lock: sibling ${FWLIVE_ADAPTIVE_LOCK_FILE:-$STATE.lock} — never the JSON +# inode (atomic mv replaces that inode; locking it would not serialize writers). +# Lock covers only short update — never held across ubus/filter work. +# Hot-path budget: ≤1 flock exec per update (release by closing the fd when the +# subshell exits — no flock -u). /proc/uptime + state I/O via shell builtins/ +# redirects (no sed/cat/jsonfilter on the adaptive path). Fail-open on missing +# flock, lock busy, or corrupt state. Lock-busy ⇒ unlocked last-writer-wins is +# acceptable (state stays one valid JSON line; ordering is not guaranteed). +# Failed ubus log.read must NOT call record() — a ~0 ms failure is not "cold" +# health and must not clear an existing hot/shed cap (#329 Hermes Q1). +# Outside the measured duration interval: plan (pre), record/merge (post). +# messages_received is 0 in Layer 1 — do not ash-scan the filter JSON after +# end_cs (Grok #329 P1); accurate count belongs in the filter or a later layer. + +FWLIVE_ADAPTIVE_STATE_FILE="${FWLIVE_ADAPTIVE_STATE_FILE:-/var/run/fwlive-state.json}" +# Optional overrides; when unset, lock/off paths are siblings of the current state. +# Do not bake STATE into LOCK/OFF at source time — tests may relocate STATE after source. + +# Bucket thresholds (processing duration, ms). +FWLIVE_ADAPTIVE_COLD_MS=100 +FWLIVE_ADAPTIVE_COOL_MS=200 +FWLIVE_ADAPTIVE_WARM_MS=800 +FWLIVE_ADAPTIVE_HOT_EXIT_MS=500 +FWLIVE_ADAPTIVE_COOL_CAP=250 +FWLIVE_ADAPTIVE_HOT_FLOOR=250 +FWLIVE_ADAPTIVE_WARM_MIN=50 +FWLIVE_ADAPTIVE_COOLDOWN_WARM_CS=300 +FWLIVE_ADAPTIVE_COOLDOWN_HOT_CS=600 + +fwlive_adaptive_lock_path() { + if [ -n "${FWLIVE_ADAPTIVE_LOCK_FILE:-}" ]; then + printf '%s\n' "$FWLIVE_ADAPTIVE_LOCK_FILE" + else + printf '%s\n' "${FWLIVE_ADAPTIVE_STATE_FILE}.lock" + fi +} + +fwlive_adaptive_off_path() { + if [ -n "${FWLIVE_ADAPTIVE_OFF_FILE:-}" ]; then + printf '%s\n' "$FWLIVE_ADAPTIVE_OFF_FILE" + else + _dir=${FWLIVE_ADAPTIVE_STATE_FILE%/*} + [ "$_dir" = "$FWLIVE_ADAPTIVE_STATE_FILE" ] && _dir=/var/run + printf '%s\n' "$_dir/fwlive-adaptive-off" + fi +} + +fwlive_adaptive_enabled() { + case "${FWLIVE_ADAPTIVE:-1}" in + 0|false|off|no|FALSE|OFF|NO) return 1 ;; + esac + [ -e "$(fwlive_adaptive_off_path)" ] && return 1 + return 0 +} + +fwlive_adaptive_atoi() { + # Decimal int from digit string — avoids $((08)) octal errors under ash. + _s=$1 + _n=0 + case "$_s" in ''|*[!0-9]*) printf '0\n'; return 0 ;; esac + while [ -n "$_s" ]; do + _d=${_s%"${_s#?}"} + _s=${_s#?} + _n=$((_n * 10 + _d)) + done + printf '%s\n' "$_n" +} + +fwlive_adaptive_clock_cs() { + # /proc/uptime centiseconds — BusyBox date lacks %N. Builtin read only. + _up= + read -r _up _ /dev/null || { + printf '%s\n' 0 + return 0 + } + _sec=${_up%.*} + _frac=${_up#*.} + [ "$_frac" = "$_up" ] && _frac=0 + # At most two decimal digits (centiseconds), no cut(1). + case "$_frac" in + '') _frac=0 ;; + ?) _frac="${_frac}0" ;; + ??) ;; + *) + _a=${_frac%${_frac#?}} + _r=${_frac#?} + _b=${_r%${_r#?}} + _frac="${_a}${_b}" + ;; + esac + _sec=$(fwlive_adaptive_atoi "$_sec") + _frac=$(fwlive_adaptive_atoi "$_frac") + printf '%s\n' $((_sec * 100 + _frac)) +} + +fwlive_adaptive_state_dir_ok() { + _path=$FWLIVE_ADAPTIVE_STATE_FILE + _dir=${_path%/*} + [ -n "$_dir" ] || return 1 + [ "$_dir" = "$_path" ] && return 1 + [ -L "$_dir" ] && return 1 + [ -d "$_dir" ] || return 1 + # shellcheck disable=SC3067 + [ -O "$_dir" ] || return 1 + return 0 +} + +# Extract a decimal integer field from a one-line JSON object (builtin only). +# Prints default ($2) when missing/corrupt. +fwlive_adaptive_json_int() { + _blob=$1 + _key=$2 + _def=$3 + case "$_blob" in + *"\"$_key\""*) ;; + *) printf '%s\n' "$_def"; return 0 ;; + esac + _rest=${_blob#*"\"$_key\""} + _rest=${_rest#*:} + while case "$_rest" in ' '*) true;; *) false;; esac; do + _rest=${_rest# } + done + _num= + while :; do + case "$_rest" in + '') break ;; + esac + _c=${_rest%"${_rest#?}"} + case "$_c" in + [0-9]) _num="${_num}${_c}"; _rest=${_rest#?} ;; + *) break ;; + esac + done + case "$_num" in + ''|*[!0-9]*) printf '%s\n' "$_def" ;; + *) printf '%s\n' "$_num" ;; + esac +} + +# Extract a lowercase alpha string field. +fwlive_adaptive_json_str() { + _blob=$1 + _key=$2 + _def=$3 + case "$_blob" in + *"\"$_key\""*) ;; + *) printf '%s\n' "$_def"; return 0 ;; + esac + _rest=${_blob#*"\"$_key\""} + _rest=${_rest#*:} + while case "$_rest" in ' '*) true;; *) false;; esac; do + _rest=${_rest# } + done + case "$_rest" in + \"*) _rest=${_rest#\"}; _val=${_rest%%\"*} ;; + *) printf '%s\n' "$_def"; return 0 ;; + esac + case "$_val" in + *[!a-z]*|'') printf '%s\n' "$_def" ;; + *) printf '%s\n' "$_val" ;; + esac +} + +# Print: duration_ms limit bucket warm_halved shed completed_cs +fwlive_adaptive_read_state() { + _d=0 + _l="${POLL_LINES_MAX:-2000}" + _b=cold + _w=0 + _s=0 + _c=0 + _raw= + if [ -f "$FWLIVE_ADAPTIVE_STATE_FILE" ]; then + IFS= read -r _raw <"$FWLIVE_ADAPTIVE_STATE_FILE" || _raw= + fi + if [ -n "$_raw" ]; then + _d=$(fwlive_adaptive_json_int "$_raw" duration_ms 0) + _l=$(fwlive_adaptive_json_int "$_raw" limit "${POLL_LINES_MAX:-2000}") + _b=$(fwlive_adaptive_json_str "$_raw" bucket cold) + _w=$(fwlive_adaptive_json_int "$_raw" warm_halved 0) + _s=$(fwlive_adaptive_json_int "$_raw" shed 0) + _c=$(fwlive_adaptive_json_int "$_raw" completed_cs 0) + case "$_w" in 0|1) ;; *) _w=0 ;; esac + case "$_s" in 0|1) ;; *) _s=0 ;; esac + fi + printf '%s %s %s %s %s %s\n' "$_d" "$_l" "$_b" "$_w" "$_s" "$_c" +} + +fwlive_adaptive_write_state() { + _d=$1 + _l=$2 + _b=$3 + _w=$4 + _s=$5 + _c=$6 + fwlive_adaptive_state_dir_ok || return 0 + # Refuse to follow a symlinked state path (same discipline as logging.lock). + [ -L "$FWLIVE_ADAPTIVE_STATE_FILE" ] && return 0 + _tmp="${FWLIVE_ADAPTIVE_STATE_FILE}.tmp.$$" + umask 077 + printf '{"duration_ms":%s,"limit":%s,"bucket":"%s","warm_halved":%s,"shed":%s,"completed_cs":%s}\n' \ + "$_d" "$_l" "$_b" "$_w" "$_s" "$_c" >"$_tmp" 2>/dev/null || { + rm -f "$_tmp" + return 0 + } + mv -f "$_tmp" "$FWLIVE_ADAPTIVE_STATE_FILE" 2>/dev/null || rm -f "$_tmp" + return 0 +} + +# One non-blocking flock on the persistent sibling lock file; fail-open if +# missing/busy. Release by exiting the subshell (closes fd 9) — no flock -u. +# Busy ⇒ run unlocked: last-writer-wins is acceptable (valid one-line JSON; +# ordering under contention is not guaranteed). +# Lock-open failure (redirection) also fail-opens unlocked so record() still runs. +fwlive_adaptive_with_lock() { + if ! command -v flock >/dev/null 2>&1; then + "$@" + return $? + fi + fwlive_adaptive_state_dir_ok || { + "$@" + return $? + } + _lock=$(fwlive_adaptive_lock_path) + # Symlinked or non-file lock path: do not create/follow; fail open unlocked. + if [ -L "$_lock" ] || [ -d "$_lock" ]; then + "$@" + return $? + fi + # Create lock at 0600 (logging.lock #167 — world-readable fd can take LOCK_EX). + if [ ! -e "$_lock" ]; then + if ! ( umask 077; : >"$_lock" ) 2>/dev/null; then + "$@" + return $? + fi + elif [ ! -w "$_lock" ]; then + "$@" + return $? + fi + ( + if flock -n 9; then + "$@" + exit $? + fi + # Lock busy — fail open (unlocked). + "$@" + ) 9>>"$_lock" + return $? +} + +fwlive_adaptive_bucket_for_ms() { + _ms=$1 + case "$_ms" in ''|*[!0-9]*) _ms=0 ;; esac + if [ "$_ms" -lt "$FWLIVE_ADAPTIVE_COLD_MS" ]; then + printf '%s\n' cold + elif [ "$_ms" -lt "$FWLIVE_ADAPTIVE_COOL_MS" ]; then + printf '%s\n' cool + elif [ "$_ms" -le "$FWLIVE_ADAPTIVE_WARM_MS" ]; then + printf '%s\n' warm + else + printf '%s\n' hot + fi +} + +# Args: requested_limit duration_ms prev_limit prev_bucket prev_warm prev_completed_cs +# Prints next limit. +fwlive_adaptive_compute_limit() { + _req=$1 + _ms=$2 + _prev_l=$3 + _prev_b=$4 + _prev_w=$5 + _prev_c=$6 + _max="${POLL_LINES_MAX:-2000}" + _now=$(fwlive_adaptive_clock_cs) + _bucket=$(fwlive_adaptive_bucket_for_ms "$_ms") + _limit=$_max + _cd=0 + + case "$_prev_b" in + hot) _cd=$FWLIVE_ADAPTIVE_COOLDOWN_HOT_CS ;; + warm) _cd=$FWLIVE_ADAPTIVE_COOLDOWN_WARM_CS ;; + esac + + if [ "$_cd" -gt 0 ] && [ "$_prev_c" -gt 0 ] && \ + [ "$((_now - _prev_c))" -lt "$_cd" ]; then + if [ "$_ms" -le "$FWLIVE_ADAPTIVE_HOT_EXIT_MS" ] && \ + [ "$_bucket" != hot ]; then + : + else + _limit=$_prev_l + [ "$_limit" -gt "$_req" ] && _limit=$_req + [ "$_limit" -gt "$_max" ] && _limit=$_max + [ "$_limit" -lt 1 ] && _limit=1 + printf '%s\n' "$_limit" + return 0 + fi + fi + + case "$_bucket" in + cold) _limit=$_max ;; + cool) _limit=$FWLIVE_ADAPTIVE_COOL_CAP ;; + warm) + if [ "$_prev_w" = 1 ] && [ "$_prev_b" = warm ]; then + _limit=$_prev_l + else + _base=$_prev_l + [ "$_base" -lt 1 ] && _base=$_max + _limit=$((_base / 2)) + [ "$_limit" -lt "$FWLIVE_ADAPTIVE_WARM_MIN" ] && \ + _limit=$FWLIVE_ADAPTIVE_WARM_MIN + fi + ;; + hot) _limit=$FWLIVE_ADAPTIVE_HOT_FLOOR ;; + esac + + [ "$_limit" -gt "$_req" ] && _limit=$_req + [ "$_limit" -gt "$_max" ] && _limit=$_max + [ "$_limit" -lt 1 ] && _limit=1 + printf '%s\n' "$_limit" +} + +# Apply cap using *previous* poll duration. Prints: lines shed_flag bucket +# Unlocked read (stale OK) — flock reserved for record(). +fwlive_adaptive_plan() { + _req=$1 + _max="${POLL_LINES_MAX:-2000}" + if ! fwlive_adaptive_enabled; then + _l=$_req + [ "$_l" -gt "$_max" ] && _l=$_max + printf '%s 0 off\n' "$_l" + return 0 + fi + # shellcheck disable=SC2046 + set -- $(fwlive_adaptive_read_state) + _d=$1 + _prev_l=$2 + _b=$3 + _prev_w=$4 + _s=$5 + _prev_c=$6 + _l=$(fwlive_adaptive_compute_limit "$_req" "$_d" "$_prev_l" "$_b" "$_prev_w" "$_prev_c") + _nb=$(fwlive_adaptive_bucket_for_ms "$_d") + _shed=0 + [ "$_nb" = hot ] && _shed=1 + [ "$_s" = 1 ] && [ "$_b" = hot ] && _shed=1 + printf '%s %s %s\n' "$_l" "$_shed" "$_nb" +} + +fwlive_adaptive__record_body() { + _ms=$1 + _served=$2 + # shellcheck disable=SC2046 + set -- $(fwlive_adaptive_read_state) + _prev_l=$2 + _prev_b=$3 + _prev_w=$4 + _prev_c=$6 + _bucket=$(fwlive_adaptive_bucket_for_ms "$_ms") + _now=$(fwlive_adaptive_clock_cs) + _limit=$(fwlive_adaptive_compute_limit "$_served" "$_ms" "$_prev_l" "$_prev_b" "$_prev_w" "$_prev_c") + _warm=0 + _shed=0 + case "$_bucket" in + warm) _warm=1 ;; + hot) _shed=1 ;; + esac + fwlive_adaptive_write_state "$_ms" "$_limit" "$_bucket" "$_warm" "$_shed" "$_now" +} + +fwlive_adaptive_record() { + _ms=$1 + _served=$2 + fwlive_adaptive_enabled || return 0 + fwlive_adaptive_with_lock fwlive_adaptive__record_body "$_ms" "$_served" +} + +fwlive_adaptive_is_hot() { + fwlive_adaptive_enabled || return 1 + # shellcheck disable=SC2046 + set -- $(fwlive_adaptive_read_state) + [ "$5" = 1 ] && return 0 + [ "$3" = hot ] && return 0 + return 1 +} + +# Merge adaptive siblings into a JSON object ending with }. +# Args: json_body shed_flag limit truncated messages_received +fwlive_adaptive_merge_reply() { + _body=$1 + _shed=$2 + _limit=$3 + _trunc=$4 + _msgs=$5 + _adapt=1 + fwlive_adaptive_enabled || _adapt=0 + case "$_body" in + *\}) ;; + *) printf '%s' "$_body"; return 0 ;; + esac + _base=${_body%\}} + if [ "$_adapt" = 0 ]; then + printf '%s,"adaptive":0,"messages_received":%s}' "$_base" "$_msgs" + return 0 + fi + if [ "$_shed" = 1 ]; then + printf '%s,"adaptive":1,"messages_received":%s,"truncated":%s,"shed":{"level":"hot","limit":%s}}' \ + "$_base" "$_msgs" "$_trunc" "$_limit" + else + printf '%s,"adaptive":1,"messages_received":%s,"truncated":%s}' \ + "$_base" "$_msgs" "$_trunc" + fi +} + +# Count log[*] via "msg" keys — helper for tests / future filter-side count. +# NOT called on the poll hot path (Grok #329 P1): ash-scanning a 2000-entry +# reply after end_cs is unmeasured overhead that cannot shed itself. +fwlive_adaptive_count_log() { + _json=$1 + _n=0 + _rest=$_json + while :; do + case "$_rest" in + *'"msg"'*) + _n=$((_n + 1)) + _rest=${_rest#*\"msg\"} + ;; + *) break ;; + esac + done + printf '%s\n' "$_n" +} diff --git a/luci-app-fwlive/root/usr/libexec/rpcd/fwlive b/luci-app-fwlive/root/usr/libexec/rpcd/fwlive index 85a7fe9eb..09724e264 100755 --- a/luci-app-fwlive/root/usr/libexec/rpcd/fwlive +++ b/luci-app-fwlive/root/usr/libexec/rpcd/fwlive @@ -18,6 +18,7 @@ FW4_TAG='!fw4: ' LIBEXEC_DIR="$(cd "$(dirname "$0")/.." && pwd)" FILTER_SH="$LIBEXEC_DIR/fwlive-log-filter.sh" LOGGING_SH="$LIBEXEC_DIR/fwlive-logging.sh" +ADAPTIVE_SH="$LIBEXEC_DIR/fwlive-adaptive-cap.sh" # Max reverse-DNS lookups per resolve call (~32 names keeps nslookup load bounded on small routers). RESOLVE_MAX=32 # Per-lookup nslookup timeout (integer seconds — BusyBox `timeout` truncates @@ -44,6 +45,8 @@ RULESMAP_IPTABLES_FILE='/tmp/rulesmap' # json_escape lives in fwlive-logging.sh so prerm can source that file alone (#222). # shellcheck disable=SC1090,SC1091 # LOGGING_SH resolves to sibling ../fwlive-logging.sh . "$LOGGING_SH" +# shellcheck disable=SC1090,SC1091 +. "$ADAPTIVE_SH" slug_key() { echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' _' '--' @@ -425,12 +428,19 @@ poll_lines_from_input() { fetch_firewall_logs() { input="$1" - lines=$(poll_lines_from_input "$input") - # lines is numeric-clamped; stream/oneshot are fixed (not caller-controlled). - # A dead logd must not look like an empty firewall table (#303): return a - # structured error instead of filtering an empty fallback into {"log":[]}. - if ! raw=$(ubus call log read "{\"lines\":$lines,\"stream\":false,\"oneshot\":true}" 2>/dev/null); then - printf '%s' '{"log":[],"error":"log_read_failed"}' + requested=$(poll_lines_from_input "$input") + # shellcheck disable=SC2046 + set -- $(fwlive_adaptive_plan "$requested") + lines=$1 + shed_flag=${2:-0} + # Adaptive plan prints: lines shed bucket + start_cs=$(fwlive_adaptive_clock_cs) + # Drop dead oneshot flag — only consumed on logd's stream branch (#306). + if ! raw=$(ubus call log read "{\"lines\":$lines,\"stream\":false}" 2>/dev/null); then + # Do not record ~0 ms as cold: failure is not healthy fast processing + # and must not clear an existing hot/shed cap (#329 Hermes Q1). + out='{"log":[],"error":"log_read_failed"}' + fwlive_adaptive_merge_reply "$out" "$shed_flag" "$lines" 0 0 return 0 fi @@ -444,7 +454,17 @@ fetch_firewall_logs() { fi fi [ -n "$out" ] || out='{"log":[],"error":"filter_empty"}' - printf '%s' "$out" + end_cs=$(fwlive_adaptive_clock_cs) + # Uptime wrap / skew: end < start — do not record as cold (Grok #329 P2). + if [ "$end_cs" -ge "$start_cs" ]; then + _dur=$(((end_cs - start_cs) * 10)) + fwlive_adaptive_record "$_dur" "$lines" + fi + # Layer 1: messages_received stays 0 — no post-filter ash count (Grok #329 P1). + _trunc=0 + [ "$shed_flag" = 1 ] && _trunc=1 + [ "$lines" -lt "$requested" ] && _trunc=1 + fwlive_adaptive_merge_reply "$out" "$shed_flag" "$lines" "$_trunc" 0 } poll_logs() { @@ -584,6 +604,12 @@ resolve_addresses() { return 0 fi + # Load shed: previous poll was hot (#306 Layer 1). + if fwlive_adaptive_is_hot; then + printf '{"names":{},"disabled":"load"}' + return 0 + fi + # Wall-clock budget clock starts at the TRUE function entry (luna # folds 2026-08-10): the budget must bound the whole call — including # read_rpc_input (stalled stdin must not escape the budget) and json diff --git a/luci-app-gecoosac/Makefile b/luci-app-gecoosac/Makefile index 35cd0f1e8..6f7661d53 100644 --- a/luci-app-gecoosac/Makefile +++ b/luci-app-gecoosac/Makefile @@ -7,10 +7,10 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-gecoosac PKG_VERSION:=2.2 -PKG_RELEASE:=20 +PKG_RELEASE:=21 LUCI_TITLE:=LuCI Support for gecoosac -LUCI_DEPENDS:=+luci-base +gecoosac +gecoosac-files +LUCI_DEPENDS:=+luci-base +gecoosac +gecoosac-files +gecoosac-common LUCI_EXTRA_DEPENDS:=gecoosac (>=2.2.20251015) LUCI_PKGARCH:=all PKG_LICENSE:=AGPL-3.0-only diff --git a/luci-app-gecoosac/htdocs/luci-static/resources/view/gecoosac.js b/luci-app-gecoosac/htdocs/luci-static/resources/view/gecoosac.js index 5c4d4392c..c19d730f3 100644 --- a/luci-app-gecoosac/htdocs/luci-static/resources/view/gecoosac.js +++ b/luci-app-gecoosac/htdocs/luci-static/resources/view/gecoosac.js @@ -403,6 +403,7 @@ return view.extend({ o.default = '8080'; o.datatype = 'port'; o.depends('isonlyoneprot', '0'); + o.retain = true; portOption.validate = function(section_id, value) { return validatePortValue(section_id, value, managementPortOption, singlePortOption, true); @@ -422,10 +423,11 @@ return view.extend({ return true; }; httpsOption = s.option(form.Flag, 'https', _('Enable HTTPS service'), - _('Default certificate files are generated when HTTPS starts; custom paths must point to a readable certificate and matching key.')); + _('When HTTPS starts, default certificates are generated or renewed if fewer than 30 days remain. Custom certificates must be currently valid and match their private keys.')); o = httpsOption; o.default = '0'; o.depends('isonlyoneprot', '0'); + o.retain = true; certificateOption = s.option(form.Value, 'crt_file', _('Specify crt certificate file')); o = certificateOption; @@ -433,6 +435,7 @@ return view.extend({ o.default = DEFAULT_CRT_FILE; o.datatype = 'file'; o.depends({ isonlyoneprot: '0', https: '1' }); + o.retain = true; o.validate = function(section_id, value) { return validateCertificatePath(section_id, value, singlePortOption, httpsOption); }; @@ -443,6 +446,7 @@ return view.extend({ o.default = DEFAULT_KEY_FILE; o.datatype = 'file'; o.depends({ isonlyoneprot: '0', https: '1' }); + o.retain = true; o.validate = function(section_id, value) { return validateCertificatePath(section_id, value, singlePortOption, httpsOption); }; @@ -481,7 +485,7 @@ return view.extend({ }; o = s.option(form.Value, 'db_dir', _('Database dir path'), - _('Store the config database under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac. Do not place it inside the upload directory.')); + _('Store the config database under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac. Only /etc/gecoosac persists across reboots on standard OpenWrt. Do not place it inside the upload directory.')); o.placeholder = DEFAULT_DB_DIR; o.default = DEFAULT_DB_DIR; o.datatype = 'directory'; diff --git a/luci-app-gecoosac/po/zh_Hans/gecoosac.po b/luci-app-gecoosac/po/zh_Hans/gecoosac.po index 8c51604c5..6aff00712 100644 --- a/luci-app-gecoosac/po/zh_Hans/gecoosac.po +++ b/luci-app-gecoosac/po/zh_Hans/gecoosac.po @@ -9,8 +9,8 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -msgid "Default certificate files are generated when HTTPS starts; custom paths must point to a readable certificate and matching key." -msgstr "默认证书文件会在 HTTPS 启动时生成;自定义路径必须指向可读取且相互匹配的证书和私钥。" +msgid "When HTTPS starts, default certificates are generated or renewed if fewer than 30 days remain. Custom certificates must be currently valid and match their private keys." +msgstr "HTTPS 启动时会生成默认证书,或在剩余有效期不足 30 天时更新。自定义证书必须在有效期内且与私钥匹配。" msgid "Gecoos AC" msgstr "集客AC控制器" @@ -68,8 +68,8 @@ msgstr "上传目录必须是 /tmp/gecoosac/upload 或 /mnt/storage-name/gecoosa msgid "Database dir path" msgstr "数据库目录" -msgid "Store the config database under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac. Do not place it inside the upload directory." -msgstr "将配置数据库存放在 /etc/gecoosac、/tmp/gecoosac 或 /var/lib/gecoosac 下。不要放在上传目录内。" +msgid "Store the config database under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac. Only /etc/gecoosac persists across reboots on standard OpenWrt. Do not place it inside the upload directory." +msgstr "将配置数据库存放在 /etc/gecoosac、/tmp/gecoosac 或 /var/lib/gecoosac 下。标准 OpenWrt 上只有 /etc/gecoosac 的数据会在重启后保留。不要放在上传目录内。" msgid "Database directory must be under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac." msgstr "数据库目录必须位于 /etc/gecoosac、/tmp/gecoosac 或 /var/lib/gecoosac 下。" diff --git a/luci-app-gecoosac/po/zh_Hant/gecoosac.po b/luci-app-gecoosac/po/zh_Hant/gecoosac.po index 01d24d62e..947bdf0c3 100644 --- a/luci-app-gecoosac/po/zh_Hant/gecoosac.po +++ b/luci-app-gecoosac/po/zh_Hant/gecoosac.po @@ -9,8 +9,8 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -msgid "Default certificate files are generated when HTTPS starts; custom paths must point to a readable certificate and matching key." -msgstr "預設憑證檔案會在 HTTPS 啟動時產生;自訂路徑必須指向可讀取且相互匹配的憑證與私鑰。" +msgid "When HTTPS starts, default certificates are generated or renewed if fewer than 30 days remain. Custom certificates must be currently valid and match their private keys." +msgstr "HTTPS 啟動時會產生預設憑證,或在剩餘有效期不足 30 天時更新。自訂憑證必須在有效期內且與私鑰相符。" msgid "Gecoos AC" msgstr "集客 AC 控制器" @@ -68,8 +68,8 @@ msgstr "上傳目錄必須是 /tmp/gecoosac/upload 或 /mnt/storage-name/gecoosa msgid "Database dir path" msgstr "資料庫目錄" -msgid "Store the config database under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac. Do not place it inside the upload directory." -msgstr "將設定資料庫存放在 /etc/gecoosac、/tmp/gecoosac 或 /var/lib/gecoosac 下。不要放在上傳目錄內。" +msgid "Store the config database under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac. Only /etc/gecoosac persists across reboots on standard OpenWrt. Do not place it inside the upload directory." +msgstr "將設定資料庫存放在 /etc/gecoosac、/tmp/gecoosac 或 /var/lib/gecoosac 下。標準 OpenWrt 上只有 /etc/gecoosac 的資料會在重新啟動後保留。不要放在上傳目錄內。" msgid "Database directory must be under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac." msgstr "資料庫目錄必須位於 /etc/gecoosac、/tmp/gecoosac 或 /var/lib/gecoosac 下。" diff --git a/luci-app-gecoosac/root/usr/libexec/rpcd/luci.gecoosac b/luci-app-gecoosac/root/usr/libexec/rpcd/luci.gecoosac index 4106ce887..c0e949af1 100755 --- a/luci-app-gecoosac/root/usr/libexec/rpcd/luci.gecoosac +++ b/luci-app-gecoosac/root/usr/libexec/rpcd/luci.gecoosac @@ -2,12 +2,7 @@ . /usr/share/libubox/jshn.sh . /lib/functions.sh - -DEFAULT_DB_DIR=/etc/gecoosac -DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload -DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt -DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key -DEFAULT_PID_DIR=/var/run +. /usr/share/gecoosac/common.sh json_result() { local ok="$1" @@ -22,116 +17,6 @@ json_result() { json_cleanup } -normalize_path() { - local path="$1" - local part normalized parent - - case "$path" in - /*) ;; - *) return 1 ;; - esac - - normalized="/" - path="${path#/}" - - while [ -n "$path" ]; do - part="${path%%/*}" - if [ "$part" = "$path" ]; then - path="" - else - path="${path#*/}" - fi - - case "$part" in - ""|.) ;; - ..) - if [ "$normalized" != "/" ]; then - parent="${normalized%/*}" - [ -n "$parent" ] || parent="/" - normalized="$parent" - fi - ;; - *) normalized="${normalized%/}/$part" ;; - esac - done - - printf '%s\n' "$normalized" -} - -path_has_clear_stage_component() { - local path="$1" part rest - - path="$(normalize_path "$path")" || return 1 - rest="${path#/}" - while [ -n "$rest" ]; do - part="${rest%%/*}" - if [ "$part" = "$rest" ]; then - rest="" - else - rest="${rest#*/}" - fi - case "$part" in - .gecoosac-clear.*) return 0 ;; - esac - done - - return 1 -} - -path_uses_clear_stage() { - local path="$1" resolved - - path_has_clear_stage_component "$path" && return 0 - if [ -e "$path" ] || [ -L "$path" ]; then - resolved="$(readlink -f "$path" 2>/dev/null)" || return 1 - path_has_clear_stage_component "$resolved" && return 0 - fi - - return 1 -} - -is_supported_upload_path() { - local path="$1" storage - - path="$(normalize_path "$path")" || return 1 - path_uses_clear_stage "$path" && return 1 - [ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0 - - case "$path" in - /mnt/*/gecoosac/upload) - storage="${path#/mnt/}" - storage="${storage%/gecoosac/upload}" - [ -n "$storage" ] && [ "${storage#*/}" = "$storage" ] - ;; - *) return 1 ;; - esac -} - -managed_dir_path() { - local role="$1" path anchor - - case "$role" in - upload|db|pid|file) ;; - *) return 1 ;; - esac - path="$(normalize_path "$2")" || return 1 - case "$path" in - /var/run|/var/run/*) - anchor="$(readlink -f /var/run 2>/dev/null)" || return 1 - [ "$anchor" = "/tmp/run" ] || return 1 - printf '%s%s\n' "$anchor" "${path#/var/run}" - ;; - /var|/var/*) - anchor="$(readlink -f /var 2>/dev/null)" || return 1 - case "$anchor" in - /var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;; - *) return 1 ;; - esac - ;; - *) printf '%s\n' "$path" ;; - esac -} - load_clear_config() { local section_type @@ -151,135 +36,17 @@ load_clear_config() { config_get clear_key_file config key_file "$DEFAULT_KEY_FILE" } -safe_upload_path() { - local path - - path="$(normalize_path "$1")" || return 1 - is_supported_upload_path "$path" -} - -path_in_dir() { - local path root - - path="$(normalize_path "$1")" || return 1 - root="$(normalize_path "$2")" || return 1 - - [ "$root" != "/" ] || return 1 - [ "$path" = "$root" ] && return 0 - [ "${path#"$root"/}" != "$path" ] -} - -path_has_mount() { - local root line mount_path - - root="$(normalize_path "$1")" || return 2 - [ -r /proc/self/mountinfo ] || return 2 - - while IFS= read -r line; do - mount_path="$(printf '%s\n' "$line" | cut -d ' ' -f 5)" || return 2 - [ -n "$mount_path" ] || return 2 - mount_path="$(printf '%b\n' "$mount_path" 2>/dev/null)" || return 2 - mount_path="$(normalize_path "$mount_path")" || return 2 - path_in_dir "$mount_path" "$root" && return 0 - done < /proc/self/mountinfo - - return 1 -} - -is_secure_dir() { - local allow_sticky="$2" owner permissions metadata - - [ -d "$1" ] && [ ! -L "$1" ] || return 1 - metadata="$(ls -ldn "$1" 2>/dev/null)" || return 1 - set -- $metadata - permissions="$1" - owner="$3" - [ "$owner" = "0" ] || return 1 - case "$permissions" in - d?????????) ;; - *) return 1 ;; - esac - - if [ "$(printf '%s' "$permissions" | cut -c6)" = "w" ] || \ - [ "$(printf '%s' "$permissions" | cut -c9)" = "w" ]; then - [ "$allow_sticky" = "1" ] && [ "$(printf '%s' "$permissions" | cut -c10)" = "t" ] || return 1 - fi -} - -is_secure_upload_dir() { - local path current part rest - - path="$(normalize_path "$1")" || return 1 - [ -d "$path" ] && [ ! -L "$path" ] || return 1 - current="/" - rest="${path#/}" - - while [ -n "$rest" ]; do - part="${rest%%/*}" - if [ "$part" = "$rest" ]; then - rest="" - else - rest="${rest#*/}" - fi - current="${current%/}/$part" - case "$current" in - /tmp) is_secure_dir "$current" 1 || return 1 ;; - *) is_secure_dir "$current" || return 1 ;; - esac - done -} - configured_path_in_upload() { - local option="$1" checked_root="$2" - local live_logical="${3:-$2}" live_physical="${4:-$2}" - local role path physical real_path suffix mapped + local path role - case "$option" in + case "$1" in db_dir) path="$clear_db_dir"; role=db ;; piddir) path="$clear_piddir"; role=pid ;; crt_file) path="$clear_crt_file"; role="file" ;; key_file) path="$clear_key_file"; role="file" ;; *) return 2 ;; esac - [ -n "$path" ] || return 1 - - path="$(normalize_path "$path")" || return 2 - physical="$(managed_dir_path "$role" "$path")" || return 2 - checked_root="$(normalize_path "$checked_root")" || return 2 - live_logical="$(normalize_path "$live_logical")" || return 2 - live_physical="$(normalize_path "$live_physical")" || return 2 - - path_in_dir "$path" "$checked_root" && return 0 - path_in_dir "$physical" "$checked_root" && return 0 - real_path= - if [ -e "$path" ] || [ -L "$path" ]; then - real_path="$(readlink -f "$path" 2>/dev/null)" || return 2 - [ -n "$real_path" ] || return 2 - real_path="$(normalize_path "$real_path")" || return 2 - path_in_dir "$real_path" "$checked_root" && return 0 - fi - - [ "$checked_root" != "$live_physical" ] || { - path_in_dir "$path" "$live_logical" && return 0 - path_in_dir "$physical" "$live_physical" && return 0 - if [ -n "$real_path" ] && path_in_dir "$real_path" "$live_physical"; then - return 0 - fi - return 1 - } - - suffix= - if path_in_dir "$path" "$live_logical"; then - suffix="${path#"$live_logical"}" - elif path_in_dir "$physical" "$live_physical"; then - suffix="${physical#"$live_physical"}" - elif [ -n "$real_path" ] && path_in_dir "$real_path" "$live_physical"; then - suffix="${real_path#"$live_physical"}" - else - return 1 - fi - mapped="${checked_root%/}${suffix}" - [ -e "$mapped" ] || [ -L "$mapped" ] + protected_path_in_dir "$path" "$role" "$2" "${3:-$2}" "${4:-$2}" } status_result() { @@ -534,7 +301,7 @@ clear_upload() { esac logical="$(normalize_path "$logical")" || { json_result 0 "Expecting an absolute path"; return; } - if ! safe_upload_path "$logical"; then + if ! is_supported_upload_path "$logical"; then json_result 0 "Only Gecoos upload directories can be cleared" "$logical" return fi @@ -542,7 +309,7 @@ clear_upload() { json_result 0 "Unable to resolve upload directory" "$logical" return } - if ! safe_upload_path "$physical"; then + if ! is_supported_upload_path "$physical"; then json_result 0 "Only Gecoos upload directories can be cleared" "$logical" return fi diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 8af62e7e2..73411a9c5 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.9.9 -PKG_RELEASE:=269 +PKG_RELEASE:=270 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/luci-app-passwall/luasrc/passwall/util_sing-box.lua index ee91ac350..ca4ee6fa7 100644 --- a/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -913,7 +913,7 @@ function gen_config_server(node) stream_receive_window = node.hysteria_recv_window_conn and tonumber(node.hysteria_recv_window_conn) or nil, connection_receive_window = node.hysteria_recv_window_client and tonumber(node.hysteria_recv_window_client) or nil, max_concurrent_streams = node.hysteria_max_conn_client and tonumber(node.hysteria_max_conn_client) or nil, - disable_path_mtu_discover = (node.hysteria_disable_mtu_discovery == "1") and true or false, + disable_path_mtu_discovery = (node.hysteria_disable_mtu_discovery == "1") and true or false, tls = tls } end diff --git a/luci-app-passwall/root/usr/share/passwall/iptables.sh b/luci-app-passwall/root/usr/share/passwall/iptables.sh index 5cee89fbe..405d54576 100755 --- a/luci-app-passwall/root/usr/share/passwall/iptables.sh +++ b/luci-app-passwall/root/usr/share/passwall/iptables.sh @@ -1098,7 +1098,7 @@ add_firewall_rule() { insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW" # Only TCP, UDP Invalid. - insert_rule_before "$ipt_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT" + insert_rule_before "$ipt_m" "PREROUTING" "PSW" "-p tcp -m socket --transparent -j PSW_DIVERT" $ipt_m -N PSW_OUTPUT $ipt_m -A PSW_OUTPUT $(dst $IPSET_LAN) -j RETURN @@ -1172,7 +1172,7 @@ add_firewall_rule() { insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW" # Only TCP, UDP Invalid. - insert_rule_before "$ip6t_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT" + insert_rule_before "$ip6t_m" "PREROUTING" "PSW" "-p tcp -m socket --transparent -j PSW_DIVERT" $ip6t_m -N PSW_OUTPUT $ip6t_m -A PSW_OUTPUT -m mark --mark 0xff/0xff -j RETURN @@ -1477,7 +1477,7 @@ gen_include() { [ -z "${_ipt}" ] && return echo "*$2" - ${_ipt}-save -t $2 | grep "PSW" | grep -v "\-j PSW$" | grep -v "mangle\-OUTPUT\-PSW" | grep -v "socket \-j PSW_DIVERT$" | sed -e "s/^-A \(OUTPUT\|PREROUTING\)/-I \1 1/" + ${_ipt}-save -t $2 | grep "PSW" | grep -v "\-j PSW$" | grep -v "mangle\-OUTPUT\-PSW" | grep -v "\-m socket .*\-j PSW_DIVERT$" | sed -e "s/^-A \(OUTPUT\|PREROUTING\)/-I \1 1/" echo 'COMMIT' } local __ipt="" @@ -1501,7 +1501,7 @@ gen_include() { [ -z "${is_tproxy}" ] && \$(${MY_PATH} insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p tcp -j PSW") \$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW") - \$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT") + \$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "PSW" "-p tcp -m socket --transparent -j PSW_DIVERT") EOF ) } @@ -1522,7 +1522,7 @@ gen_include() { [ "$accept_icmpv6" = "1" ] && $ip6t_n -A PREROUTING -p ipv6-icmp -j PSW \$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW") - \$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT") + \$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "PSW" "-p tcp -m socket --transparent -j PSW_DIVERT") EOF ) } diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index 54224e762..7338ef6e2 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 PKG_VERSION:=26.9.12 -PKG_RELEASE:=119 +PKG_RELEASE:=120 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua index e58d7f6b2..7a8f47f9c 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua @@ -934,7 +934,7 @@ function gen_config_server(node) stream_receive_window = node.hysteria_recv_window_conn and tonumber(node.hysteria_recv_window_conn) or nil, connection_receive_window = node.hysteria_recv_window_client and tonumber(node.hysteria_recv_window_client) or nil, max_concurrent_streams = node.hysteria_max_conn_client and tonumber(node.hysteria_max_conn_client) or nil, - disable_path_mtu_discover = (node.hysteria_disable_mtu_discovery == "1") and true or false, + disable_path_mtu_discovery = (node.hysteria_disable_mtu_discovery == "1") and true or false, tls = tls } end diff --git a/mise/Makefile b/mise/Makefile index 4551eb455..83cd56288 100644 --- a/mise/Makefile +++ b/mise/Makefile @@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264 MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437 PKG_NAME:=mise -PKG_VERSION:=2026.9.5 -PKG_RELEASE:=11 +PKG_VERSION:=2026.9.6 +PKG_RELEASE:=12 PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/ PKG_HASH:=skip diff --git a/natflow/Makefile b/natflow/Makefile index 24aa480b7..f69de6737 100644 --- a/natflow/Makefile +++ b/natflow/Makefile @@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk PKG_NAME:=natflow PKG_VERSION:=20260531 -PKG_RELEASE:=83 +PKG_RELEASE:=84 PKG_SOURCE:=$(PKG_VERSION).tar.xz PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git PKG_SOURCE_PROTO:=git -PKG_SOURCE_VERSION:=6f03a5d1e50438ff72b0c9f19a5c9c97c329c62c +PKG_SOURCE_VERSION:=36123df5df3371516b648e511dad66d5413b47aa PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_MAINTAINER:=Chen Minqiang PKG_LICENSE:=GPL-2.0 diff --git a/torrserver/Makefile b/torrserver/Makefile index 446c0fd19..57397d63c 100644 --- a/torrserver/Makefile +++ b/torrserver/Makefile @@ -8,12 +8,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=torrserver -PKG_VERSION:=MatriX.144.3 -PKG_RELEASE:=22 +PKG_VERSION:=MatriX.144.4 +PKG_RELEASE:=23 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/YouROK/TorrServer.git -PKG_SOURCE_VERSION:=80f50702c1e852dcad59cdd1399ac8aa7a092fee +PKG_SOURCE_VERSION:=215f6fcaa84685fc0263584748206fca0810a8b6 PKG_MIRROR_HASH:=skip PKG_MAINTAINER:=Konstantine Shevlakov diff --git a/wwand/Makefile b/wwand/Makefile index 1431853cd..f4b87b4e0 100644 --- a/wwand/Makefile +++ b/wwand/Makefile @@ -49,8 +49,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=wwand -PKG_VERSION:=1.6.6_p22 -PKG_RELEASE:=34 +PKG_VERSION:=1.6.6_p24 +PKG_RELEASE:=35 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git