💐 Sync 2026-09-13 10:49:47

This commit is contained in:
github-actions[bot] committed 2026-09-13 10:49:47 +08:00
1 parent 2f26109690
commit 886123f647
21 files changed
+881 -807

No files matched your search

+5 -3
View File
@@ -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 <laipeng668@qq.com>
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)))
+48 -322
View File
@@ -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
status="$?"
rm -rf "$tmp_dir"
return "$status"
}
is_ipv4() {
local value="$1"
local part count
@@ -328,48 +78,55 @@ san_has_entry() {
local san="$1"
local entry="$2"
echo "$san" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -F -x -q "$entry"
printf '%s\n' "$san" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -F -x -q "$entry"
}
cert_matches_san() {
local cert_file="$1" cert_host="$2" cert_ip="$3"
local san tmp_dir
run_with_timeout_output() {
local limit="$1" output="$2"
shift 2
is_readable_regular_file "$cert_file" || return 1
tmp_dir="$(mktemp -d /tmp/gecoosac-cert.XXXXXX)" || return 1
if ! run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/san" \
openssl x509 -in "$cert_file" -noout -ext subjectAltName </dev/null; then
rm -rf "$tmp_dir"
return 1
fi
san="$(cat "$tmp_dir/san" 2>/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 || \
! run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/key.pub" \
openssl pkey -in "$key_file" -pubout </dev/null; then
rm -rf "$tmp_dir"
return 1
status=1
if run_with_timeout "$CERT_TIMEOUT" /bin/sh -c '
[ -f "$1" ] && [ -r "$1" ] && [ -s "$1" ] &&
[ -f "$2" ] && [ -r "$2" ] && [ -s "$2" ]
' gecoosac-file-check "$cert_file" "$key_file" </dev/null && \
run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/cert" \
openssl x509 -in "$cert_file" -noout -pubkey -ext subjectAltName -checkend "$min_lifetime" </dev/null && \
run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/key.pub" \
openssl pkey -in "$key_file" -pubout </dev/null && \
run_with_timeout "$CERT_TIMEOUT" \
openssl verify -partial_chain -trusted "$cert_file" "$cert_file" </dev/null; then
# Trust the configured leaf only for the local validity-period check;
# browser trust still depends on the certificate installed by the user.
cert_pub="$(sed -n '/^-----BEGIN PUBLIC KEY-----$/,/^-----END PUBLIC KEY-----$/p' "$tmp_dir/cert")"
key_pub="$(cat "$tmp_dir/key.pub")"
if [ -n "$cert_pub" ] && [ "$cert_pub" = "$key_pub" ]; then
status=0
if [ -n "$cert_host" ]; then
san="$(cat "$tmp_dir/cert")"
san_has_entry "$san" "DNS:${cert_host}" || status=1
[ -z "$cert_ip" ] || san_has_entry "$san" "IP Address:${cert_ip}" || status=1
fi
fi
fi
cert_pub="$(cat "$tmp_dir/cert.pub" 2>/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
+29 -205
View File
@@ -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() {
+282
View File
@@ -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" ]
}
+1 -1
View File
@@ -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
+443
View File
@@ -0,0 +1,443 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# Copyright 2025-2026 Lucas Albers <lucas.b.albers@gmail.com>
#
# 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:-<state-dir>/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 _ </proc/uptime 2>/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"
}
+33 -7
View File
@@ -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
+2 -2
View File
@@ -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
@@ -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';
+4 -4
View File
@@ -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 下。"
+4 -4
View File
@@ -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 下。"
@@ -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
+1 -1
View File
@@ -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:= \
@@ -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
@@ -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
)
}
+1 -1
View File
@@ -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:= \
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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 <ptpt52@gmail.com>
PKG_LICENSE:=GPL-2.0
+3 -3
View File
@@ -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 <shevlakov@132lan.ru>
+2 -2
View File
@@ -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