🎉 Sync 2026-09-27 21:03:26

This commit is contained in:
github-actions[bot] committed 2026-09-27 21:03:26 +08:00
1 parent f4559bbed1
commit 065763f7fd
673 files changed
+261285 -34160

No files matched your search

+258
View File
@@ -0,0 +1,258 @@
#!/bin/sh
# shellcheck shell=ash
# Variables below are populated by sourced helpers and ss_load_config()
# shellcheck disable=SC2154
LC_ALL=C
IPKG_INSTROOT="${IPKG_INSTROOT:-}"
export IPKG_INSTROOT
# shellcheck disable=SC1091
. /lib/functions.sh
# shellcheck disable=SC1091
. /usr/lib/safeshield/core.sh
readonly SS_REFRESHD_LOCK='/var/lock/safeshield-refreshd.lock'
readonly SS_REFRESHD_LOCK_FD=308
ss_should_terminate=0
ss_sleep_pid=''
refreshd_lock() {
mkdir -p "${SS_REFRESHD_LOCK%/*}" || return 1
eval "exec ${SS_REFRESHD_LOCK_FD}>\"${SS_REFRESHD_LOCK}\"" || return 1
flock -n "${SS_REFRESHD_LOCK_FD}" || return 1
}
refreshd_unlock() {
flock -u "${SS_REFRESHD_LOCK_FD}" 2>/dev/null || true
eval "exec ${SS_REFRESHD_LOCK_FD}>&-" 2>/dev/null || true
}
handle_term() {
ss_should_terminate=1
if [ -n "$ss_sleep_pid" ]; then
kill "$ss_sleep_pid" 2>/dev/null || true
fi
}
sleep_seconds() {
local seconds="$1"
local rc=0
[ "$ss_should_terminate" -eq 1 ] && return 1
[ "$seconds" -gt 0 ] 2>/dev/null || return 0
sleep "$seconds" &
ss_sleep_pid=$!
wait "$ss_sleep_pid" 2>/dev/null || rc=$?
ss_sleep_pid=''
[ "$ss_should_terminate" -eq 1 ] && return 1
[ "$rc" -eq 0 ]
}
wait_for_wan_if_needed() {
local require_wan
require_wan="$(ss_config_get config require_wan 1)"
case "$require_wan" in
1)
ubus -t 30 wait_for network.interface.wan 2>/dev/null || true
;;
esac
}
status_epoch() {
local key="$1"
local value
value="$(json get "$key" 2>/dev/null)"
is_valid_integer "$value" || value=0
printf '%s\n' "$value"
}
schedule_base_epoch() {
local now="$1"
local last_success last_attempt base=0
last_success="$(status_epoch last_success)"
last_attempt="$(status_epoch last_attempt)"
if [ "$last_success" -gt 0 ] 2>/dev/null && [ "$last_success" -le "$now" ] 2>/dev/null; then
base="$last_success"
fi
# A failed or currently-running refresh must also move the retry window
# forward so refreshd cannot enter an immediate retry loop. A successful
# refresh is naturally newer than its attempt timestamp and wins here.
if [ "$last_attempt" -gt "$base" ] 2>/dev/null && [ "$last_attempt" -le "$now" ] 2>/dev/null; then
base="$last_attempt"
fi
printf '%s\n' "$base"
}
has_recent_success() {
local interval="$1"
local now last_success age
now="$(date +%s)"
last_success="$(status_epoch last_success)"
[ "$last_success" -gt 0 ] 2>/dev/null || return 1
[ "$last_success" -le "$now" ] 2>/dev/null || return 1
age=$((now - last_success))
[ "$age" -lt "$interval" ] 2>/dev/null
}
set_next_refresh_epoch() {
local epoch="$1"
is_valid_integer "$epoch" || epoch=0
ss_status_set next_refresh_at "$epoch"
}
clear_next_refresh_at() {
ss_status_set next_refresh_at "0"
}
wait_until_refresh_due() {
local interval="$1"
local fallback_epoch="$2"
local now base due remaining
while :; do
[ "$ss_should_terminate" -eq 1 ] && return 1
now="$(date +%s)"
base="$(schedule_base_epoch "$now")"
if [ "$base" -le 0 ] 2>/dev/null; then
base="$fallback_epoch"
fi
due=$((base + interval))
remaining=$((due - now))
if [ "$remaining" -le 0 ] 2>/dev/null; then
clear_next_refresh_at
return 0
fi
set_next_refresh_epoch "$due"
sleep_seconds "$remaining" || return 1
# A manual refresh may have completed while refreshd was sleeping.
# Recalculate from the latest last_success/last_attempt before running.
done
}
main() {
local interval
local boot_delay
local refresh_on_boot
local schedule_anchor
refreshd_lock || exit 0
ss_load_config || exit 1
if [ ! -s "${RUNNING_STATUS_FILE}" ]; then
ss_status_reset || true
fi
ss_sync_blocklist_status
ss_status_set status "idle"
ss_status_set stage "starting"
interval="$(ss_config_get config refresh_interval_s 28800)"
boot_delay="$(ss_config_get config boot_start_delay_s 30)"
refresh_on_boot="$(ss_config_get config refresh_on_boot 1)"
is_valid_integer "$interval" || interval=28800
[ "$interval" -gt 0 ] 2>/dev/null || interval=28800
is_valid_integer "$boot_delay" || boot_delay=30
wait_for_wan_if_needed
ss_status_set stage "boot_delay"
set_next_refresh_epoch "$(($(date +%s) + boot_delay))"
sleep_seconds "$boot_delay" || exit 0
if [ "$refresh_on_boot" = "1" ] && [ "${ss_enabled}" = "1" ]; then
if has_recent_success "$interval"; then
log_info "Skipping boot refresh because a recent refresh already succeeded"
ss_status_set status "idle"
ss_status_set stage "boot_refresh_skipped"
else
ss_status_set status "running"
ss_status_set stage "boot_refresh"
clear_next_refresh_at
safeshield_force_download
fi
fi
schedule_anchor="$(date +%s)"
while :; do
ss_load_config || {
schedule_anchor="$(date +%s)"
sleep_seconds 5 || break
continue
}
interval="$(ss_config_get config refresh_interval_s 28800)"
is_valid_integer "$interval" || interval=28800
[ "$interval" -gt 0 ] 2>/dev/null || interval=28800
if [ "${ss_enabled}" != "1" ]; then
ss_status_set status "disabled"
ss_status_set stage "disabled"
clear_next_refresh_at
sleep_seconds "$interval" || break
schedule_anchor="$(date +%s)"
continue
fi
ss_status_set status "idle"
ss_status_set stage "scheduled_wait"
wait_until_refresh_due "$interval" "$schedule_anchor" || break
ss_load_config || {
schedule_anchor="$(date +%s)"
continue
}
if [ "${ss_enabled}" != "1" ]; then
ss_status_set status "disabled"
ss_status_set stage "disabled"
clear_next_refresh_at
schedule_anchor="$(date +%s)"
continue
fi
wait_for_wan_if_needed || true
ss_status_set status "running"
ss_status_set stage "scheduled_refresh"
clear_next_refresh_at
safeshield_force_download
schedule_anchor="$(date +%s)"
done
ss_load_config >/dev/null 2>&1 || true
if [ "${ss_enabled:-0}" = "1" ]; then
ss_status_set status "idle"
ss_status_set stage "stopped"
else
ss_status_set status "disabled"
ss_status_set stage "disabled"
fi
clear_next_refresh_at
}
trap "handle_term" TERM INT
trap "refreshd_unlock; clear_next_refresh_at" EXIT
main "$@"
+496
View File
@@ -0,0 +1,496 @@
#!/bin/sh
# shellcheck shell=ash
# Variables are populated by sourced SafeShield helpers and ss_load_config().
# shellcheck disable=SC2154
LC_ALL=C
export LC_ALL
umask 077
SS_STATS_UPLOAD_FUNCTIONS_LIB="${SS_STATS_UPLOAD_FUNCTIONS_LIB:-/lib/functions.sh}"
SS_STATS_UPLOAD_CORE_LIB="${SS_STATS_UPLOAD_CORE_LIB:-/usr/lib/safeshield/core.sh}"
SS_STATS_UPLOAD_INTERVAL_S="${SS_STATS_UPLOAD_INTERVAL_S:-1800}"
SS_STATS_UPLOAD_IDLE_WAIT_S="${SS_STATS_UPLOAD_IDLE_WAIT_S:-15}"
SS_STATS_UPLOAD_RETRY_DELAY_S="${SS_STATS_UPLOAD_RETRY_DELAY_S:-2}"
SS_STATS_UPLOAD_FULL_EVERY="${SS_STATS_UPLOAD_FULL_EVERY:-23}"
SS_STATS_UPLOAD_DENIED_RECHECK_S="${SS_STATS_UPLOAD_DENIED_RECHECK_S:-43200}"
SS_STATS_UPLOAD_RESPONSE_FILE="${SS_STATS_UPLOAD_RESPONSE_FILE:-/tmp/safeshield/statistics/upload.response.json}"
# shellcheck disable=SC1090
. "$SS_STATS_UPLOAD_FUNCTIONS_LIB"
# shellcheck disable=SC1090
. "$SS_STATS_UPLOAD_CORE_LIB"
ss_should_terminate=0
upload_sleep_pid=''
upload_state_generation=''
upload_state_last_success_seq=0
upload_state_last_full_seq=0
upload_state_recent_successes=0
upload_state_needs_full=1
upload_pending_mode=''
upload_pending_generation=''
upload_pending_seq=0
upload_handle_term() {
ss_should_terminate=1
if [ -n "$upload_sleep_pid" ]; then
kill "$upload_sleep_pid" 2>/dev/null || true
fi
}
upload_sleep() {
local seconds="$1"
local rc=0
[ "$ss_should_terminate" -eq 1 ] && return 1
[ "$seconds" -gt 0 ] 2>/dev/null || return 0
sleep "$seconds" &
upload_sleep_pid=$!
wait "$upload_sleep_pid" 2>/dev/null || rc=$?
upload_sleep_pid=''
[ "$ss_should_terminate" -eq 1 ] && return 1
[ "$rc" -eq 0 ]
}
upload_state_reset() {
upload_state_generation=''
upload_state_last_success_seq=0
upload_state_last_full_seq=0
upload_state_recent_successes=0
upload_state_needs_full=1
}
upload_state_load() {
local old_ifs
upload_state_reset
[ -s "$SS_STATISTICS_UPLOAD_STATE_FILE" ] || return 0
old_ifs="$IFS"
IFS="$(printf '\t')"
read -r upload_state_generation upload_state_last_success_seq upload_state_last_full_seq upload_state_recent_successes upload_state_needs_full <"$SS_STATISTICS_UPLOAD_STATE_FILE" || true
IFS="$old_ifs"
is_valid_integer "$upload_state_last_success_seq" || upload_state_last_success_seq=0
is_valid_integer "$upload_state_last_full_seq" || upload_state_last_full_seq=0
is_valid_integer "$upload_state_recent_successes" || upload_state_recent_successes=0
case "$upload_state_needs_full" in
0 | 1) ;;
*) upload_state_needs_full=1 ;;
esac
}
upload_state_save() {
local tmp="${SS_STATISTICS_UPLOAD_STATE_FILE}.tmp.$$"
mkdir -p "${SS_STATISTICS_UPLOAD_STATE_FILE%/*}" || return 1
printf '%s\t%s\t%s\t%s\t%s\n' \
"$upload_state_generation" \
"$upload_state_last_success_seq" \
"$upload_state_last_full_seq" \
"$upload_state_recent_successes" \
"$upload_state_needs_full" >"$tmp" || {
rm -f "$tmp"
return 1
}
mv -f "$tmp" "$SS_STATISTICS_UPLOAD_STATE_FILE" || {
rm -f "$tmp"
return 1
}
}
upload_mark_needs_full() {
local generation="${1:-$upload_state_generation}"
if [ -n "$generation" ] && [ "$upload_state_generation" != "$generation" ]; then
upload_state_generation="$generation"
upload_state_last_success_seq=0
upload_state_last_full_seq=0
upload_state_recent_successes=0
fi
upload_state_needs_full=1
upload_state_save >/dev/null 2>&1 || true
}
upload_pending_clear() {
rm -f "$SS_STATISTICS_UPLOAD_PENDING_FILE" "$SS_STATISTICS_UPLOAD_PENDING_META_FILE"
upload_pending_mode=''
upload_pending_generation=''
upload_pending_seq=0
}
upload_pending_load() {
local old_ifs payload_generation payload_seq
upload_pending_mode=''
upload_pending_generation=''
upload_pending_seq=0
[ -s "$SS_STATISTICS_UPLOAD_PENDING_FILE" ] || return 1
[ -s "$SS_STATISTICS_UPLOAD_PENDING_META_FILE" ] || {
upload_pending_clear
return 1
}
old_ifs="$IFS"
IFS="$(printf '\t')"
read -r upload_pending_mode upload_pending_generation upload_pending_seq <"$SS_STATISTICS_UPLOAD_PENDING_META_FILE" || true
IFS="$old_ifs"
case "$upload_pending_mode" in
full | recent) ;;
*)
upload_pending_clear
return 1
;;
esac
is_valid_integer "$upload_pending_seq" || {
upload_pending_clear
return 1
}
[ "$upload_pending_seq" -gt 0 ] 2>/dev/null || {
upload_pending_clear
return 1
}
payload_generation="$(ss_json_get_file "$SS_STATISTICS_UPLOAD_PENDING_FILE" '@.generation_id')"
payload_seq="$(ss_json_get_file "$SS_STATISTICS_UPLOAD_PENDING_FILE" '@.snapshot_seq')"
if [ "$payload_generation" != "$upload_pending_generation" ] || [ "$payload_seq" != "$upload_pending_seq" ]; then
upload_pending_clear
return 1
fi
return 0
}
upload_source_is_v3() {
local file="$1"
local schema_name schema_version seq generation
[ -s "$file" ] || return 1
schema_name="$(ss_json_get_file "$file" '@.schema.name')"
schema_version="$(ss_json_get_file "$file" '@.schema.version')"
generation="$(ss_json_get_file "$file" '@.generation_id')"
seq="$(ss_json_get_file "$file" '@.snapshot_seq')"
[ "$schema_name" = 'safeshield.statistics' ] || return 1
[ "$schema_version" = '3' ] || return 1
[ -n "$generation" ] || return 1
is_valid_integer "$seq" || return 1
[ "$seq" -gt 0 ] 2>/dev/null
}
upload_pending_create() {
local source mode source_generation source_seq full_generation full_seq
local tmp_payload tmp_meta
upload_state_load
upload_source_is_v3 "$SS_STATISTICS_JSON_FILE" || return 1
full_generation="$(ss_json_get_file "$SS_STATISTICS_JSON_FILE" '@.generation_id')"
full_seq="$(ss_json_get_file "$SS_STATISTICS_JSON_FILE" '@.snapshot_seq')"
if [ "$upload_state_generation" != "$full_generation" ]; then
upload_state_generation="$full_generation"
upload_state_last_success_seq=0
upload_state_last_full_seq=0
upload_state_recent_successes=0
upload_state_needs_full=1
upload_state_save >/dev/null 2>&1 || true
fi
mode='recent'
source="$SS_STATISTICS_UPLOAD_JSON_FILE"
if [ "$upload_state_needs_full" = '1' ] ||
[ "$upload_state_last_full_seq" -le 0 ] 2>/dev/null ||
[ "$upload_state_recent_successes" -ge "$SS_STATS_UPLOAD_FULL_EVERY" ] 2>/dev/null; then
mode='full'
source="$SS_STATISTICS_JSON_FILE"
elif ! upload_source_is_v3 "$source"; then
mode='full'
source="$SS_STATISTICS_JSON_FILE"
fi
tmp_payload="${SS_STATISTICS_UPLOAD_PENDING_FILE}.tmp.$$"
tmp_meta="${SS_STATISTICS_UPLOAD_PENDING_META_FILE}.tmp.$$"
mkdir -p "${SS_STATISTICS_UPLOAD_PENDING_FILE%/*}" || return 1
cp "$source" "$tmp_payload" || {
rm -f "$tmp_payload"
return 1
}
upload_source_is_v3 "$tmp_payload" || {
rm -f "$tmp_payload"
return 1
}
source_generation="$(ss_json_get_file "$tmp_payload" '@.generation_id')"
source_seq="$(ss_json_get_file "$tmp_payload" '@.snapshot_seq')"
if [ "$source_generation" != "$full_generation" ]; then
rm -f "$tmp_payload"
return 1
fi
if [ "$mode" = 'recent' ] && [ "$source_seq" != "$full_seq" ]; then
# The collector publishes the full snapshot before its recent projection.
# Avoid promoting a previous projection while those two atomic writes cross.
rm -f "$tmp_payload"
return 1
fi
if [ "$source_seq" -le "$upload_state_last_success_seq" ] 2>/dev/null; then
rm -f "$tmp_payload"
return 2
fi
printf '%s\t%s\t%s\n' "$mode" "$source_generation" "$source_seq" >"$tmp_meta" || {
rm -f "$tmp_payload" "$tmp_meta"
return 1
}
mv -f "$tmp_payload" "$SS_STATISTICS_UPLOAD_PENDING_FILE" || {
rm -f "$tmp_payload" "$tmp_meta"
return 1
}
mv -f "$tmp_meta" "$SS_STATISTICS_UPLOAD_PENDING_META_FILE" || {
rm -f "$tmp_meta"
upload_pending_clear
return 1
}
upload_pending_mode="$mode"
upload_pending_generation="$source_generation"
upload_pending_seq="$source_seq"
return 0
}
upload_ensure_credentials() {
ss_statistics_upload_denied && return 2
ss_statistics_load_upload_credentials && return 0
log_info 'Refreshing Hub statistics upload credentials'
ss_statistics_refresh_upload_credentials || return $?
ss_statistics_load_upload_credentials
}
upload_response_valid() {
local response="$1"
local status generation server_seq
[ -s "$response" ] || return 1
status="$(ss_json_get_file "$response" '@.status')"
generation="$(ss_json_get_file "$response" '@.generation_id')"
server_seq="$(ss_json_get_file "$response" '@.last_snapshot_seq')"
case "$status" in
applied | duplicate | stale) ;;
*) return 1 ;;
esac
[ "$generation" = "$upload_pending_generation" ] || return 1
is_valid_integer "$server_seq" || return 1
[ "$server_seq" -ge "$upload_pending_seq" ] 2>/dev/null || return 1
SS_STATS_UPLOAD_RESPONSE_STATUS="$status"
SS_STATS_UPLOAD_RESPONSE_SEQ="$server_seq"
return 0
}
upload_record_success() {
local result_status="$1"
local server_seq="$2"
upload_state_load
if [ "$upload_state_generation" != "$upload_pending_generation" ]; then
upload_state_generation="$upload_pending_generation"
upload_state_last_success_seq=0
upload_state_last_full_seq=0
upload_state_recent_successes=0
upload_state_needs_full=1
fi
if [ "$server_seq" -gt "$upload_state_last_success_seq" ] 2>/dev/null; then
upload_state_last_success_seq="$server_seq"
fi
if [ "$upload_pending_mode" = 'full' ] && [ "$result_status" != 'stale' ]; then
upload_state_last_full_seq="$upload_pending_seq"
upload_state_recent_successes=0
upload_state_needs_full=0
else
if [ "$result_status" = 'stale' ]; then
upload_state_needs_full=1
elif [ "$upload_state_needs_full" = '0' ]; then
upload_state_recent_successes=$((upload_state_recent_successes + 1))
fi
fi
upload_state_save >/dev/null 2>&1 || true
}
upload_record_failure() {
upload_mark_needs_full "$upload_pending_generation"
}
upload_send_pending() {
local retries reauthenticated=0 response_status response_seq credentials_rc=0 refresh_rc=0
upload_pending_load || return 1
upload_ensure_credentials || credentials_rc=$?
case "$credentials_rc" in
0) ;;
2)
upload_pending_clear
return 2
;;
*)
upload_record_failure 'statistics_credentials_unavailable'
return 1
;;
esac
retries=1
while [ "$retries" -le "${ss_download_retry:-3}" ]; do
[ "$ss_should_terminate" -eq 1 ] && return 130
rm -f "$SS_STATS_UPLOAD_RESPONSE_FILE"
if ss_http_post_json \
"$SS_STATISTICS_UPLOAD_URL" \
"$SS_STATISTICS_UPLOAD_PENDING_FILE" \
"$SS_STATS_UPLOAD_RESPONSE_FILE" \
"Bearer $SS_STATISTICS_UPLOAD_TOKEN"; then
if upload_response_valid "$SS_STATS_UPLOAD_RESPONSE_FILE"; then
response_status="$SS_STATS_UPLOAD_RESPONSE_STATUS"
response_seq="$SS_STATS_UPLOAD_RESPONSE_SEQ"
upload_record_success "$response_status" "$response_seq"
log_info "Uploaded SafeShield statistics (${upload_pending_mode}, seq=${upload_pending_seq}, status=${response_status})"
upload_pending_clear
rm -f "$SS_STATS_UPLOAD_RESPONSE_FILE"
return 0
fi
upload_record_failure 'statistics_upload_invalid_response' '200'
log_warn 'Hub statistics upload returned an invalid success response; preserving pending payload for retry'
rm -f "$SS_STATS_UPLOAD_RESPONSE_FILE"
return 1
fi
case "${SS_HTTP_STATUS:-}" in
401)
if [ "$reauthenticated" -eq 0 ]; then
refresh_rc=0
reauthenticated=1
ss_statistics_clear_upload_credentials
ss_statistics_refresh_upload_credentials || refresh_rc=$?
if [ "$refresh_rc" -eq 0 ] && ss_statistics_load_upload_credentials; then
continue
fi
if [ "$refresh_rc" -eq 2 ]; then
log_info 'Cloud statistics upload is no longer enabled for the current license'
upload_pending_clear
return 2
fi
fi
upload_record_failure 'statistics_upload_auth_failed' '401'
log_warn 'Hub rejected statistics upload credentials'
return 1
;;
400 | 409 | 413)
upload_record_failure "statistics_upload_http_${SS_HTTP_STATUS}" "$SS_HTTP_STATUS"
log_error "Hub rejected statistics payload (HTTP ${SS_HTTP_STATUS}); discarding this snapshot and scheduling full reconciliation"
upload_pending_clear
rm -f "$SS_STATS_UPLOAD_RESPONSE_FILE"
return 2
;;
esac
upload_record_failure 'statistics_upload_failed' "${SS_HTTP_STATUS:-0}"
retries=$((retries + 1))
if [ "$retries" -le "${ss_download_retry:-3}" ]; then
upload_sleep "$SS_STATS_UPLOAD_RETRY_DELAY_S" || return 130
fi
done
log_warn "Statistics upload failed after ${ss_download_retry:-3} attempt(s); pending payload will be retried"
return 1
}
main() {
local create_rc=0 wait_s entitlement_rc
ss_load_config || return 1
[ "${ss_enabled}" = '1' ] || return 0
[ "${ss_statistics_enabled}" = '1' ] || return 0
mkdir -p "$SS_STATISTICS_DIR" || return 1
is_valid_integer "$SS_STATS_UPLOAD_INTERVAL_S" || SS_STATS_UPLOAD_INTERVAL_S=1800
[ "$SS_STATS_UPLOAD_INTERVAL_S" -ge 60 ] 2>/dev/null || SS_STATS_UPLOAD_INTERVAL_S=1800
is_valid_integer "$SS_STATS_UPLOAD_IDLE_WAIT_S" || SS_STATS_UPLOAD_IDLE_WAIT_S=15
[ "$SS_STATS_UPLOAD_IDLE_WAIT_S" -ge 1 ] 2>/dev/null || SS_STATS_UPLOAD_IDLE_WAIT_S=15
is_valid_integer "$SS_STATS_UPLOAD_RETRY_DELAY_S" || SS_STATS_UPLOAD_RETRY_DELAY_S=2
is_valid_integer "$SS_STATS_UPLOAD_FULL_EVERY" || SS_STATS_UPLOAD_FULL_EVERY=23
[ "$SS_STATS_UPLOAD_FULL_EVERY" -ge 1 ] 2>/dev/null || SS_STATS_UPLOAD_FULL_EVERY=23
is_valid_integer "$SS_STATS_UPLOAD_DENIED_RECHECK_S" || SS_STATS_UPLOAD_DENIED_RECHECK_S=43200
[ "$SS_STATS_UPLOAD_DENIED_RECHECK_S" -ge 60 ] 2>/dev/null || SS_STATS_UPLOAD_DENIED_RECHECK_S=43200
upload_state_load
while [ "$ss_should_terminate" -eq 0 ]; do
ss_load_config || {
upload_sleep "$SS_STATS_UPLOAD_IDLE_WAIT_S" || break
continue
}
if [ "${ss_enabled}" != '1' ] || [ "${ss_statistics_enabled}" != '1' ]; then
break
fi
wait_s="$SS_STATS_UPLOAD_INTERVAL_S"
if [ -z "${ss_license_key:-}" ]; then
ss_statistics_upload_denied || ss_statistics_disable_cloud_upload >/dev/null 2>&1 || true
fi
if ss_statistics_upload_denied; then
upload_pending_clear
if [ -n "${ss_license_key:-}" ] && ss_statistics_upload_denied_recheck_due "$SS_STATS_UPLOAD_DENIED_RECHECK_S"; then
log_info 'Rechecking Hub statistics upload entitlement'
# Stamp this attempt before network I/O so a failed resolve cannot
# turn into a routine upload retry loop while entitlement is denied.
ss_statistics_set_upload_entitlement denied >/dev/null 2>&1 || true
entitlement_rc=0
ss_statistics_refresh_upload_credentials || entitlement_rc=$?
case "$entitlement_rc" in
0) ;;
2)
upload_sleep "$wait_s" || break
continue
;;
*)
log_warn 'Hub statistics entitlement recheck failed; keeping cloud upload disabled'
upload_sleep "$wait_s" || break
continue
;;
esac
fi
if ss_statistics_upload_denied; then
upload_sleep "$wait_s" || break
continue
fi
fi
if upload_pending_load; then
upload_send_pending || true
else
create_rc=0
upload_pending_create || create_rc=$?
case "$create_rc" in
0)
upload_send_pending || true
;;
1)
wait_s="$SS_STATS_UPLOAD_IDLE_WAIT_S"
;;
2)
# The collector has not produced a sequence newer than the last ACK yet.
;;
esac
fi
upload_sleep "$wait_s" || break
done
rm -f "$SS_STATS_UPLOAD_RESPONSE_FILE"
return 0
}
if [ "${SS_STATS_UPLOAD_LIBRARY_ONLY:-0}" != '1' ]; then
trap 'upload_handle_term' TERM INT
trap 'rm -f "$SS_STATS_UPLOAD_RESPONSE_FILE"' EXIT
main "$@"
fi
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/ucode
'use strict';
const ubus = require('ubus');
function clean(value) {
return replace(sprintf('%s', value || ''), /[\t\r\n]/g, '_');
}
function main() {
let conn = ubus.connect();
if (!conn) {
return;
}
let data = conn.call('dhcp', 'ipv6leases', {});
if (type(data) != 'object' || type(data.device) != 'object') {
conn.disconnect();
return;
}
for (let _, device in data.device) {
if (type(device) != 'object' || type(device.leases) != 'array') {
continue;
}
for (let lease in device.leases) {
if (type(lease) != 'object') {
continue;
}
let duid = lc(clean(lease.duid));
let hostname = clean(lease.hostname);
let addresses = lease['ipv6-addr'];
if (!duid || type(addresses) != 'array') {
continue;
}
for (let address in addresses) {
if (type(address) != 'object') {
continue;
}
let ip = clean(address.address);
if (ip) {
printf('%s\t%s\t%s\n', ip, duid, hostname);
}
}
}
}
conn.disconnect();
}
main();
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/ucode
'use strict';
let ubus = require('ubus').connect();
function token(value) {
let text = sprintf('%s', value == null ? '' : value);
return replace(text, /[\t\r\n]/g, '');
}
function uint_or_null(value) {
if (value == null) {
return null;
}
let number = int(value);
return (type(number) == 'int' && number >= 0) ? number : null;
}
function optional_uint(value) {
let number = uint_or_null(value);
return number == null ? 0 : number;
}
if (!ubus) {
exit(1);
}
let data = ubus.call('dnsmasq', 'safeshield_stats', {});
if (type(data) != 'object' || uint_or_null(data.schema) != 1) {
exit(1);
}
let instance_id = token(data.instance_id);
if (!instance_id || type(data.totals) != 'object' || type(data.clients) != 'array') {
exit(1);
}
let total_queries = uint_or_null(data.totals.queries);
let total_blocked = uint_or_null(data.totals.blocked);
if (total_queries == null || total_blocked == null) {
exit(1);
}
let transport_scope = token(data.transport_scope || 'unknown');
let client_capacity = optional_uint(data.client_capacity);
let tracked_clients = optional_uint(data.tracked_clients);
let untracked_queries = optional_uint(data.untracked_queries);
let untracked_blocked = optional_uint(data.untracked_blocked);
let normalized_clients = [];
for (let client in data.clients) {
if (type(client) != 'object') {
exit(1);
}
let address = token(client.address);
let queries = uint_or_null(client.queries);
let blocked = uint_or_null(client.blocked);
if (!address || queries == null || blocked == null) {
exit(1);
}
push(normalized_clients, {
address: address,
queries: queries,
blocked: blocked
});
}
printf('snapshot\t%s\t%s\t%d\t%d\t%d\t%d\t%d\t%d\n',
instance_id,
transport_scope,
client_capacity,
tracked_clients,
untracked_queries,
untracked_blocked,
total_queries,
total_blocked);
for (let client in normalized_clients) {
printf('client\t%s\t%d\t%d\n',
client.address,
client.queries,
client.blocked);
}
print('commit\n');
exit(0);
+283
View File
@@ -0,0 +1,283 @@
#!/bin/sh
# shellcheck shell=ash
# Variables are populated by sourced SafeShield helpers and ss_load_config().
# shellcheck disable=SC2154
LC_ALL=C
export LC_ALL
umask 077
SS_STATSD_FUNCTIONS_LIB="${SS_STATSD_FUNCTIONS_LIB:-/lib/functions.sh}"
SS_STATSD_CORE_LIB="${SS_STATSD_CORE_LIB:-/usr/lib/safeshield/core.sh}"
SS_STATSD_AWK_DIR="${SS_STATSD_AWK_DIR:-/usr/lib/safeshield/statistics}"
SS_STATSD_POLL_COMMAND="${SS_STATSD_POLL_COMMAND:-/usr/libexec/safeshield-stats-poll}"
SS_STATSD_IDENTITY_COMMAND="${SS_STATSD_IDENTITY_COMMAND:-/usr/libexec/safeshield-stats-identities}"
SS_STATSD_PERSISTENT_STATE_FILE="${SS_STATSD_PERSISTENT_STATE_FILE:-/etc/safeshield/statistics-state.tsv}"
SS_STATSD_PERSISTENT_JOURNAL_FILE="${SS_STATSD_PERSISTENT_JOURNAL_FILE:-/etc/safeshield/statistics-journal.tsv}"
SS_STATSD_SEQUENCE_FILE="${SS_STATSD_SEQUENCE_FILE:-/etc/safeshield/statistics-sequence.tsv}"
SS_STATSD_SEQUENCE_RESERVATION_SIZE="${SS_STATSD_SEQUENCE_RESERVATION_SIZE:-4096}"
SS_STATSD_UPLOAD_WINDOW_HOURS="${SS_STATSD_UPLOAD_WINDOW_HOURS:-2}"
SS_STATSD_PERSISTENT_INTERVAL_S="${SS_STATSD_PERSISTENT_INTERVAL_S:-3600}"
SS_STATSD_PERSISTENT_COMPACT_INTERVAL_S="${SS_STATSD_PERSISTENT_COMPACT_INTERVAL_S:-604800}"
# shellcheck disable=SC1090
. "$SS_STATSD_FUNCTIONS_LIB"
# shellcheck disable=SC1090
. "$SS_STATSD_CORE_LIB"
stats_poller_pid=''
stats_awk_pid=''
stats_fifo=''
stats_lock_dir=''
stats_lock_owned=0
stats_new_generation_id() {
local generation_id="${SS_STATSD_GENERATION_ID:-}"
if [ -n "$generation_id" ]; then
printf '%s\n' "$generation_id"
return 0
fi
if [ -r /proc/sys/kernel/random/uuid ]; then
IFS= read -r generation_id </proc/sys/kernel/random/uuid || generation_id=''
fi
if [ -z "$generation_id" ]; then
generation_id="fallback-$(date +%s 2>/dev/null || printf '0')-$$"
fi
printf '%s\n' "$generation_id"
}
stats_process_running() {
local pid="${1:-}"
[ -n "$pid" ] || return 1
kill -0 "$pid" 2>/dev/null
}
stats_cleanup() {
trap - EXIT HUP INT TERM
# Stop the low-frequency UBus poller first. awk then receives EOF and runs
# its END block, which performs the final snapshot and persistence flush.
if stats_process_running "$stats_poller_pid"; then
kill "$stats_poller_pid" 2>/dev/null || true
fi
[ -n "$stats_poller_pid" ] && wait "$stats_poller_pid" 2>/dev/null || true
stats_poller_pid=''
[ -n "$stats_awk_pid" ] && wait "$stats_awk_pid" 2>/dev/null || true
stats_awk_pid=''
[ -n "$stats_fifo" ] && rm -f "$stats_fifo"
if [ "$stats_lock_owned" -eq 1 ] && [ -n "$stats_lock_dir" ]; then
rm -rf "$stats_lock_dir"
fi
}
stats_terminate() {
local rc="${1:-0}"
stats_cleanup
exit "$rc"
}
stats_acquire_lock() {
local owner_pid=''
stats_lock_dir="${SS_STATISTICS_DIR}/collector.lock"
if mkdir "$stats_lock_dir" 2>/dev/null; then
stats_lock_owned=1
printf '%s\n' "$$" >"${stats_lock_dir}/pid"
return 0
fi
if [ -r "${stats_lock_dir}/pid" ]; then
owner_pid="$(cat "${stats_lock_dir}/pid" 2>/dev/null || true)"
fi
if stats_process_running "$owner_pid"; then
log_warn "Statistics collector is already running (pid=${owner_pid})"
return 1
fi
log_warn 'Removing stale statistics collector lock'
rm -rf "$stats_lock_dir" || return 1
mkdir "$stats_lock_dir" || return 1
stats_lock_owned=1
printf '%s\n' "$$" >"${stats_lock_dir}/pid"
return 0
}
stats_poll_loop() {
local snapshot_interval="$1"
local poll_command="$2"
local sleep_pid=''
trap 'if [ -n "$sleep_pid" ]; then kill "$sleep_pid" 2>/dev/null || true; fi; exit 0' HUP INT TERM
while :; do
if ! "$poll_command"; then
printf 'error\tdnsmasq_ubus_poll_failed\n'
fi
sleep "$snapshot_interval" &
sleep_pid=$!
wait "$sleep_pid" 2>/dev/null || true
sleep_pid=''
done
}
stats_run_collector() {
local snapshot_interval="$1"
local retention_hours="$2"
local lease_file="$3"
local persistent_state_file="$4"
local persistent_journal_file="$5"
local sequence_file="$6"
local generation_id ipv6_neigh_command='' ipv6_identity_command='' ipv6_identity_file="${SS_STATSD_IPV6_IDENTITY_FILE:-}" force_rebaseline=0 rc=0
if [ -f "${SS_STATISTICS_REBASELINE_FILE}" ]; then
force_rebaseline=1
fi
# This is only a candidate for a brand-new dataset. The collector modules keep
# the generation restored from tmpfs or persistent state when one already exists.
generation_id="$(stats_new_generation_id)"
if command_exists ip; then
ipv6_neigh_command='ip -6 neigh show 2>/dev/null'
fi
if [ -z "$ipv6_identity_file" ] && [ -x "$SS_STATSD_IDENTITY_COMMAND" ]; then
ipv6_identity_command="$SS_STATSD_IDENTITY_COMMAND 2>/dev/null"
fi
stats_fifo="${SS_STATISTICS_DIR}/events.$$"
rm -f "$stats_fifo"
mkfifo "$stats_fifo" || {
log_error "Failed to create statistics FIFO: ${stats_fifo}"
return 1
}
stats_poll_loop "$snapshot_interval" "$SS_STATSD_POLL_COMMAND" >"$stats_fifo" &
stats_poller_pid=$!
awk \
-v state_file="${SS_STATISTICS_STATE_FILE}" \
-v json_file="${SS_STATISTICS_JSON_FILE}" \
-v upload_json_file="${SS_STATISTICS_UPLOAD_JSON_FILE}" \
-v upload_window_hours="${SS_STATSD_UPLOAD_WINDOW_HOURS}" \
-v persistent_state_file="${persistent_state_file}" \
-v persistent_journal_file="${persistent_journal_file}" \
-v sequence_file="${sequence_file}" \
-v snapshot_seq_reservation_size="${SS_STATSD_SEQUENCE_RESERVATION_SIZE}" \
-v persistent_interval="${SS_STATSD_PERSISTENT_INTERVAL_S}" \
-v persistent_compact_interval="${SS_STATSD_PERSISTENT_COMPACT_INTERVAL_S}" \
-v snapshot_interval="${snapshot_interval}" \
-v retention_hours="${retention_hours}" \
-v lease_file="${lease_file}" \
-v ipv6_neigh_command="${ipv6_neigh_command}" \
-v ipv6_identity_file="${ipv6_identity_file}" \
-v ipv6_identity_command="${ipv6_identity_command}" \
-v lease_refresh_interval=60 \
-v identity_cache_ttl=60 \
-v max_devices=128 \
-v generation_seed="${generation_id}" \
-v force_rebaseline="${force_rebaseline}" \
-v rebaseline_file="${SS_STATISTICS_REBASELINE_FILE}" \
-f "$SS_STATSD_AWK_DIR/00-common.awk" \
-f "$SS_STATSD_AWK_DIR/10-recovery.awk" \
-f "$SS_STATSD_AWK_DIR/20-identity.awk" \
-f "$SS_STATSD_AWK_DIR/30-aggregate.awk" \
-f "$SS_STATSD_AWK_DIR/40-persistence.awk" \
-f "$SS_STATSD_AWK_DIR/50-output.awk" \
-f "$SS_STATSD_AWK_DIR/90-main.awk" \
<"$stats_fifo" &
stats_awk_pid=$!
wait "$stats_awk_pid" || rc=$?
stats_awk_pid=''
if stats_process_running "$stats_poller_pid"; then
kill "$stats_poller_pid" 2>/dev/null || true
fi
wait "$stats_poller_pid" 2>/dev/null || true
stats_poller_pid=''
return "$rc"
}
main() {
local snapshot_interval configured_snapshot_interval retention_hours lease_file configured_lease_file
local persistent_state_file persistent_journal_file sequence_file persistence_mode rc=0
ss_load_config || return 1
[ "${ss_enabled}" = "1" ] || return 0
[ "${ss_statistics_enabled}" = "1" ] || return 0
if ! ss_dnsmasq_safeshield_block_supported; then
log_warn 'SafeShield statistics collector is disabled because dnsmasq does not provide the SafeShield extension'
return 0
fi
[ -x "$SS_STATSD_POLL_COMMAND" ] || {
log_error "SafeShield statistics poll command is unavailable: ${SS_STATSD_POLL_COMMAND}"
return 1
}
command_exists mkfifo || {
log_error 'mkfifo is required for SafeShield statistics'
return 1
}
persistent_state_file="${SS_STATSD_PERSISTENT_STATE_FILE}"
persistent_journal_file="${SS_STATSD_PERSISTENT_JOURNAL_FILE}"
sequence_file="${SS_STATSD_SEQUENCE_FILE}"
persistence_mode='journal'
if ! ss_statistics_persistence_enabled; then
persistent_state_file=''
persistent_journal_file=''
sequence_file=''
persistence_mode='volatile'
fi
mkdir -p "${SS_STATISTICS_DIR}" || return 1
if [ -n "$persistent_state_file" ]; then
mkdir -p "${persistent_state_file%/*}" || return 1
fi
if [ -n "$persistent_journal_file" ]; then
mkdir -p "${persistent_journal_file%/*}" || return 1
fi
if [ -n "$sequence_file" ]; then
mkdir -p "${sequence_file%/*}" || return 1
fi
stats_acquire_lock || return 0
trap 'stats_cleanup' EXIT
trap 'stats_terminate 129' HUP
trap 'stats_terminate 130' INT
trap 'stats_terminate 143' TERM
configured_snapshot_interval="${ss_statistics_snapshot_interval_s}"
snapshot_interval="$(ss_statistics_effective_snapshot_interval "$configured_snapshot_interval")"
retention_hours="${ss_statistics_retention_hours}"
lease_file="${SS_STATSD_DHCP_LEASE_FILE:-/tmp/dhcp.leases}"
if [ -z "${SS_STATSD_DHCP_LEASE_FILE:-}" ] && command_exists uci; then
configured_lease_file="$(uci -q get dhcp.@dnsmasq[0].leasefile 2>/dev/null || true)"
[ -n "$configured_lease_file" ] && lease_file="$configured_lease_file"
fi
log_info "Starting statistics collector (source=dnsmasq-ubus snapshot=${snapshot_interval}s configured_snapshot=${configured_snapshot_interval}s retention=${retention_hours}h persistence=${persistence_mode} journal=${SS_STATSD_PERSISTENT_INTERVAL_S}s compact=${SS_STATSD_PERSISTENT_COMPACT_INTERVAL_S}s)"
stats_run_collector \
"$snapshot_interval" \
"$retention_hours" \
"$lease_file" \
"$persistent_state_file" \
"$persistent_journal_file" \
"$sequence_file" || rc=$?
return "$rc"
}
main "$@"