mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 09:54:06 +08:00
🎉 Sync 2026-11-02 12:48:14
This commit is contained in:
1 parent
42d5cbfa43
commit
db798bdf5e
53 files changed
+1895
-568
No files matched your search
@@ -8,12 +8,12 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=apfree-wifidog
|
PKG_NAME:=apfree-wifidog
|
||||||
PKG_RELEASE:=9
|
PKG_RELEASE:=10
|
||||||
|
|
||||||
PKG_SOURCE_PROTO:=git
|
PKG_SOURCE_PROTO:=git
|
||||||
PKG_SOURCE_URL:=https://github.com/liudf0716/apfree-wifidog.git
|
PKG_SOURCE_URL:=https://github.com/liudf0716/apfree-wifidog.git
|
||||||
PKG_SOURCE_DATE:=2026-10-25
|
PKG_SOURCE_DATE:=2026-10-25
|
||||||
PKG_SOURCE_VERSION:=4075182ffedaf19b36926dc2f068fe413901aba3
|
PKG_SOURCE_VERSION:=9997baf26c737102940dd3a44e9d95c84dd57dab
|
||||||
PKG_MIRROR_HASH:=skip
|
PKG_MIRROR_HASH:=skip
|
||||||
|
|
||||||
PKG_MAINTAINER:=Dengfeng Liu <liudf0716@gmail.com>
|
PKG_MAINTAINER:=Dengfeng Liu <liudf0716@gmail.com>
|
||||||
|
|||||||
+13
-5
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=dockerd
|
PKG_NAME:=dockerd
|
||||||
PKG_VERSION:=29.6.1
|
PKG_VERSION:=29.6.1
|
||||||
PKG_RELEASE:=10
|
PKG_RELEASE:=11
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_LICENSE_FILES:=LICENSE
|
PKG_LICENSE_FILES:=LICENSE
|
||||||
PKG_CPE_ID:=cpe:/a:mobyproject:moby
|
PKG_CPE_ID:=cpe:/a:mobyproject:moby
|
||||||
@@ -138,10 +138,18 @@ define Package/dockerd/install
|
|||||||
$(INSTALL_DIR) $(1)/etc/config
|
$(INSTALL_DIR) $(1)/etc/config
|
||||||
$(INSTALL_CONF) ./files/etc/config/dockerd $(1)/etc/config/dockerd
|
$(INSTALL_CONF) ./files/etc/config/dockerd $(1)/etc/config/dockerd
|
||||||
|
|
||||||
# Must be after systcl 11-br-netfilter.conf from kmod-br-netfilter
|
$(INSTALL_DIR) $(1)/etc/hotplug.d/net
|
||||||
$(INSTALL_DIR) $(1)/etc/sysctl.d
|
$(INSTALL_DATA) ./files/etc/hotplug.d/net/br-netfilter \
|
||||||
$(INSTALL_DATA) ./files/etc/sysctl.d/sysctl-br-netfilter-ip.conf \
|
$(1)/etc/hotplug.d/net/10-docker-br-netfilter
|
||||||
$(1)/etc/sysctl.d/12-br-netfilter-ip.conf
|
|
||||||
|
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||||
|
$(INSTALL_DATA) ./files/etc/uci-defaults/16_docker-netifd-migrate2 \
|
||||||
|
$(1)/etc/uci-defaults/16_docker-netifd-migrate2
|
||||||
|
$(INSTALL_DATA) ./files/etc/uci-defaults/17_docker-fw4 \
|
||||||
|
$(1)/etc/uci-defaults/17_docker-fw4
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/lib/fstab.d
|
||||||
|
$(INSTALL_DATA) ./files/lib/fstab.d/dockerd.sh $(1)/lib/fstab.d/dockerd.sh
|
||||||
endef
|
endef
|
||||||
|
|
||||||
define Package/dockerd/postinst
|
define Package/dockerd/postinst
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-passwall
|
PKG_NAME:=luci-app-passwall
|
||||||
PKG_VERSION:=26.10.29
|
PKG_VERSION:=26.10.29
|
||||||
PKG_RELEASE:=362
|
PKG_RELEASE:=363
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
@@ -149,6 +149,13 @@ local com = self.com
|
|||||||
|
|
||||||
function doUpdate(btn, app) {
|
function doUpdate(btn, app) {
|
||||||
if (luciUpdating || (app === appname && componentUpdates > 0)) return;
|
if (luciUpdating || (app === appname && componentUpdates > 0)) return;
|
||||||
|
var appInfo = appInfoList[app];
|
||||||
|
if (!appInfo || !appInfo.data || !appInfo.data.browser_download_url ||
|
||||||
|
(app === appname && (!appInfo.i18n || !appInfo.i18n.browser_download_url))) {
|
||||||
|
onRequestError(btn, updateFailedText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (app === appname) luciUpdating = true;
|
if (app === appname) luciUpdating = true;
|
||||||
else componentUpdates++;
|
else componentUpdates++;
|
||||||
setButtonDisabled(btn, true);
|
setButtonDisabled(btn, true);
|
||||||
@@ -157,7 +164,6 @@ local com = self.com
|
|||||||
addPageNotice();
|
addPageNotice();
|
||||||
|
|
||||||
var appUpdateUrl = '<%=api.url("update_")%>' + app;
|
var appUpdateUrl = '<%=api.url("update_")%>' + app;
|
||||||
var appInfo = appInfoList[app];
|
|
||||||
var downloadId = Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
|
var downloadId = Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
|
||||||
var totalSize = appInfo && appInfo.data ? Number(appInfo.data.size) : 0;
|
var totalSize = appInfo && appInfo.data ? Number(appInfo.data.size) : 0;
|
||||||
if (app === appname) totalSize += Number(appInfo.i18n.size) || 0;
|
if (app === appname) totalSize += Number(appInfo.i18n.size) || 0;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-passwall2
|
PKG_NAME:=luci-app-passwall2
|
||||||
PKG_VERSION:=26.10.29
|
PKG_VERSION:=26.10.29
|
||||||
PKG_RELEASE:=163
|
PKG_RELEASE:=164
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
@@ -149,6 +149,20 @@ local com = self.com
|
|||||||
|
|
||||||
function doUpdate(btn, app) {
|
function doUpdate(btn, app) {
|
||||||
if (luciUpdating || (app === appname && componentUpdates > 0)) return;
|
if (luciUpdating || (app === appname && componentUpdates > 0)) return;
|
||||||
|
var appInfo = appInfoList[app];
|
||||||
|
if (!appInfo || !appInfo.data) {
|
||||||
|
onRequestError(btn, updateFailedText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var downloadList = [appInfo.data];
|
||||||
|
if (app === appname && Array.isArray(appInfo.i18n)) downloadList = downloadList.concat(appInfo.i18n);
|
||||||
|
for (var i = 0; i < downloadList.length; i++) {
|
||||||
|
if (!downloadList[i] || !downloadList[i].browser_download_url) {
|
||||||
|
onRequestError(btn, updateFailedText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (app === appname) luciUpdating = true;
|
if (app === appname) luciUpdating = true;
|
||||||
else componentUpdates++;
|
else componentUpdates++;
|
||||||
setButtonDisabled(btn, true);
|
setButtonDisabled(btn, true);
|
||||||
@@ -157,10 +171,7 @@ local com = self.com
|
|||||||
addPageNotice();
|
addPageNotice();
|
||||||
|
|
||||||
var appUpdateUrl = '<%=api.url("update_")%>' + app;
|
var appUpdateUrl = '<%=api.url("update_")%>' + app;
|
||||||
var appInfo = appInfoList[app];
|
|
||||||
var downloadId = Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
|
var downloadId = Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
|
||||||
var downloadList = appInfo && appInfo.data ? [appInfo.data] : [];
|
|
||||||
if (app === appname && Array.isArray(appInfo.i18n)) downloadList = downloadList.concat(appInfo.i18n);
|
|
||||||
var totalSize = 0;
|
var totalSize = 0;
|
||||||
for (var i = 0; i < downloadList.length; i++) totalSize += Number(downloadList[i].size) || 0;
|
for (var i = 0; i < downloadList.length; i++) totalSize += Number(downloadList[i].size) || 0;
|
||||||
if (!isFinite(totalSize) || totalSize <= 0) {
|
if (!isFinite(totalSize) || totalSize <= 0) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-trafficctl
|
PKG_NAME:=luci-app-trafficctl
|
||||||
PKG_VERSION:=1.25.2
|
PKG_VERSION:=1.25.4
|
||||||
PKG_RELEASE:=18
|
PKG_RELEASE:=19
|
||||||
|
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_MAINTAINER:=Denis Iusupov <yusdyr@gmail.com>
|
PKG_MAINTAINER:=Denis Iusupov <yusdyr@gmail.com>
|
||||||
|
|||||||
@@ -41,9 +41,23 @@ fi
|
|||||||
|
|
||||||
# Restore blocks and ratelimits (if persist enabled)
|
# Restore blocks and ratelimits (if persist enabled)
|
||||||
if [ -f "$RULES_FILE" ] && tctl_persist_enabled; then
|
if [ -f "$RULES_FILE" ] && tctl_persist_enabled; then
|
||||||
jsonfilter -i "$RULES_FILE" -e '@[*]' 2>/dev/null | while read -r _; do true; done
|
# How many records there are comes from the file, not from a constant.
|
||||||
|
# `seq 0 99` stopped at the hundredth, so every rule past it was silently
|
||||||
for idx in $(seq 0 99); do
|
# not restored: present in rules.json, shown by the UI as persisted, and
|
||||||
|
# absent from the kernel after a reboot (#108). Raising 99 to a bigger
|
||||||
|
# number would only move the cliff. The absent-record sentinel below is
|
||||||
|
# what ends the loop, exactly as it did before.
|
||||||
|
#
|
||||||
|
# (The line that used to sit here piped '@[*]' into a loop whose body was
|
||||||
|
# `true` — an abandoned attempt at this same iteration that did nothing.)
|
||||||
|
# Advanced at the TOP, so there is exactly one increment and `continue`
|
||||||
|
# below cannot skip it. With the increment at the bottom, a record that
|
||||||
|
# has a type but no ip — a truncated or hand-edited file — would spin the
|
||||||
|
# loop forever on the same index, inside a boot hook. That is a worse
|
||||||
|
# failure than the one being fixed here.
|
||||||
|
idx=-1
|
||||||
|
while :; do
|
||||||
|
idx=$((idx + 1))
|
||||||
type=$(jsonfilter -i "$RULES_FILE" -e "@[$idx].type" 2>/dev/null)
|
type=$(jsonfilter -i "$RULES_FILE" -e "@[$idx].type" 2>/dev/null)
|
||||||
[ -z "$type" ] && break
|
[ -z "$type" ] && break
|
||||||
ip=$(jsonfilter -i "$RULES_FILE" -e "@[$idx].ip" 2>/dev/null)
|
ip=$(jsonfilter -i "$RULES_FILE" -e "@[$idx].ip" 2>/dev/null)
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ STAMP="/tmp/trafficctl_history.stamp"
|
|||||||
LOCKD="/tmp/trafficctl_history.lock.d"
|
LOCKD="/tmp/trafficctl_history.lock.d"
|
||||||
TOTALS_STATE="/tmp/trafficctl_totals.state"
|
TOTALS_STATE="/tmp/trafficctl_totals.state"
|
||||||
IFCURSOR="/tmp/trafficctl_history.ifcursor"
|
IFCURSOR="/tmp/trafficctl_history.ifcursor"
|
||||||
|
# Per-period, and suffixed with the period id so a period roll starts a fresh
|
||||||
|
# one instead of needing to be cleared. tmpfs: it is rebuilt from the durable
|
||||||
|
# file whenever it is missing, which is every boot.
|
||||||
|
IFACC="/tmp/trafficctl_history.ifacc"
|
||||||
|
|
||||||
# Row layout, tab-separated:
|
# Row layout, tab-separated:
|
||||||
# key mac ip name rx tx tcp udp first_seen last_seen degraded
|
# key mac ip name rx tx tcp udp first_seen last_seen degraded
|
||||||
@@ -628,15 +632,27 @@ hist_if_accuracy() {
|
|||||||
# time the link goes down, which on the multi-WAN box this feature is for is
|
# time the link goes down, which on the multi-WAN box this feature is for is
|
||||||
# routine — so a flap costs one sampling interval instead of corrupting the
|
# routine — so a flap costs one sampling interval instead of corrupting the
|
||||||
# month.
|
# month.
|
||||||
hist_if_flush() {
|
# Fold the growth since the last tick into the tmpfs accumulator and advance
|
||||||
local dir pid file tmp now sample rc=0
|
# the cursor. Called on every sample tick, because a cursor that only moved at
|
||||||
|
# flush time lost up to a whole flush interval of a link's traffic whenever its
|
||||||
|
# counter restarted — an hour by default, on exactly the multi-WAN box with a
|
||||||
|
# flapping link this series exists for (#111). The accumulator is in tmpfs, so
|
||||||
|
# touching it every 60s costs no flash.
|
||||||
|
#
|
||||||
|
# The durable file is written by hist_if_flush alone. The accumulator is seeded
|
||||||
|
# from it when absent (every boot, and on a period roll), which is what makes
|
||||||
|
# the eventual copy-over safe rather than double-counting.
|
||||||
|
hist_if_accumulate() {
|
||||||
|
local dir pid file tmp now sample acc rc=0
|
||||||
hist_ifaces_enabled || return 0
|
hist_ifaces_enabled || return 0
|
||||||
dir=$(hist_dir)
|
dir=$(hist_dir)
|
||||||
pid=$(hist_period_id)
|
pid=$(hist_period_id)
|
||||||
file=$(hist_iffile "$pid")
|
file=$(hist_iffile "$pid")
|
||||||
|
acc="$IFACC.$pid"
|
||||||
now=$(date +%s)
|
now=$(date +%s)
|
||||||
sample="/tmp/.trafficctl_history.ifsample.$$"
|
sample="/tmp/.trafficctl_history.ifsample.$$"
|
||||||
tmp="$dir/.history-if.$pid.$$"
|
tmp="$acc.$$"
|
||||||
|
[ -f "$acc" ] && file="$acc"
|
||||||
# shellcheck disable=SC2064
|
# shellcheck disable=SC2064
|
||||||
trap "rm -f '$sample' '$tmp'" EXIT INT TERM
|
trap "rm -f '$sample' '$tmp'" EXIT INT TERM
|
||||||
|
|
||||||
@@ -694,12 +710,24 @@ hist_if_flush() {
|
|||||||
lrx = num(line, "rx_bytes"); if (lrx < 0) continue
|
lrx = num(line, "rx_bytes"); if (lrx < 0) continue
|
||||||
ltx = num(line, "tx_bytes"); if (ltx < 0) continue
|
ltx = num(line, "tx_bytes"); if (ltx < 0) continue
|
||||||
|
|
||||||
|
# A live counter BELOW the cursor has restarted: the link went
|
||||||
|
# down and came back, or the driver reset it. The bytes carried
|
||||||
|
# since then have never been counted by anything, because a
|
||||||
|
# /proc/net/dev counter is monotonic for the lifetime of the
|
||||||
|
# interface, so they are the delta.
|
||||||
|
#
|
||||||
|
# Adding nothing here is right for the DEVICE series, where the
|
||||||
|
# live value is a conntrack sum that collapses when flows expire
|
||||||
|
# and those bytes were already accumulated while the flows lived.
|
||||||
|
# That premise does not transfer to an interface counter, and the
|
||||||
|
# zero came across with the code (#111). It also made this branch
|
||||||
|
# disagree with the unseen-interface case, which always took the
|
||||||
|
# full counter: the same situation, a counter starting from zero,
|
||||||
|
# handled two ways depending on whether the cursor happened to
|
||||||
|
# remember the interface.
|
||||||
drx = lrx; dtx = ltx
|
drx = lrx; dtx = ltx
|
||||||
if (seen[d]) {
|
if (lrx >= crx[d]) drx = lrx - crx[d]
|
||||||
drx = 0; dtx = 0
|
if (ltx >= ctx[d]) dtx = ltx - ctx[d]
|
||||||
if (lrx >= crx[d]) drx = lrx - crx[d]
|
|
||||||
if (ltx >= ctx[d]) dtx = ltx - ctx[d]
|
|
||||||
}
|
|
||||||
if (!(d in have)) {
|
if (!(d in have)) {
|
||||||
order[++norder] = d
|
order[++norder] = d
|
||||||
have[d] = 1
|
have[d] = 1
|
||||||
@@ -736,11 +764,32 @@ hist_if_flush() {
|
|||||||
}' </dev/null || rc=1
|
}' </dev/null || rc=1
|
||||||
|
|
||||||
[ "$rc" = 0 ] && [ -f "$tmp" ] || return 1
|
[ "$rc" = 0 ] && [ -f "$tmp" ] || return 1
|
||||||
hist_guard mv "$tmp" "$file" 2>/dev/null || return 1
|
mv "$tmp" "$acc" 2>/dev/null || return 1
|
||||||
|
# Only after the accumulator is in place, for the same reason the device
|
||||||
|
# cursor waits for its period file: advancing first and then failing would
|
||||||
|
# discard the growth for good, where this way it is folded in next tick.
|
||||||
mv "$IFCURSOR.new" "$IFCURSOR" 2>/dev/null || true
|
mv "$IFCURSOR.new" "$IFCURSOR" 2>/dev/null || true
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Publish the accumulator to the durable per-period file. Picks up the final
|
||||||
|
# interval first, so no traffic is stranded in tmpfs at flush time.
|
||||||
|
hist_if_flush() {
|
||||||
|
local pid acc file tmp
|
||||||
|
hist_ifaces_enabled || return 0
|
||||||
|
hist_if_accumulate || return 1
|
||||||
|
pid=$(hist_period_id)
|
||||||
|
acc="$IFACC.$pid"
|
||||||
|
[ -f "$acc" ] || return 0
|
||||||
|
file=$(hist_iffile "$pid")
|
||||||
|
tmp="$(hist_dir)/.history-if.$pid.$$"
|
||||||
|
# shellcheck disable=SC2064
|
||||||
|
trap "rm -f '$tmp'" EXIT INT TERM
|
||||||
|
hist_guard cp "$acc" "$tmp" 2>/dev/null || return 1
|
||||||
|
hist_guard mv "$tmp" "$file" 2>/dev/null || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
hist_if_read() {
|
hist_if_read() {
|
||||||
local dir from to files f p
|
local dir from to files f p
|
||||||
dir=$(hist_dir)
|
dir=$(hist_dir)
|
||||||
@@ -955,6 +1004,9 @@ hist_keeper() {
|
|||||||
while :; do
|
while :; do
|
||||||
if hist_enabled; then
|
if hist_enabled; then
|
||||||
/usr/local/bin/trafficctl-totals.sh --all >/dev/null 2>&1
|
/usr/local/bin/trafficctl-totals.sh --all >/dev/null 2>&1
|
||||||
|
# Both series advance their tmpfs cursor on every tick. Only the
|
||||||
|
# durable store waits for the flush interval below.
|
||||||
|
hist_if_accumulate >/dev/null 2>&1
|
||||||
now=$(date +%s)
|
now=$(date +%s)
|
||||||
interval=$(hist_interval)
|
interval=$(hist_interval)
|
||||||
# A clock that jumped backwards (NTP settling after boot, which is
|
# A clock that jumped backwards (NTP settling after boot, which is
|
||||||
@@ -977,6 +1029,10 @@ case "$1" in
|
|||||||
# and the thing that makes an hourly flush mean anything.
|
# and the thing that makes an hourly flush mean anything.
|
||||||
hist_enabled || { printf '{"ok":false,"reason":"disabled"}\n'; exit 0; }
|
hist_enabled || { printf '{"ok":false,"reason":"disabled"}\n'; exit 0; }
|
||||||
/usr/local/bin/trafficctl-totals.sh --all >/dev/null 2>&1
|
/usr/local/bin/trafficctl-totals.sh --all >/dev/null 2>&1
|
||||||
|
# The interface series advances its own tmpfs cursor on the same tick.
|
||||||
|
# Leaving it to the flush meant a counter that restarted in between
|
||||||
|
# lost everything since the last flush, not since the last tick (#111).
|
||||||
|
hist_if_accumulate >/dev/null 2>&1
|
||||||
printf '{"ok":true,"at":%s}\n' "$(date +%s)"
|
printf '{"ok":true,"at":%s}\n' "$(date +%s)"
|
||||||
;;
|
;;
|
||||||
flush) hist_flush ;;
|
flush) hist_flush ;;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI support for vless-core
|
||||||
|
LUCI_DEPENDS:=+luci-base +vless-core-cli
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
PKG_LICENSE:=GPL-2.0
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
# call BuildPackage - OpenWrt buildroot signature
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require form';
|
||||||
|
'require rpc';
|
||||||
|
|
||||||
|
var callServiceList = rpc.declare({
|
||||||
|
object: 'service',
|
||||||
|
method: 'list',
|
||||||
|
params: [ 'name' ],
|
||||||
|
expect: { '': {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
function isRunning(res) {
|
||||||
|
try {
|
||||||
|
return Object.keys(res['vless-core'].instances).length > 0;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
load: function() {
|
||||||
|
return L.resolveDefault(callServiceList('vless-core'), {});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(res) {
|
||||||
|
var m, s, o;
|
||||||
|
|
||||||
|
m = new form.Map('vless-core', _('VLESS Core'),
|
||||||
|
_('Standalone client for VLESS, Trojan, VMess, Shadowsocks and SOCKS5 upstreams with a local SOCKS5 proxy.') + '<br />' +
|
||||||
|
'<b>' + _('Status') + ':</b> ' +
|
||||||
|
(isRunning(res)
|
||||||
|
? '<span style="color:green">' + _('running') + '</span>'
|
||||||
|
: '<span style="color:red">' + _('stopped') + '</span>'));
|
||||||
|
|
||||||
|
s = m.section(form.NamedSection, 'main', 'vless-core');
|
||||||
|
s.addremove = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'uri', _('Upstream URI'),
|
||||||
|
_('<code>vless://</code>, <code>vmess://</code>, <code>trojan://</code>, ' +
|
||||||
|
'<code>ss://</code> or <code>socks5://</code> URI of the upstream server.'));
|
||||||
|
o.password = true;
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function(section_id, value) {
|
||||||
|
if (value && !/^(vless|vmess|trojan|ss|socks5):\/\//.test(value))
|
||||||
|
return _('Unsupported URI scheme');
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'listen_port', _('SOCKS5 port'),
|
||||||
|
_('Local SOCKS5 listener, binds to 127.0.0.1 only.'));
|
||||||
|
o.datatype = 'port';
|
||||||
|
o.placeholder = '1080';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'routing', _('Routing'),
|
||||||
|
_('Optional. Format: <code>ENABLED;DEFAULT;LAN;action,type,value;...</code><br />' +
|
||||||
|
'DEFAULT and action: <code>proxy</code>, <code>direct</code>, <code>block</code>. ' +
|
||||||
|
'Type: <code>domain</code>, <code>suffix</code>, <code>cidr</code>, <code>port</code>.<br />' +
|
||||||
|
'Example: <code>1;proxy;1;direct,suffix,ru;block,port,25</code>'));
|
||||||
|
o.optional = true;
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'tls_mode', _('Certificate verification'),
|
||||||
|
_('<code>strict</code>: verified TLS only (needs <code>ca-bundle</code> for security=tls). ' +
|
||||||
|
'<code>insecure</code>: never verify. ' +
|
||||||
|
'<code>tofu</code>: pin the certificate on first connection.'));
|
||||||
|
o.value('strict', _('strict'));
|
||||||
|
o.value('insecure', _('insecure'));
|
||||||
|
o.value('tofu', _('tofu'));
|
||||||
|
o.default = 'strict';
|
||||||
|
|
||||||
|
return m.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"admin/services/vless-core": {
|
||||||
|
"title": "VLESS Core",
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "vless-core"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": [ "luci-app-vless-core" ],
|
||||||
|
"uci": { "vless-core": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"luci-app-vless-core": {
|
||||||
|
"description": "Grant access to VLESS Core configuration",
|
||||||
|
"read": {
|
||||||
|
"ubus": { "service": [ "list" ] },
|
||||||
|
"uci": [ "vless-core" ]
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"uci": [ "vless-core" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -246,13 +246,23 @@ return view.extend({
|
|||||||
Delete — and LuCI's actions column does not wrap, so on a modem
|
Delete — and LuCI's actions column does not wrap, so on a modem
|
||||||
that offered them all the row ran off the right edge of the table
|
that offered them all the row ran off the right edge of the table
|
||||||
(field report with a screenshot, RUTM11). Everything from Tools on
|
(field report with a screenshot, RUTM11). Everything from Tools on
|
||||||
now lives in one dropdown (below), which leaves four. The wrapping
|
now lives in one dropdown (below), which leaves four, laid out
|
||||||
flex row stays as the backstop for a narrow column: the buttons
|
as a fixed two-by-two grid.
|
||||||
keep their theme styling and stay right-aligned, they just fold
|
|
||||||
onto a second line rather than overflowing. */
|
Not a wrapping flex row: a button that grows reflows one of
|
||||||
box.style.display = 'flex';
|
those, and a button DOES grow while its action runs: Config is
|
||||||
box.style.flexWrap = 'wrap';
|
a createHandlerFn button (form.js:3238), which marks it
|
||||||
box.style.justifyContent = 'flex-end';
|
`spinning` (ui.js:5683), and Footstrap draws that spinner as a
|
||||||
|
::before in front of the label (cascade.css:265). A click on
|
||||||
|
Config then moved every button in the cell for the moment
|
||||||
|
before the modal opened (luci-app-wwand#18). In a grid the
|
||||||
|
wider button only widens its column. Each keeps its own width
|
||||||
|
(justify-items), or Bootstrap's wide Actions dropdown would
|
||||||
|
stretch Config above it to the same size. */
|
||||||
|
box.style.display = 'grid';
|
||||||
|
box.style.gridTemplateColumns = 'repeat(2, auto)';
|
||||||
|
box.style.justifyContent = 'end';
|
||||||
|
box.style.justifyItems = 'end';
|
||||||
box.style.gap = '.25em';
|
box.style.gap = '.25em';
|
||||||
|
|
||||||
var jump = function(label, title, url) {
|
var jump = function(label, title, url) {
|
||||||
|
|||||||
@@ -548,6 +548,13 @@ return baseclass.extend({
|
|||||||
before it gets that far */
|
before it gets that far */
|
||||||
o = s.taboption(tab, form.Value, 'rat_order', _('RAT priority'),
|
o = s.taboption(tab, form.Value, 'rat_order', _('RAT priority'),
|
||||||
_('QMI and MBIM modems: the order in which the modem searches the radio technologies, most preferred first, e.g. <code>nr5g,lte</code>. wwand writes it at every start when the modem holds a different one. A technology missing from the list may never be searched for, even when "Radio technology" allows it. Empty = leave the modem\'s order unchanged. Saving a change restarts this modem\'s bring-up, so its connection drops briefly. The current order is shown, and can be changed live, under Modem Tools.'));
|
_('QMI and MBIM modems: the order in which the modem searches the radio technologies, most preferred first, e.g. <code>nr5g,lte</code>. wwand writes it at every start when the modem holds a different one. A technology missing from the list may never be searched for, even when "Radio technology" allows it. Empty = leave the modem\'s order unchanged. Saving a change restarts this modem\'s bring-up, so its connection drops briefly. The current order is shown, and can be changed live, under Modem Tools.'));
|
||||||
|
/* an explicit empty entry, like "Modem default" above, rather than the
|
||||||
|
optional placeholder: ui.js adds that placeholder only when no '' item
|
||||||
|
exists (ui.js:1180), and Footstrap hides a placeholder while the list
|
||||||
|
is open (Footstrap cascade.css:829, `:not([open])`), so
|
||||||
|
the open box had no text and the row, aligned on the baseline, moved
|
||||||
|
its label down to the box's bottom edge (luci-app-wwand#17) */
|
||||||
|
o.value('', _('Keep the modem\'s order'));
|
||||||
o.value('nr5g,lte', 'NR5G, LTE');
|
o.value('nr5g,lte', 'NR5G, LTE');
|
||||||
o.value('nr5g,lte,umts', 'NR5G, LTE, UMTS');
|
o.value('nr5g,lte,umts', 'NR5G, LTE, UMTS');
|
||||||
o.value('lte,nr5g', 'LTE, NR5G');
|
o.value('lte,nr5g', 'LTE, NR5G');
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,14 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2008-2014 The LuCI Team <luci@lists.subsignal.org>
|
||||||
|
#
|
||||||
|
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||||
|
#
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
LUCI_TITLE:=Support for hev socks5 tunnel
|
||||||
|
LUCI_DEPENDS:=+hev-socks5-tunnel +kmod-tun
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
# call BuildPackage - OpenWrt buildroot signature
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
'use strict';
|
||||||
|
'require form';
|
||||||
|
'require network';
|
||||||
|
|
||||||
|
return network.registerProtocol('h2s', {
|
||||||
|
getI18n: function() {
|
||||||
|
return _('Hev SOCKS5 Tunnel');
|
||||||
|
},
|
||||||
|
|
||||||
|
getIfname: function() {
|
||||||
|
return this._ubus('l3_device') || this.sid;
|
||||||
|
},
|
||||||
|
|
||||||
|
getOpkgPackage: function() {
|
||||||
|
return 'hev-socks5-tunnel';
|
||||||
|
},
|
||||||
|
|
||||||
|
isFloating: function() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
isVirtual: function() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
getDevices: function() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
containsDevice: function(ifname) {
|
||||||
|
return (network.getIfnameOf(ifname) == this.getIfname());
|
||||||
|
},
|
||||||
|
|
||||||
|
renderFormOptions: function(s) {
|
||||||
|
var dev = this.getL3Device() || this.getDevice(), o;
|
||||||
|
|
||||||
|
o = s.taboption('general', form.Value, 'server', _('SOCKS5 server'),
|
||||||
|
_('IP address or hostname of the SOCKS5 server'));
|
||||||
|
o.datatype = 'or(host,ipaddr)';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.taboption('general', form.Value, 'port', _('SOCKS5 port'));
|
||||||
|
o.datatype = 'port';
|
||||||
|
o.placeholder = '1080';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.taboption('general', form.Value, 'username', _('Username'),
|
||||||
|
_('Both username and password must be set, or both left empty'));
|
||||||
|
o.rmempty = true;
|
||||||
|
|
||||||
|
o = s.taboption('general', form.Value, 'password', _('Password'));
|
||||||
|
o.password = true;
|
||||||
|
o.rmempty = true;
|
||||||
|
|
||||||
|
o = s.taboption('general', form.ListValue, 'udp', _('UDP relay mode'));
|
||||||
|
o.value('udp', _('UDP (native SOCKS5 UDP ASSOCIATE)'));
|
||||||
|
o.value('tcp', _('UDP over TCP'));
|
||||||
|
o.default = 'udp';
|
||||||
|
|
||||||
|
o = s.taboption('general', form.DynamicList, 'ipaddr', _('Tunnel addresses'),
|
||||||
|
_('IPv4 and/or IPv6, CIDR notation. Without a mask /32 or /128 is used'));
|
||||||
|
o.datatype = 'ipmask';
|
||||||
|
o.rmempty = true;
|
||||||
|
|
||||||
|
o = s.taboption('general', form.DynamicList, 'gateway', _('Gateway'),
|
||||||
|
_('Adds a default route through this address'));
|
||||||
|
o.datatype = 'ipaddr("nomask")';
|
||||||
|
o.rmempty = true;
|
||||||
|
|
||||||
|
o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU'),
|
||||||
|
_('Tunnel device maximum transmission unit'));
|
||||||
|
o.placeholder = '8500';
|
||||||
|
o.datatype = 'range(576,9200)';
|
||||||
|
|
||||||
|
o = s.taboption('advanced', form.Value, 'mark', _('Firewall mark'),
|
||||||
|
_('Mark for packets of the SOCKS5 connection itself'));
|
||||||
|
o.datatype = 'uinteger';
|
||||||
|
o.rmempty = true;
|
||||||
|
|
||||||
|
o = s.taboption('advanced', form.Flag, 'tcp_fast_open', _('TCP Fast Open'));
|
||||||
|
o.rmempty = true;
|
||||||
|
|
||||||
|
o = s.taboption('advanced', form.ListValue, 'log_level', _('Log level'));
|
||||||
|
o.value('debug', _('Debug'));
|
||||||
|
o.value('info', _('Info'));
|
||||||
|
o.value('warn', _('Warning'));
|
||||||
|
o.value('error', _('Error'));
|
||||||
|
o.default = 'warn';
|
||||||
|
}
|
||||||
|
});
|
||||||
+228
@@ -0,0 +1,228 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
[ -n "$INCLUDE_ONLY" ] || {
|
||||||
|
. /lib/functions.sh
|
||||||
|
. /lib/functions/network.sh
|
||||||
|
. ../netifd-proto.sh
|
||||||
|
init_proto "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
proto_h2s_init_config() {
|
||||||
|
no_device=1
|
||||||
|
available=1
|
||||||
|
|
||||||
|
proto_config_add_array "ipaddr:list(string)"
|
||||||
|
proto_config_add_array "gateway:list(string)"
|
||||||
|
proto_config_add_string server
|
||||||
|
proto_config_add_int port
|
||||||
|
proto_config_add_string username
|
||||||
|
proto_config_add_string password
|
||||||
|
proto_config_add_string udp
|
||||||
|
proto_config_add_int mtu
|
||||||
|
proto_config_add_string log_level
|
||||||
|
proto_config_add_int mark
|
||||||
|
proto_config_add_boolean tcp_fast_open
|
||||||
|
|
||||||
|
proto_config_add_defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write a YAML single-quoted scalar.
|
||||||
|
# In YAML, a single quote inside a single-quoted scalar is doubled.
|
||||||
|
h2s_yaml_quote() {
|
||||||
|
printf "'"
|
||||||
|
printf '%s' "$1" | sed "s/'/''/g"
|
||||||
|
printf "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
proto_h2s_setup() {
|
||||||
|
local interface="$1"
|
||||||
|
local server port username password
|
||||||
|
local udp mtu log_level mark tcp_fast_open
|
||||||
|
local ipaddrs gateways a ad m g src4
|
||||||
|
local config i
|
||||||
|
|
||||||
|
json_get_values ipaddrs ipaddr
|
||||||
|
json_get_values gateways gateway
|
||||||
|
json_get_var server server
|
||||||
|
json_get_var port port
|
||||||
|
json_get_var username username
|
||||||
|
json_get_var password password
|
||||||
|
json_get_var udp udp
|
||||||
|
json_get_var mtu mtu
|
||||||
|
json_get_var log_level log_level
|
||||||
|
json_get_var mark mark
|
||||||
|
json_get_var tcp_fast_open tcp_fast_open
|
||||||
|
|
||||||
|
local ipaddr_type
|
||||||
|
json_get_type ipaddr_type ipaddr
|
||||||
|
|
||||||
|
# Hev uses the logical interface name as its TUN device name.
|
||||||
|
if [ "${#interface}" -gt 15 ]; then
|
||||||
|
proto_notify_error "$interface" INVALID_INTERFACE_NAME
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$interface" in
|
||||||
|
""|*[!a-zA-Z0-9_.-]*)
|
||||||
|
proto_notify_error "$interface" INVALID_INTERFACE_NAME
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -n "$server" ] || {
|
||||||
|
proto_notify_error "$interface" SERVER_MISSING
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$port" in
|
||||||
|
""|*[!0-9]*)
|
||||||
|
proto_notify_error "$interface" INVALID_PORT
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ "$port" -ge 1 ] && [ "$port" -le 65535 ] || {
|
||||||
|
proto_notify_error "$interface" INVALID_PORT
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -n "$username" ] && [ -z "$password" ] ||
|
||||||
|
[ -z "$username" ] && [ -n "$password" ]; then
|
||||||
|
proto_notify_error "$interface" INVALID_AUTH
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -n "$udp" ] || udp="udp"
|
||||||
|
case "$udp" in
|
||||||
|
tcp|udp) ;;
|
||||||
|
*)
|
||||||
|
proto_notify_error "$interface" INVALID_UDP_MODE
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -n "$mtu" ] || mtu=8500
|
||||||
|
case "$mtu" in
|
||||||
|
*[!0-9]*|"")
|
||||||
|
proto_notify_error "$interface" INVALID_MTU
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -n "$log_level" ] || log_level="warn"
|
||||||
|
case "$log_level" in
|
||||||
|
debug|info|warn|error) ;;
|
||||||
|
*)
|
||||||
|
proto_notify_error "$interface" INVALID_LOG_LEVEL
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
config="/var/run/hev-${interface}.yml"
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
mkdir -p /var/run || {
|
||||||
|
proto_notify_error "$interface" CONFIG_DIR_FAILED
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
cat <<EOF
|
||||||
|
tunnel:
|
||||||
|
name: $(h2s_yaml_quote "$interface")
|
||||||
|
mtu: $mtu
|
||||||
|
|
||||||
|
socks5:
|
||||||
|
address: $(h2s_yaml_quote "$server")
|
||||||
|
port: $port
|
||||||
|
udp: $(h2s_yaml_quote "$udp")
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ -n "$username" ]; then
|
||||||
|
printf ' username: '
|
||||||
|
h2s_yaml_quote "$username"
|
||||||
|
printf '\n password: '
|
||||||
|
h2s_yaml_quote "$password"
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$mark" ]; then
|
||||||
|
case "$mark" in
|
||||||
|
*[!0-9]*)
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
printf ' mark: %s\n' "$mark"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$tcp_fast_open" = "1" ]; then
|
||||||
|
printf ' tcp-fastopen: true\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
misc:
|
||||||
|
log-level: $(h2s_yaml_quote "$log_level")
|
||||||
|
EOF
|
||||||
|
} > "$config" || {
|
||||||
|
rm -f "$config"
|
||||||
|
proto_notify_error "$interface" CONFIG_WRITE_FAILED
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hev must stay in foreground: do not configure misc.pid-file.
|
||||||
|
proto_run_command "$interface" \
|
||||||
|
/usr/bin/hev-socks5-tunnel "$config"
|
||||||
|
|
||||||
|
# Wait until Hev creates its TUN device.
|
||||||
|
i=0
|
||||||
|
while [ "$i" -lt 50 ]; do
|
||||||
|
[ -d "/sys/class/net/$interface" ] && break
|
||||||
|
sleep 3
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ! -d "/sys/class/net/$interface" ]; then
|
||||||
|
proto_kill_command "$interface"
|
||||||
|
rm -f "$config"
|
||||||
|
proto_notify_error "$interface" TUN_CREATE_FAILED
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
proto_init_update "$interface" 1
|
||||||
|
proto_set_keep 1
|
||||||
|
|
||||||
|
for a in $ipaddrs; do
|
||||||
|
ad="${a%%/*}"
|
||||||
|
m="${a#*/}"
|
||||||
|
case "$a" in
|
||||||
|
*:*)
|
||||||
|
[ "$m" = "$a" ] && m=128
|
||||||
|
proto_add_ipv6_address "$ad" "$m"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
[ "$m" = "$a" ] && m=32
|
||||||
|
proto_add_ipv4_address "$ad" "$m"
|
||||||
|
[ "$src4" ] || src4=$ad
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
for g in $gateways; do
|
||||||
|
case "$g" in
|
||||||
|
*:*) proto_add_ipv6_route "::" 0 "$g" ;;
|
||||||
|
*) proto_add_ipv4_route "0.0.0.0" 0 "$g" "$src4" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
proto_send_update "$interface"
|
||||||
|
}
|
||||||
|
|
||||||
|
proto_h2s_teardown() {
|
||||||
|
local interface="$1"
|
||||||
|
|
||||||
|
proto_kill_command "$interface"
|
||||||
|
rm -f "/var/run/hev-${interface}.yml"
|
||||||
|
}
|
||||||
|
|
||||||
|
[ -n "$INCLUDE_ONLY" ] || add_protocol h2s
|
||||||
|
|
||||||
@@ -1,5 +1,48 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.3.26-r1] - 2026-11-02
|
||||||
|
|
||||||
|
- Bump version for release.
|
||||||
|
|
||||||
|
## [0.3.25-r4] - 2026-11-02
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Reduce large temporary files during blocklist refreshes and clean up intermediate files after each refresh to lower memory usage on routers.
|
||||||
|
- Add a lightweight statistics summary method for dashboards that only need aggregate information while preserving the existing full statistics response.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Verify custom allow/block rule precedence, generated blocklist output, and compatibility between the new statistics summary and existing detailed queries.
|
||||||
|
|
||||||
|
## [0.3.25-r3] - 2026-11-01
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Skip expired-statistics cleanup when the retention boundary is unchanged, and avoid recomputing aggregate totals when statistics have not changed.
|
||||||
|
- Skip temporary devices with no records in the upload period when preparing recent statistics uploads.
|
||||||
|
- Batch status initialization and refresh-result updates to reduce repeated reads and writes of the status file.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Verify that retention behavior and upload output remain unchanged and that batched status updates preserve all values.
|
||||||
|
|
||||||
|
## [0.3.25-r2] - 2026-11-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Harden locking to prevent concurrent statistics collector and automatic refresh runs.
|
||||||
|
- Avoid unnecessary DNS service restarts when blocklist downloads or preparation fail, preserving existing protection.
|
||||||
|
- Prevent status file corruption and lost updates during concurrent status writes.
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Use built-in OpenWrt tools for locking without requiring a separate `flock` command.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Test lock contention, stale-lock recovery, lock ownership, concurrent status updates, and avoidance of DNS restarts after pre-installation failures.
|
||||||
|
|
||||||
## [0.3.25-r1] - 2026-10-10
|
## [0.3.25-r1] - 2026-10-10
|
||||||
|
|
||||||
- Bump version for release.
|
- Bump version for release.
|
||||||
|
|||||||
+3
-2
@@ -6,8 +6,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=safeshield
|
PKG_NAME:=safeshield
|
||||||
PKG_VERSION:=0.3.25
|
PKG_VERSION:=0.3.26
|
||||||
PKG_RELEASE:=4
|
PKG_RELEASE:=5
|
||||||
|
|
||||||
PKG_MAINTAINER:=Beomjun Kang <kals323@gmail.com>
|
PKG_MAINTAINER:=Beomjun Kang <kals323@gmail.com>
|
||||||
PKG_LICENSE:=GPL-3.0-or-later
|
PKG_LICENSE:=GPL-3.0-or-later
|
||||||
@@ -80,6 +80,7 @@ define Package/safeshield/install
|
|||||||
$(INSTALL_BIN) ./files/usr/lib/safeshield/log.sh $(1)/usr/lib/safeshield/log.sh
|
$(INSTALL_BIN) ./files/usr/lib/safeshield/log.sh $(1)/usr/lib/safeshield/log.sh
|
||||||
$(INSTALL_BIN) ./files/usr/lib/safeshield/status.sh $(1)/usr/lib/safeshield/status.sh
|
$(INSTALL_BIN) ./files/usr/lib/safeshield/status.sh $(1)/usr/lib/safeshield/status.sh
|
||||||
$(INSTALL_BIN) ./files/usr/lib/safeshield/utils.sh $(1)/usr/lib/safeshield/utils.sh
|
$(INSTALL_BIN) ./files/usr/lib/safeshield/utils.sh $(1)/usr/lib/safeshield/utils.sh
|
||||||
|
$(INSTALL_DATA) ./files/usr/lib/safeshield/locking.sh $(1)/usr/lib/safeshield/locking.sh
|
||||||
$(INSTALL_DATA) ./files/usr/lib/safeshield/config.sh $(1)/usr/lib/safeshield/config.sh
|
$(INSTALL_DATA) ./files/usr/lib/safeshield/config.sh $(1)/usr/lib/safeshield/config.sh
|
||||||
$(INSTALL_BIN) ./files/usr/lib/safeshield/identity.sh $(1)/usr/lib/safeshield/identity.sh
|
$(INSTALL_BIN) ./files/usr/lib/safeshield/identity.sh $(1)/usr/lib/safeshield/identity.sh
|
||||||
$(INSTALL_DATA) ./files/usr/lib/safeshield/dns.sh $(1)/usr/lib/safeshield/dns.sh
|
$(INSTALL_DATA) ./files/usr/lib/safeshield/dns.sh $(1)/usr/lib/safeshield/dns.sh
|
||||||
|
|||||||
@@ -399,6 +399,7 @@ Available methods:
|
|||||||
safeshield.status
|
safeshield.status
|
||||||
safeshield.config
|
safeshield.config
|
||||||
safeshield.statistics
|
safeshield.statistics
|
||||||
|
safeshield.statistics_summary
|
||||||
safeshield.config_update
|
safeshield.config_update
|
||||||
safeshield.set_enabled
|
safeshield.set_enabled
|
||||||
safeshield.refresh
|
safeshield.refresh
|
||||||
@@ -420,6 +421,18 @@ ubus call safeshield statistics
|
|||||||
/etc/init.d/safeshield statistics
|
/etc/init.d/safeshield statistics
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For dashboards that need collector health and aggregate counters only, use:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ubus call safeshield statistics_summary
|
||||||
|
```
|
||||||
|
|
||||||
|
The summary retains configuration, service health, source details and totals,
|
||||||
|
but omits the potentially large `hourly` and `devices` arrays. The original
|
||||||
|
`statistics` response remains unchanged (schema v3). Both methods read the
|
||||||
|
current statistics snapshot; the summary specifically reduces RPC-side array
|
||||||
|
normalization and JSON response size, not the snapshot file read itself.
|
||||||
|
|
||||||
Statistics are collected from dnsmasq cumulative SafeShield UBus counters. Raw queried domains
|
Statistics are collected from dnsmasq cumulative SafeShield UBus counters. Raw queried domains
|
||||||
are never written to the statistics state. Loopback (`127.0.0.0/8` and `::1`)
|
are never written to the statistics state. Loopback (`127.0.0.0/8` and `::1`)
|
||||||
queries are excluded so SafeShield's own DNS health checks do not inflate user
|
queries are excluded so SafeShield's own DNS health checks do not inflate user
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ IPKG_INSTROOT="${IPKG_INSTROOT:-}"
|
|||||||
export IPKG_INSTROOT
|
export IPKG_INSTROOT
|
||||||
|
|
||||||
readonly PKG_NAME='safeshield'
|
readonly PKG_NAME='safeshield'
|
||||||
readonly PKG_VERSION='0.3.25-r1'
|
readonly PKG_VERSION='0.3.26-r1'
|
||||||
|
|
||||||
if type extra_command >/dev/null 2>&1; then
|
if type extra_command >/dev/null 2>&1; then
|
||||||
extra_command 'apply_local_rules' 'Apply local allow/block rules using the cached Hub artifact'
|
extra_command 'apply_local_rules' 'Apply local allow/block rules using the cached Hub artifact'
|
||||||
|
|||||||
@@ -876,7 +876,80 @@ ss_merge_sorted_api_sources() {
|
|||||||
sort -m -u "$@" >"$out"
|
sort -m -u "$@" >"$out"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Build the final blocked-rule stream without two full-sized domain copies.
|
||||||
|
# Keep the ancestor/equal precedence used by ss_filter_domains_shadowed_by_ancestors:
|
||||||
|
# local allow > local block > Hub allow > Hub block.
|
||||||
|
ss_write_effective_block_rules() {
|
||||||
|
local shadow_file="$1"
|
||||||
|
local local_blocks="$2"
|
||||||
|
local api_blocks="$3"
|
||||||
|
local output="$4"
|
||||||
|
local prefix="$5"
|
||||||
|
local suffix="$6"
|
||||||
|
|
||||||
|
# With no effective allow rules there is nothing to shadow the Hub blocks.
|
||||||
|
# Avoid building an AWK lookup table and splitting every domain, but keep
|
||||||
|
# the same local-first ordering and input validation as the normal path.
|
||||||
|
if [ ! -s "$shadow_file" ]; then
|
||||||
|
awk -v local_blocks="$local_blocks" -v prefix="$prefix" -v suffix="$suffix" '
|
||||||
|
FILENAME == local_blocks {
|
||||||
|
if ($0 != "") print prefix $0 suffix
|
||||||
|
next
|
||||||
|
}
|
||||||
|
/^[a-z0-9._-]+$/ { print prefix $0 suffix }
|
||||||
|
' "$local_blocks" "$api_blocks" >"$output"
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
awk -v shadow_file="$shadow_file" -v local_blocks="$local_blocks" \
|
||||||
|
-v prefix="$prefix" -v suffix="$suffix" '
|
||||||
|
FILENAME == shadow_file {
|
||||||
|
if ($0 != "") shadow[$0] = 1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
FILENAME == local_blocks {
|
||||||
|
if ($0 != "") print prefix $0 suffix
|
||||||
|
next
|
||||||
|
}
|
||||||
|
/^[a-z0-9._-]+$/ {
|
||||||
|
if ($0 in shadow) next
|
||||||
|
n = split($0, labels, ".")
|
||||||
|
ancestor = labels[n]
|
||||||
|
for (i = n - 1; i >= 1; i--) {
|
||||||
|
ancestor = labels[i] "." ancestor
|
||||||
|
if (ancestor in shadow) next
|
||||||
|
}
|
||||||
|
print prefix $0 suffix
|
||||||
|
}
|
||||||
|
' "$shadow_file" "$local_blocks" "$api_blocks" >"$output"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Merge scratch files live on tmpfs. API shards and local lists are retained
|
||||||
|
# for refresh retry/reuse, but these derived copies are never reused.
|
||||||
|
ss_cleanup_merge_scratch() {
|
||||||
|
rm -f \
|
||||||
|
"${SS_TMP_DIR}/api-blocks.domains" \
|
||||||
|
"${SS_TMP_DIR}/api-allows.domains" \
|
||||||
|
"${SS_TMP_DIR}/effective-local-blocks.domains" \
|
||||||
|
"${SS_TMP_DIR}/effective-api-allows.domains" \
|
||||||
|
"${SS_TMP_DIR}/effective-allows.domains" \
|
||||||
|
"${SS_TMP_DIR}/effective-api-blocks.domains" \
|
||||||
|
"${SS_TMP_DIR}/final-blocks.domains"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Always release derived tmpfs storage, including when validation fails.
|
||||||
ss_merge_lists() {
|
ss_merge_lists() {
|
||||||
|
local result=0
|
||||||
|
# Firmware refresh uses the C locale. Enforce it here too so direct
|
||||||
|
# calls (including CI) retain the exact same bytewise domain order.
|
||||||
|
local LC_ALL=C
|
||||||
|
export LC_ALL
|
||||||
|
ss_merge_lists_inner "$@" || result=$?
|
||||||
|
ss_cleanup_merge_scratch
|
||||||
|
return "$result"
|
||||||
|
}
|
||||||
|
|
||||||
|
ss_merge_lists_inner() {
|
||||||
local final
|
local final
|
||||||
local api_blocks="${SS_TMP_DIR}/api-blocks.domains"
|
local api_blocks="${SS_TMP_DIR}/api-blocks.domains"
|
||||||
local api_allows="${SS_TMP_DIR}/api-allows.domains"
|
local api_allows="${SS_TMP_DIR}/api-allows.domains"
|
||||||
@@ -885,10 +958,9 @@ ss_merge_lists() {
|
|||||||
local effective_local_blocks="${SS_TMP_DIR}/effective-local-blocks.domains"
|
local effective_local_blocks="${SS_TMP_DIR}/effective-local-blocks.domains"
|
||||||
local effective_api_allows="${SS_TMP_DIR}/effective-api-allows.domains"
|
local effective_api_allows="${SS_TMP_DIR}/effective-api-allows.domains"
|
||||||
local effective_allows="${SS_TMP_DIR}/effective-allows.domains"
|
local effective_allows="${SS_TMP_DIR}/effective-allows.domains"
|
||||||
local effective_api_blocks="${SS_TMP_DIR}/effective-api-blocks.domains"
|
|
||||||
local final_blocks="${SS_TMP_DIR}/final-blocks.domains"
|
|
||||||
local final_size_kb
|
local final_size_kb
|
||||||
local valid_count
|
local valid_count
|
||||||
|
local rule_prefix rule_suffix
|
||||||
|
|
||||||
final="$(ss_blocklist_tmp_path)" || return 1
|
final="$(ss_blocklist_tmp_path)" || return 1
|
||||||
: >"$final" || return 1
|
: >"$final" || return 1
|
||||||
@@ -929,39 +1001,39 @@ ss_merge_lists() {
|
|||||||
rm -f "$final"
|
rm -f "$final"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
ss_filter_domains_shadowed_by_ancestors "$effective_allows" "$api_blocks" "$effective_api_blocks" || {
|
# Produce final rule lines directly rather than materializing both a full
|
||||||
|
# filtered API blocklist and a second combined domain list. The input API
|
||||||
|
# shards were already merged and sorted above; the final sort preserves the
|
||||||
|
# exact dnsmasq rule order/deduplication of the old pipeline.
|
||||||
|
if ss_dnsmasq_safeshield_block_supported; then
|
||||||
|
rule_prefix='safeshield-block=/'
|
||||||
|
rule_suffix='/'
|
||||||
|
else
|
||||||
|
rule_prefix='address=/'
|
||||||
|
rule_suffix='/#'
|
||||||
|
fi
|
||||||
|
ss_write_effective_block_rules \
|
||||||
|
"$effective_allows" "$effective_local_blocks" "$api_blocks" \
|
||||||
|
"$final" "$rule_prefix" "$rule_suffix" || {
|
||||||
rm -f "$final"
|
rm -f "$final"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
{
|
# Sort by the domain field (between slashes), not the whole dnsmasq rule.
|
||||||
[ -f "$effective_local_blocks" ] && cat "$effective_local_blocks"
|
# Sorting full rules changes legacy ordering for domain-prefix pairs such
|
||||||
[ -f "$effective_api_blocks" ] && cat "$effective_api_blocks"
|
# as foo and foo-bar, causing otherwise identical refreshes to differ.
|
||||||
} | sort -u >"$final_blocks" || {
|
# The domain field cannot contain '/' after normalization. -o keeps the
|
||||||
|
# output in-place without allocating a second full-size domain copy.
|
||||||
|
# Bound GNU sort's in-memory buffer on low-memory routers. External sort
|
||||||
|
# runs may use more temporary disk space; the final order is unchanged.
|
||||||
|
sort -S 1M -u -t/ -k2,2 -o "$final" "$final" || {
|
||||||
rm -f "$final"
|
rm -f "$final"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
# Preserve the cancellation point from the original merge pipeline.
|
||||||
ss_should_stop && {
|
ss_should_stop && {
|
||||||
rm -f "$final"
|
rm -f "$final"
|
||||||
return 130
|
return 130
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ -s "$final_blocks" ]; then
|
|
||||||
if ss_dnsmasq_safeshield_block_supported; then
|
|
||||||
awk '{ print "safeshield-block=/" $0 "/" }' "$final_blocks" >"$final" || {
|
|
||||||
rm -f "$final"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
else
|
|
||||||
# Stock dnsmasq does not understand the SafeShield extension. Preserve
|
|
||||||
# protection with the standard address rule and leave statistics off.
|
|
||||||
awk '{ print "address=/" $0 "/#" }' "$final_blocks" >"$final" || {
|
|
||||||
rm -f "$final"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -s "$effective_allows" ]; then
|
if [ -s "$effective_allows" ]; then
|
||||||
awk '{ print "server=/" $0 "/#" }' "$effective_allows" >>"$final" || {
|
awk '{ print "server=/" $0 "/#" }' "$effective_allows" >>"$final" || {
|
||||||
rm -f "$final"
|
rm -f "$final"
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ readonly SS_MAX_ARTIFACT_SOURCES=16
|
|||||||
readonly SS_PREV_BLOCKLIST_GZ="/tmp/safeshield.prev.blocklist.gz"
|
readonly SS_PREV_BLOCKLIST_GZ="/tmp/safeshield.prev.blocklist.gz"
|
||||||
readonly SS_RUNTIME_OUT="${SS_TMP_DIR}/runtime.out"
|
readonly SS_RUNTIME_OUT="${SS_TMP_DIR}/runtime.out"
|
||||||
readonly SS_REFRESH_LOCK='/var/lock/safeshield-refresh.lock'
|
readonly SS_REFRESH_LOCK='/var/lock/safeshield-refresh.lock'
|
||||||
readonly SS_REFRESH_LOCK_FD=307
|
|
||||||
readonly SS_LOCAL_APPLY_STATE="${SS_TMP_DIR}/local-apply.state"
|
readonly SS_LOCAL_APPLY_STATE="${SS_TMP_DIR}/local-apply.state"
|
||||||
readonly SS_STATISTICS_DIR="${SS_TMP_DIR}/statistics"
|
readonly SS_STATISTICS_DIR="${SS_TMP_DIR}/statistics"
|
||||||
readonly SS_STATISTICS_STATE_FILE="${SS_STATISTICS_DIR}/state.tsv"
|
readonly SS_STATISTICS_STATE_FILE="${SS_STATISTICS_DIR}/state.tsv"
|
||||||
@@ -444,7 +443,6 @@ safeshield_apply_local_rules() {
|
|||||||
*)
|
*)
|
||||||
log_error "Failed to merge cached Hub artifact with local rules"
|
log_error "Failed to merge cached Hub artifact with local rules"
|
||||||
ss_mark_local_apply_failure "local_merge_failed"
|
ss_mark_local_apply_failure "local_merge_failed"
|
||||||
ss_restore_and_restart
|
|
||||||
ss_refresh_lock_close
|
ss_refresh_lock_close
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
@@ -647,9 +645,7 @@ safeshield_force_download() {
|
|||||||
*)
|
*)
|
||||||
failure_code="$(ss_resolve_error_code)"
|
failure_code="$(ss_resolve_error_code)"
|
||||||
ss_status_mark_failure "$failure_code"
|
ss_status_mark_failure "$failure_code"
|
||||||
if [ "$failure_code" != "safeshield_upgrade_required" ]; then
|
# No blocklist replacement has happened; keep the running DNS service.
|
||||||
ss_restore_and_restart
|
|
||||||
fi
|
|
||||||
ss_refresh_lock_close
|
ss_refresh_lock_close
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
@@ -672,9 +668,7 @@ safeshield_force_download() {
|
|||||||
*)
|
*)
|
||||||
failure_code="$(ss_artifact_download_error_code)"
|
failure_code="$(ss_artifact_download_error_code)"
|
||||||
ss_status_mark_failure "$failure_code"
|
ss_status_mark_failure "$failure_code"
|
||||||
if [ "$failure_code" != "safeshield_upgrade_required" ]; then
|
# No blocklist replacement has happened; keep the running DNS service.
|
||||||
ss_restore_and_restart
|
|
||||||
fi
|
|
||||||
ss_refresh_lock_close
|
ss_refresh_lock_close
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
@@ -689,7 +683,6 @@ safeshield_force_download() {
|
|||||||
ss_prepare_local_rule_files || {
|
ss_prepare_local_rule_files || {
|
||||||
log_error "Failed to build allowlist"
|
log_error "Failed to build allowlist"
|
||||||
ss_status_mark_failure "allowlist_build_failed"
|
ss_status_mark_failure "allowlist_build_failed"
|
||||||
ss_restore_and_restart
|
|
||||||
ss_refresh_lock_close
|
ss_refresh_lock_close
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@@ -711,7 +704,6 @@ safeshield_force_download() {
|
|||||||
*)
|
*)
|
||||||
log_error "Failed to merge API artifact with local overrides"
|
log_error "Failed to merge API artifact with local overrides"
|
||||||
ss_status_mark_failure "merge_failed"
|
ss_status_mark_failure "merge_failed"
|
||||||
ss_restore_and_restart
|
|
||||||
ss_refresh_lock_close
|
ss_refresh_lock_close
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# shellcheck shell=ash
|
||||||
|
|
||||||
|
# Directory locks work with the OpenWrt BusyBox base utilities; flock(1) is
|
||||||
|
# intentionally not required. A pid-less directory is treated as initializing
|
||||||
|
# (or abandoned) and must age before it can be reclaimed.
|
||||||
|
ss_lock_pid_alive() {
|
||||||
|
case "${1:-}" in
|
||||||
|
'' | *[!0-9]* | 0) return 1 ;;
|
||||||
|
esac
|
||||||
|
kill -0 "$1" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
ss_lock_stale() {
|
||||||
|
local path="$1" owner='' modified='' now=''
|
||||||
|
|
||||||
|
[ -d "$path" ] || return 1
|
||||||
|
if [ -r "$path/pid" ]; then
|
||||||
|
IFS= read -r owner <"$path/pid" || true
|
||||||
|
# A live owner always wins, even if the directory appears old.
|
||||||
|
ss_lock_pid_alive "$owner" && return 1
|
||||||
|
# Never reclaim an invalid pid that could be mid-write without a grace period.
|
||||||
|
case "$owner" in
|
||||||
|
'' | *[!0-9]* | 0) ;;
|
||||||
|
*) return 0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# stat is available in the supported BusyBox builds. If it is missing,
|
||||||
|
# fail closed rather than destroying a possibly initializing lock.
|
||||||
|
modified="$(stat -c %Y "$path" 2>/dev/null)" || return 1
|
||||||
|
now="$(date +%s 2>/dev/null)" || return 1
|
||||||
|
case "$modified:$now" in
|
||||||
|
*[!0-9:]* | :* | *:) return 1 ;;
|
||||||
|
esac
|
||||||
|
[ "$((now - modified))" -ge 30 ] 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
ss_lock_reclaim() {
|
||||||
|
local path="$1" guard="${1}.reclaim" owner=''
|
||||||
|
|
||||||
|
# Only one contender may inspect/remove a stale directory at a time.
|
||||||
|
mkdir "$guard" 2>/dev/null || return 1
|
||||||
|
if ! ss_lock_stale "$path"; then
|
||||||
|
rmdir "$guard" 2>/dev/null || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ -r "$path/pid" ]; then
|
||||||
|
IFS= read -r owner <"$path/pid" || true
|
||||||
|
fi
|
||||||
|
if ss_lock_pid_alive "$owner"; then
|
||||||
|
rmdir "$guard" 2>/dev/null || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
# A foreign file makes this fail safely instead of deleting arbitrary data.
|
||||||
|
rm -f "$path/pid" 2>/dev/null || true
|
||||||
|
if ! rmdir "$path" 2>/dev/null; then
|
||||||
|
rmdir "$guard" 2>/dev/null || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rmdir "$guard" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ss_lock_try() {
|
||||||
|
local path="$1" attempt=0
|
||||||
|
[ -n "$path" ] || return 1
|
||||||
|
mkdir -p "${path%/*}" || return 1
|
||||||
|
while [ "$attempt" -lt 2 ]; do
|
||||||
|
# Prevent new acquisitions while stale-lock removal is in progress.
|
||||||
|
if [ -d "${path}.reclaim" ]; then
|
||||||
|
# A crashed reclaimer must not block all future acquisitions forever.
|
||||||
|
# Refuse to touch a recent guard still owned by an active reclaimer.
|
||||||
|
ss_lock_stale "${path}.reclaim" && rmdir "${path}.reclaim" 2>/dev/null || return 1
|
||||||
|
fi
|
||||||
|
if mkdir "$path" 2>/dev/null; then
|
||||||
|
if ! printf '%s\n' "$$" >"$path/pid"; then
|
||||||
|
rmdir "$path" 2>/dev/null || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ -e "${path}.reclaim" ]; then
|
||||||
|
ss_lock_release "$path"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
[ "$attempt" -eq 0 ] || break
|
||||||
|
ss_lock_reclaim "$path" || return 1
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
ss_lock_wait() {
|
||||||
|
local path="$1" timeout="${2:-10}" stop_cb="${3:-}" waited=0
|
||||||
|
case "$timeout" in
|
||||||
|
'' | *[!0-9]*) timeout=10 ;;
|
||||||
|
esac
|
||||||
|
while ! ss_lock_try "$path"; do
|
||||||
|
if [ "$waited" -ge "$timeout" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ -n "$stop_cb" ] && "$stop_cb"; then
|
||||||
|
return 130
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
waited=$((waited + 1))
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ss_lock_release() {
|
||||||
|
local path="$1" owner=''
|
||||||
|
[ -n "$path" ] && [ -r "$path/pid" ] || return 0
|
||||||
|
IFS= read -r owner <"$path/pid" || true
|
||||||
|
[ "$owner" = "$$" ] || return 1
|
||||||
|
# Do not remove someone else's data even if the lock has extra contents.
|
||||||
|
rm -f "$path/pid" || return 1
|
||||||
|
rmdir "$path" 2>/dev/null
|
||||||
|
}
|
||||||
@@ -30,6 +30,16 @@ function device_bucket_key(key, bucket) {
|
|||||||
|
|
||||||
function prune_buckets(now, cutoff, bucket, composite, parts) {
|
function prune_buckets(now, cutoff, bucket, composite, parts) {
|
||||||
cutoff = hour_start(now) - ((retention_hours - 1) * 3600)
|
cutoff = hour_start(now) - ((retention_hours - 1) * 3600)
|
||||||
|
# Existing snapshots only add counters to the active hour. Repeating the
|
||||||
|
# full expired-bucket sweep every minute does no work until the cutoff
|
||||||
|
# advances. A collector restart starts with an empty cache and prunes once.
|
||||||
|
if (cutoff == last_prune_cutoff) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
last_prune_cutoff = cutoff
|
||||||
|
# A new retention window can invalidate cached totals even without DNS
|
||||||
|
# traffic; the full aggregate is rebuilt at the next snapshot.
|
||||||
|
aggregate_dirty = 1
|
||||||
for (bucket in queries) {
|
for (bucket in queries) {
|
||||||
if ((bucket + 0) < cutoff) {
|
if ((bucket + 0) < cutoff) {
|
||||||
delete queries[bucket]
|
delete queries[bucket]
|
||||||
@@ -136,6 +146,7 @@ function register_device(key, mac, ip, hostname, client_id, duid, selected, o
|
|||||||
# device record structurally complete, this avoids passing a missing awk
|
# device record structurally complete, this avoids passing a missing awk
|
||||||
# associative-array element to helper functions during serialization.
|
# associative-array element to helper functions during serialization.
|
||||||
device_seen[selected] = 1
|
device_seen[selected] = 1
|
||||||
|
aggregate_dirty = 1
|
||||||
device_mac[selected] = ""
|
device_mac[selected] = ""
|
||||||
device_ip[selected] = ""
|
device_ip[selected] = ""
|
||||||
device_hostname[selected] = ""
|
device_hostname[selected] = ""
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ function migrate_device(from_key, to_key, mac, ip, hostname, client_id, duid,
|
|||||||
if (!journal_replaying) {
|
if (!journal_replaying) {
|
||||||
journal_deleted_device[from_key] = 1
|
journal_deleted_device[from_key] = 1
|
||||||
}
|
}
|
||||||
|
aggregate_dirty = 1
|
||||||
remove_device_metadata(from_key)
|
remove_device_metadata(from_key)
|
||||||
selected = register_device(to_key, mac, ip, hostname, client_id, duid)
|
selected = register_device(to_key, mac, ip, hostname, client_id, duid)
|
||||||
if (selected == "") {
|
if (selected == "") {
|
||||||
@@ -533,6 +534,7 @@ function record_device(ip, query_delta, blocked_delta, now, key, bucket, comp
|
|||||||
|
|
||||||
bucket = hour_start(now)
|
bucket = hour_start(now)
|
||||||
composite = device_bucket_key(key, bucket)
|
composite = device_bucket_key(key, bucket)
|
||||||
|
aggregate_dirty = 1
|
||||||
device_hour_queries[composite] += 0
|
device_hour_queries[composite] += 0
|
||||||
device_hour_blocked[composite] += 0
|
device_hour_blocked[composite] += 0
|
||||||
if (query_delta > 0) {
|
if (query_delta > 0) {
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ function save_json_range(path, now, first_hour, last_hour, include_empty_devices
|
|||||||
if (!is_unknown_output_device(key)) {
|
if (!is_unknown_output_device(key)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
# An upload contains only a short recent window. Ignore provisional
|
||||||
|
# devices whose retained buckets fall completely outside that window.
|
||||||
|
if ((device_last_bucket[key] + 0) < first_hour ||
|
||||||
|
(device_first_bucket[key] + 0) > last_hour) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
for (bucket = first_hour; bucket <= last_hour; bucket += 3600) {
|
for (bucket = first_hour; bucket <= last_hour; bucket += 3600) {
|
||||||
composite = device_bucket_key(key, bucket)
|
composite = device_bucket_key(key, bucket)
|
||||||
if (!((composite in device_hour_queries) || (composite in device_hour_blocked))) {
|
if (!((composite in device_hour_queries) || (composite in device_hour_blocked))) {
|
||||||
@@ -228,9 +234,16 @@ function save_snapshot(now, force_persistent, force_snapshot, serialize_snaps
|
|||||||
serialize_snapshot = snapshot_dirty || force_snapshot || (force_persistent && persistence_configured)
|
serialize_snapshot = snapshot_dirty || force_snapshot || (force_persistent && persistence_configured)
|
||||||
if (serialize_snapshot) {
|
if (serialize_snapshot) {
|
||||||
prune_buckets(now)
|
prune_buckets(now)
|
||||||
recompute_totals()
|
# Error-only and unchanged UBus snapshots still refresh source metadata
|
||||||
|
# but do not change retained counters. Avoid reaggregating 128 devices
|
||||||
|
# across up to 168 hours unless counters, identities, or cutoff changed.
|
||||||
|
if (aggregate_dirty) {
|
||||||
|
recompute_totals()
|
||||||
|
aggregate_dirty = 0
|
||||||
|
}
|
||||||
if (compact_stale_provisional_devices(now) > 0) {
|
if (compact_stale_provisional_devices(now) > 0) {
|
||||||
recompute_totals()
|
recompute_totals()
|
||||||
|
aggregate_dirty = 0
|
||||||
}
|
}
|
||||||
updated_at = now
|
updated_at = now
|
||||||
if (!ensure_snapshot_sequence_reservation(now)) {
|
if (!ensure_snapshot_sequence_reservation(now)) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ function record_event(query_delta, blocked_delta, client_ip, now, bucket) {
|
|||||||
now = current_time()
|
now = current_time()
|
||||||
bucket = hour_start(now)
|
bucket = hour_start(now)
|
||||||
snapshot_dirty = 1
|
snapshot_dirty = 1
|
||||||
|
aggregate_dirty = 1
|
||||||
queries[bucket] += 0
|
queries[bucket] += 0
|
||||||
blocked[bucket] += 0
|
blocked[bucket] += 0
|
||||||
|
|
||||||
@@ -110,6 +111,7 @@ function apply_source_snapshot( now, bucket, had_source, same_epoch, query_de
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (query_delta > 0 || blocked_delta > 0) {
|
if (query_delta > 0 || blocked_delta > 0) {
|
||||||
|
aggregate_dirty = 1
|
||||||
queries[bucket] += 0
|
queries[bucket] += 0
|
||||||
blocked[bucket] += 0
|
blocked[bucket] += 0
|
||||||
queries[bucket] += query_delta
|
queries[bucket] += query_delta
|
||||||
@@ -184,6 +186,9 @@ BEGIN {
|
|||||||
}
|
}
|
||||||
event_index = 0
|
event_index = 0
|
||||||
snapshot_dirty = 0
|
snapshot_dirty = 0
|
||||||
|
# BEGIN restores state and reconciles device identities before the first
|
||||||
|
# snapshot, so never trust persisted aggregates as an in-memory cache.
|
||||||
|
aggregate_dirty = 1
|
||||||
device_count = 0
|
device_count = 0
|
||||||
devices_truncated = 0
|
devices_truncated = 0
|
||||||
last_lease_refresh = 0
|
last_lease_refresh = 0
|
||||||
|
|||||||
@@ -62,6 +62,19 @@ else if (cmd == 'set') {
|
|||||||
|
|
||||||
obj.data[key] = value;
|
obj.data[key] = value;
|
||||||
}
|
}
|
||||||
|
else if (cmd == 'set_many') {
|
||||||
|
// Reject incomplete input before modifying any field; one JSON write is
|
||||||
|
// committed only after every key/value pair has been validated.
|
||||||
|
if (length(ARGV) < 4 || (length(ARGV) - 2) % 2 != 0)
|
||||||
|
exit(2);
|
||||||
|
|
||||||
|
for (let i = 2; i < length(ARGV); i += 2) {
|
||||||
|
if (!ARGV[i])
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
for (let i = 2; i < length(ARGV); i += 2)
|
||||||
|
obj.data[ARGV[i]] = ARGV[i + 1];
|
||||||
|
}
|
||||||
else if (cmd == 'add_error') {
|
else if (cmd == 'add_error') {
|
||||||
let code = ARGV[2];
|
let code = ARGV[2];
|
||||||
|
|
||||||
|
|||||||
@@ -10,26 +10,19 @@ ss_status_exec() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ss_status_apply() {
|
ss_status_apply() {
|
||||||
local tmp="${RUNNING_STATUS_FILE}.tmp"
|
local tmp="${RUNNING_STATUS_FILE}.tmp.$$" rc=0
|
||||||
|
|
||||||
mkdir -p "${RUNNING_STATUS_FILE%/*}" || return 1
|
mkdir -p "${RUNNING_STATUS_FILE%/*}" || return 1
|
||||||
|
json_lock_open || return 1
|
||||||
|
|
||||||
ss_status_exec "$@" >"${tmp}" || {
|
ss_status_exec "$@" >"$tmp" || rc=1
|
||||||
rm -f "${tmp}"
|
[ "$rc" -eq 0 ] && [ -s "$tmp" ] || rc=1
|
||||||
return 1
|
if [ "$rc" -eq 0 ]; then
|
||||||
}
|
mv -f "$tmp" "$RUNNING_STATUS_FILE" || rc=1
|
||||||
|
fi
|
||||||
[ -s "${tmp}" ] || {
|
[ "$rc" -eq 0 ] || rm -f "$tmp"
|
||||||
rm -f "${tmp}"
|
json_lock_close
|
||||||
return 1
|
return "$rc"
|
||||||
}
|
|
||||||
|
|
||||||
mv -f "${tmp}" "${RUNNING_STATUS_FILE}" || {
|
|
||||||
rm -f "${tmp}"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ss_now() {
|
ss_now() {
|
||||||
@@ -40,66 +33,77 @@ ss_status_set_now() {
|
|||||||
ss_status_set "$1" "$(ss_now)"
|
ss_status_set "$1" "$(ss_now)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# A single status-store invocation updates all pairs while holding the same
|
||||||
|
# JSON lock. This avoids spawning ucode once per field during a refresh reset.
|
||||||
|
ss_status_set_many() {
|
||||||
|
[ "$#" -gt 0 ] || return 0
|
||||||
|
ss_status_apply set_many "$@" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
ss_status_reset_health_fields() {
|
ss_status_reset_health_fields() {
|
||||||
ss_status_set health_dnsmasq_binary ""
|
ss_status_set_many \
|
||||||
ss_status_set health_dnsmasq_version ""
|
health_dnsmasq_binary "" \
|
||||||
ss_status_set health_dnsmasq_features ""
|
health_dnsmasq_version "" \
|
||||||
ss_status_set health_dnsmasq_confdir ""
|
health_dnsmasq_features "" \
|
||||||
ss_status_set health_dnsmasq_initial_restart ""
|
health_dnsmasq_confdir "" \
|
||||||
ss_status_set health_dnsmasq_final_restart ""
|
health_dnsmasq_initial_restart "" \
|
||||||
ss_status_set health_dns_runtime ""
|
health_dnsmasq_final_restart "" \
|
||||||
ss_status_set health_blocklist_verify ""
|
health_dns_runtime "" \
|
||||||
ss_status_set health_min_valid_line_count ""
|
health_blocklist_verify "" \
|
||||||
ss_status_set health_max_file_size ""
|
health_min_valid_line_count "" \
|
||||||
ss_status_set health_api_resolve ""
|
health_max_file_size "" \
|
||||||
ss_status_set health_artifact_download ""
|
health_api_resolve "" \
|
||||||
ss_status_set health_artifact_sha256 ""
|
health_artifact_download "" \
|
||||||
ss_status_set dnsmasq_version ""
|
health_artifact_sha256 "" \
|
||||||
ss_status_set dnsmasq_min_version "${SS_MIN_DNSMASQ_VERSION:-2.93}"
|
dnsmasq_version "" \
|
||||||
|
dnsmasq_min_version "${SS_MIN_DNSMASQ_VERSION:-2.93}"
|
||||||
}
|
}
|
||||||
|
|
||||||
ss_status_reset_blocklist_fields() {
|
ss_status_reset_blocklist_fields() {
|
||||||
ss_status_set blocklist_installed "0"
|
ss_status_set_many \
|
||||||
ss_status_set blocklist_file_size_kb "0"
|
blocklist_installed "0" \
|
||||||
ss_status_set blocklist_verification_ok "0"
|
blocklist_file_size_kb "0" \
|
||||||
ss_status_set blocklist_test_domain ""
|
blocklist_verification_ok "0" \
|
||||||
ss_status_set blocklist_test_domain_sample_count "0"
|
blocklist_test_domain "" \
|
||||||
ss_status_set blocklist_test_domain_success_count "0"
|
blocklist_test_domain_sample_count "0" \
|
||||||
ss_status_set blocklist_test_domains ""
|
blocklist_test_domain_success_count "0" \
|
||||||
ss_status_set blocklist_backup_available "0"
|
blocklist_test_domains "" \
|
||||||
|
blocklist_backup_available "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
ss_status_reset_artifact_fields() {
|
ss_status_reset_artifact_fields() {
|
||||||
ss_status_set entitlement_plan ""
|
ss_status_set_many \
|
||||||
ss_status_set entitlement_status ""
|
entitlement_plan "" \
|
||||||
ss_status_set physical_fingerprint ""
|
entitlement_status "" \
|
||||||
ss_status_set fingerprint_version ""
|
physical_fingerprint "" \
|
||||||
ss_status_set identity_provider ""
|
fingerprint_version "" \
|
||||||
ss_status_set identity_source ""
|
identity_provider "" \
|
||||||
ss_status_set identity_strength ""
|
identity_source "" \
|
||||||
ss_status_set identity_profile ""
|
identity_strength "" \
|
||||||
ss_status_set device_code ""
|
identity_profile "" \
|
||||||
ss_status_set device_code_source ""
|
device_code "" \
|
||||||
ss_status_set installation_id ""
|
device_code_source "" \
|
||||||
ss_status_set device_profile ""
|
installation_id "" \
|
||||||
ss_status_set artifact_tier ""
|
device_profile "" \
|
||||||
ss_status_set artifact_version ""
|
artifact_tier "" \
|
||||||
ss_status_set artifact_sha256 ""
|
artifact_version "" \
|
||||||
ss_status_set artifact_unique_domains "0"
|
artifact_sha256 "" \
|
||||||
ss_status_set artifact_rules "0"
|
artifact_unique_domains "0" \
|
||||||
ss_status_set artifact_download_url_present "0"
|
artifact_rules "0" \
|
||||||
ss_status_set artifact_source_count "0"
|
artifact_download_url_present "0" \
|
||||||
ss_status_set artifact_block_source_count "0"
|
artifact_source_count "0" \
|
||||||
ss_status_set artifact_allow_source_count "0"
|
artifact_block_source_count "0" \
|
||||||
|
artifact_allow_source_count "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
ss_status_mark_failure() {
|
ss_status_mark_failure() {
|
||||||
local code="$1"
|
local code="$1"
|
||||||
|
|
||||||
ss_status_set status "error"
|
ss_status_set_many \
|
||||||
ss_status_set_now last_failure
|
status "error" \
|
||||||
ss_status_set last_result "error"
|
last_failure "$(ss_now)" \
|
||||||
ss_status_set last_error_code "$code"
|
last_result "error" \
|
||||||
|
last_error_code "$code"
|
||||||
ss_status_add_error "$code"
|
ss_status_add_error "$code"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,14 +113,15 @@ ss_status_mark_success() {
|
|||||||
|
|
||||||
now="$(ss_now)"
|
now="$(ss_now)"
|
||||||
ss_status_apply clear_messages >/dev/null 2>&1 || true
|
ss_status_apply clear_messages >/dev/null 2>&1 || true
|
||||||
ss_status_set status "ready"
|
|
||||||
ss_status_set stage "done"
|
|
||||||
ss_status_set last_success "$now"
|
|
||||||
ss_status_set last_result "success"
|
|
||||||
ss_status_set last_error_code ""
|
|
||||||
|
|
||||||
if is_valid_integer "$interval" && [ "$interval" -gt 0 ] 2>/dev/null; then
|
if is_valid_integer "$interval" && [ "$interval" -gt 0 ] 2>/dev/null; then
|
||||||
ss_status_set next_refresh_at "$((now + interval))"
|
ss_status_set_many \
|
||||||
|
status "ready" stage "done" last_success "$now" \
|
||||||
|
last_result "success" last_error_code "" \
|
||||||
|
next_refresh_at "$((now + interval))"
|
||||||
|
else
|
||||||
|
ss_status_set_many \
|
||||||
|
status "ready" stage "done" last_success "$now" \
|
||||||
|
last_result "success" last_error_code ""
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,9 +136,10 @@ ss_status_prepare_refresh() {
|
|||||||
is_valid_integer "$last_local_apply_failure" || last_local_apply_failure=0
|
is_valid_integer "$last_local_apply_failure" || last_local_apply_failure=0
|
||||||
|
|
||||||
ss_status_reset
|
ss_status_reset
|
||||||
ss_status_set last_success "$last_success"
|
ss_status_set_many \
|
||||||
ss_status_set last_local_apply "$last_local_apply"
|
last_success "$last_success" \
|
||||||
ss_status_set last_local_apply_failure "$last_local_apply_failure"
|
last_local_apply "$last_local_apply" \
|
||||||
|
last_local_apply_failure "$last_local_apply_failure"
|
||||||
}
|
}
|
||||||
|
|
||||||
ss_status_set() {
|
ss_status_set() {
|
||||||
@@ -159,22 +165,14 @@ ss_status_add_warning() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ss_status_reset() {
|
ss_status_reset() {
|
||||||
rm -f "${RUNNING_STATUS_FILE}" "${RUNNING_STATUS_FILE}.tmp"
|
|
||||||
|
|
||||||
ss_status_apply reset >/dev/null 2>&1 || return 1
|
ss_status_apply reset >/dev/null 2>&1 || return 1
|
||||||
|
|
||||||
ss_status_set status "idle"
|
ss_status_set_many \
|
||||||
ss_status_set stage ""
|
status "idle" stage "" valid_line_count "0" \
|
||||||
ss_status_set valid_line_count "0"
|
last_result "idle" last_error_code "" \
|
||||||
|
last_attempt "0" last_success "0" last_failure "0" \
|
||||||
ss_status_set last_result "idle"
|
last_local_apply "0" last_local_apply_failure "0" \
|
||||||
ss_status_set last_error_code ""
|
next_refresh_at "0"
|
||||||
ss_status_set last_attempt "0"
|
|
||||||
ss_status_set last_success "0"
|
|
||||||
ss_status_set last_failure "0"
|
|
||||||
ss_status_set last_local_apply "0"
|
|
||||||
ss_status_set last_local_apply_failure "0"
|
|
||||||
ss_status_set next_refresh_at "0"
|
|
||||||
|
|
||||||
ss_status_reset_health_fields
|
ss_status_reset_health_fields
|
||||||
ss_status_reset_blocklist_fields
|
ss_status_reset_blocklist_fields
|
||||||
@@ -262,41 +260,15 @@ ss_restore_previous_blocklist() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ss_refresh_lock_open() {
|
ss_refresh_lock_open() {
|
||||||
mkdir -p "${SS_REFRESH_LOCK%/*}" || return 1
|
ss_lock_try "${SS_REFRESH_LOCK}.d"
|
||||||
eval "exec ${SS_REFRESH_LOCK_FD}>\"${SS_REFRESH_LOCK}\"" || return 1
|
|
||||||
flock -n "${SS_REFRESH_LOCK_FD}" || {
|
|
||||||
eval "exec ${SS_REFRESH_LOCK_FD}>&-"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ss_refresh_lock_open_wait() {
|
ss_refresh_lock_open_wait() {
|
||||||
local timeout="${1:-120}"
|
local timeout="${1:-120}"
|
||||||
local waited=0
|
|
||||||
|
|
||||||
is_valid_integer "$timeout" || timeout=120
|
is_valid_integer "$timeout" || timeout=120
|
||||||
mkdir -p "${SS_REFRESH_LOCK%/*}" || return 1
|
ss_lock_wait "${SS_REFRESH_LOCK}.d" "$timeout" ss_should_stop
|
||||||
eval "exec ${SS_REFRESH_LOCK_FD}>\"${SS_REFRESH_LOCK}\"" || return 1
|
|
||||||
|
|
||||||
while ! flock -n "${SS_REFRESH_LOCK_FD}"; do
|
|
||||||
if [ "$waited" -ge "$timeout" ] 2>/dev/null; then
|
|
||||||
eval "exec ${SS_REFRESH_LOCK_FD}>&-" 2>/dev/null || true
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ss_should_stop && {
|
|
||||||
eval "exec ${SS_REFRESH_LOCK_FD}>&-" 2>/dev/null || true
|
|
||||||
return 130
|
|
||||||
}
|
|
||||||
|
|
||||||
sleep 1
|
|
||||||
waited=$((waited + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ss_refresh_lock_close() {
|
ss_refresh_lock_close() {
|
||||||
flock -u "${SS_REFRESH_LOCK_FD}" 2>/dev/null || true
|
ss_lock_release "${SS_REFRESH_LOCK}.d"
|
||||||
eval "exec ${SS_REFRESH_LOCK_FD}>&-" 2>/dev/null || true
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
# shellcheck shell=ash
|
# shellcheck shell=ash
|
||||||
|
|
||||||
readonly LOCK_FD=309
|
readonly RUNNING_STATUS_LOCK="/var/lock/${PKG_NAME}.lock.d"
|
||||||
readonly RUNNING_STATUS_LOCK="/var/lock/${PKG_NAME}.lock"
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "${SS_LOCKING_LIB:-${IPKG_INSTROOT:-}/usr/lib/safeshield/locking.sh}"
|
||||||
readonly RUNNING_STATUS_FILE="/dev/shm/${PKG_NAME}.status.json"
|
readonly RUNNING_STATUS_FILE="/dev/shm/${PKG_NAME}.status.json"
|
||||||
|
|
||||||
command_exists() {
|
command_exists() {
|
||||||
@@ -21,17 +23,23 @@ is_greater_equal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
json_lock_open() {
|
json_lock_open() {
|
||||||
mkdir -p "${RUNNING_STATUS_LOCK%/*}" || return 1
|
ss_lock_wait "$RUNNING_STATUS_LOCK" 10
|
||||||
eval "exec ${LOCK_FD}>\"${RUNNING_STATUS_LOCK}\"" || return 1
|
|
||||||
flock -x "${LOCK_FD}" || {
|
|
||||||
eval "exec ${LOCK_FD}>&-"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
json_lock_close() {
|
json_lock_close() {
|
||||||
flock -u "${LOCK_FD}" 2>/dev/null || true
|
ss_lock_release "$RUNNING_STATUS_LOCK"
|
||||||
eval "exec ${LOCK_FD}>&-" 2>/dev/null || true
|
}
|
||||||
|
|
||||||
|
_json_commit() {
|
||||||
|
local tmp="${RUNNING_STATUS_FILE}.tmp.$$"
|
||||||
|
json_dump >"$tmp" || {
|
||||||
|
rm -f "$tmp"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
mv -f "$tmp" "$RUNNING_STATUS_FILE" || {
|
||||||
|
rm -f "$tmp"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_json_init_data_object() {
|
_json_init_data_object() {
|
||||||
@@ -62,7 +70,7 @@ _json_set() {
|
|||||||
|
|
||||||
json_add_string "$key" "$value"
|
json_add_string "$key" "$value"
|
||||||
json_select .. 2>/dev/null || true
|
json_select .. 2>/dev/null || true
|
||||||
json_dump >"${RUNNING_STATUS_FILE}"
|
_json_commit
|
||||||
}
|
}
|
||||||
|
|
||||||
_json_add_message() {
|
_json_add_message() {
|
||||||
@@ -82,7 +90,7 @@ _json_add_message() {
|
|||||||
json_add_string info "$value"
|
json_add_string info "$value"
|
||||||
json_close_object
|
json_close_object
|
||||||
json_select .. 2>/dev/null || true
|
json_select .. 2>/dev/null || true
|
||||||
json_dump >"${RUNNING_STATUS_FILE}"
|
_json_commit
|
||||||
}
|
}
|
||||||
|
|
||||||
json() {
|
json() {
|
||||||
|
|||||||
@@ -14,20 +14,26 @@ export IPKG_INSTROOT
|
|||||||
. /usr/lib/safeshield/core.sh
|
. /usr/lib/safeshield/core.sh
|
||||||
|
|
||||||
readonly SS_REFRESHD_LOCK='/var/lock/safeshield-refreshd.lock'
|
readonly SS_REFRESHD_LOCK='/var/lock/safeshield-refreshd.lock'
|
||||||
readonly SS_REFRESHD_LOCK_FD=308
|
refreshd_owned=0
|
||||||
|
|
||||||
ss_should_terminate=0
|
ss_should_terminate=0
|
||||||
ss_sleep_pid=''
|
ss_sleep_pid=''
|
||||||
|
|
||||||
refreshd_lock() {
|
refreshd_lock() {
|
||||||
mkdir -p "${SS_REFRESHD_LOCK%/*}" || return 1
|
ss_lock_try "${SS_REFRESHD_LOCK}.d" || return 1
|
||||||
eval "exec ${SS_REFRESHD_LOCK_FD}>\"${SS_REFRESHD_LOCK}\"" || return 1
|
refreshd_owned=1
|
||||||
flock -n "${SS_REFRESHD_LOCK_FD}" || return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshd_unlock() {
|
refreshd_unlock() {
|
||||||
flock -u "${SS_REFRESHD_LOCK_FD}" 2>/dev/null || true
|
[ "${refreshd_owned:-0}" -eq 1 ] || return 0
|
||||||
eval "exec ${SS_REFRESHD_LOCK_FD}>&-" 2>/dev/null || true
|
ss_lock_release "${SS_REFRESHD_LOCK}.d"
|
||||||
|
refreshd_owned=0
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshd_cleanup() {
|
||||||
|
[ "${refreshd_owned:-0}" -eq 1 ] || return 0
|
||||||
|
clear_next_refresh_at
|
||||||
|
refreshd_unlock
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_term() {
|
handle_term() {
|
||||||
@@ -253,6 +259,6 @@ main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trap "handle_term" TERM INT
|
trap "handle_term" TERM INT
|
||||||
trap "refreshd_unlock; clear_next_refresh_at" EXIT
|
trap "refreshd_cleanup" EXIT
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
@@ -7,6 +7,7 @@ LC_ALL=C
|
|||||||
export LC_ALL
|
export LC_ALL
|
||||||
umask 077
|
umask 077
|
||||||
|
|
||||||
|
SS_STATSD_LOCK_LIB="${SS_STATSD_LOCK_LIB:-/usr/lib/safeshield/locking.sh}"
|
||||||
SS_STATSD_FUNCTIONS_LIB="${SS_STATSD_FUNCTIONS_LIB:-/lib/functions.sh}"
|
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_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_AWK_DIR="${SS_STATSD_AWK_DIR:-/usr/lib/safeshield/statistics}"
|
||||||
@@ -24,6 +25,8 @@ SS_STATSD_PERSISTENT_COMPACT_INTERVAL_S="${SS_STATSD_PERSISTENT_COMPACT_INTERVAL
|
|||||||
. "$SS_STATSD_FUNCTIONS_LIB"
|
. "$SS_STATSD_FUNCTIONS_LIB"
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
. "$SS_STATSD_CORE_LIB"
|
. "$SS_STATSD_CORE_LIB"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$SS_STATSD_LOCK_LIB"
|
||||||
|
|
||||||
stats_poller_pid=''
|
stats_poller_pid=''
|
||||||
stats_awk_pid=''
|
stats_awk_pid=''
|
||||||
@@ -74,7 +77,8 @@ stats_cleanup() {
|
|||||||
[ -n "$stats_fifo" ] && rm -f "$stats_fifo"
|
[ -n "$stats_fifo" ] && rm -f "$stats_fifo"
|
||||||
|
|
||||||
if [ "$stats_lock_owned" -eq 1 ] && [ -n "$stats_lock_dir" ]; then
|
if [ "$stats_lock_owned" -eq 1 ] && [ -n "$stats_lock_dir" ]; then
|
||||||
rm -rf "$stats_lock_dir"
|
ss_lock_release "$stats_lock_dir" || true
|
||||||
|
stats_lock_owned=0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,30 +89,12 @@ stats_terminate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stats_acquire_lock() {
|
stats_acquire_lock() {
|
||||||
local owner_pid=''
|
|
||||||
|
|
||||||
stats_lock_dir="${SS_STATISTICS_DIR}/collector.lock"
|
stats_lock_dir="${SS_STATISTICS_DIR}/collector.lock"
|
||||||
|
if ! ss_lock_try "$stats_lock_dir"; then
|
||||||
if mkdir "$stats_lock_dir" 2>/dev/null; then
|
log_warn 'Statistics collector lock is already held or unavailable'
|
||||||
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
|
return 1
|
||||||
fi
|
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
|
stats_lock_owned=1
|
||||||
printf '%s\n' "$$" >"${stats_lock_dir}/pid"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"status",
|
"status",
|
||||||
"config",
|
"config",
|
||||||
"statistics",
|
"statistics",
|
||||||
|
"statistics_summary",
|
||||||
"rules_list"
|
"rules_list"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ return {
|
|||||||
call: statistics.build
|
call: statistics.build
|
||||||
},
|
},
|
||||||
|
|
||||||
|
statistics_summary: {
|
||||||
|
call: statistics.summary
|
||||||
|
},
|
||||||
|
|
||||||
config_update: {
|
config_update: {
|
||||||
args: {
|
args: {
|
||||||
values: {}
|
values: {}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function sanitize_source(item) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function build_statistics() {
|
function build_statistics(include_detail) {
|
||||||
reload_uci();
|
reload_uci();
|
||||||
let enabled = to_bool(cfg('statistics_enabled', '1'), true);
|
let enabled = to_bool(cfg('statistics_enabled', '1'), true);
|
||||||
let retention_hours = to_int(cfg('statistics_retention_hours', '168'), 168);
|
let retention_hours = to_int(cfg('statistics_retention_hours', '168'), 168);
|
||||||
@@ -98,7 +98,7 @@ function build_statistics() {
|
|||||||
if (!effective_enabled) {
|
if (!effective_enabled) {
|
||||||
source.available = false;
|
source.available = false;
|
||||||
}
|
}
|
||||||
return {
|
let result = {
|
||||||
schema: {
|
schema: {
|
||||||
name: STATISTICS_SCHEMA_NAME,
|
name: STATISTICS_SCHEMA_NAME,
|
||||||
version: STATISTICS_SCHEMA_VERSION
|
version: STATISTICS_SCHEMA_VERSION
|
||||||
@@ -133,13 +133,26 @@ function build_statistics() {
|
|||||||
totals: {
|
totals: {
|
||||||
queries: to_int(totals.queries, 0),
|
queries: to_int(totals.queries, 0),
|
||||||
blocked: to_int(totals.blocked, 0)
|
blocked: to_int(totals.blocked, 0)
|
||||||
},
|
}
|
||||||
hourly: sanitize_hourly(data.hourly),
|
|
||||||
device_limit: to_int(data.device_limit, 128),
|
|
||||||
devices_truncated: to_bool(data.devices_truncated, false),
|
|
||||||
devices: sanitize_devices(data.devices)
|
|
||||||
};
|
};
|
||||||
|
// Preserve the legacy detailed RPC field insertion order as well as
|
||||||
|
// names/values: totals -> hourly -> device_limit -> devices_truncated ->
|
||||||
|
// devices. Summary callers omit only the two large detail arrays.
|
||||||
|
if (include_detail !== false) {
|
||||||
|
result.hourly = sanitize_hourly(data.hourly);
|
||||||
|
}
|
||||||
|
result.device_limit = to_int(data.device_limit, 128);
|
||||||
|
result.devices_truncated = to_bool(data.devices_truncated, false);
|
||||||
|
if (include_detail !== false) {
|
||||||
|
result.devices = sanitize_devices(data.devices);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_statistics_summary() {
|
||||||
|
return build_statistics(false);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
build: build_statistics
|
build: build_statistics,
|
||||||
|
summary: build_statistics_summary
|
||||||
};
|
};
|
||||||
@@ -8,6 +8,27 @@ Describe 'SafeShield blocklist behavior'
|
|||||||
The error should equal ''
|
The error should equal ''
|
||||||
End
|
End
|
||||||
|
|
||||||
|
It 'builds large lists with bounded merge scratch files and preserves precedence'
|
||||||
|
When call ss_case_blocklist_tmpfs
|
||||||
|
The status should be success
|
||||||
|
The output should equal ''
|
||||||
|
The error should equal ''
|
||||||
|
End
|
||||||
|
|
||||||
|
It 'preserves the exact legacy domain order for both dnsmasq modes'
|
||||||
|
When call ss_case_blocklist_legacy_order
|
||||||
|
The status should be success
|
||||||
|
The output should equal ''
|
||||||
|
The error should equal ''
|
||||||
|
End
|
||||||
|
|
||||||
|
It 'filters block rules with bounded-memory lookup and no-allow fast path'
|
||||||
|
When call ss_case_blocklist_low_memory
|
||||||
|
The status should be success
|
||||||
|
The output should equal ''
|
||||||
|
The error should equal ''
|
||||||
|
End
|
||||||
|
|
||||||
It 'resolves and downloads multiple Hub artifact sources'
|
It 'resolves and downloads multiple Hub artifact sources'
|
||||||
When call ss_case_multi_artifact
|
When call ss_case_multi_artifact
|
||||||
The status should be success
|
The status should be success
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ Describe 'SafeShield core shell behavior'
|
|||||||
The error should equal ''
|
The error should equal ''
|
||||||
End
|
End
|
||||||
|
|
||||||
|
It 'updates status fields in batches rather than one process per key'
|
||||||
|
When call ss_case_status_batch_writes
|
||||||
|
The status should be success
|
||||||
|
The output should equal ''
|
||||||
|
The error should equal ''
|
||||||
|
End
|
||||||
|
|
||||||
It 'removes preserved legacy license keys during upgrade'
|
It 'removes preserved legacy license keys during upgrade'
|
||||||
When call ss_case_legacy_license_cleanup
|
When call ss_case_legacy_license_cleanup
|
||||||
The status should be success
|
The status should be success
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# shellcheck shell=sh
|
# shellcheck shell=sh
|
||||||
|
|
||||||
Describe 'SafeShield lifecycle and refresh state machines'
|
Describe 'SafeShield lifecycle and refresh state machines'
|
||||||
It 'rolls back full refresh failures while preserving the active blocklist for upgrade-required responses'
|
It 'keeps DNS running before installation failures and rolls back post-install failures'
|
||||||
When call ss_case_refresh_state_machine
|
When call ss_case_refresh_state_machine
|
||||||
The status should be success
|
The status should be success
|
||||||
The output should equal ''
|
The output should equal ''
|
||||||
|
|||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# shellcheck shell=sh
|
||||||
|
|
||||||
|
Describe 'SafeShield directory locking and state consistency'
|
||||||
|
It 'prevents lock theft, reclaims stale owners and respects ownership'
|
||||||
|
When call ss_case_lock_owner_and_contention
|
||||||
|
The status should be success
|
||||||
|
The output should equal ''
|
||||||
|
The error should equal ''
|
||||||
|
End
|
||||||
|
|
||||||
|
It 'serializes concurrent operations using BusyBox-compatible locks'
|
||||||
|
When call ss_case_lock_parallel
|
||||||
|
The status should be success
|
||||||
|
The output should equal ''
|
||||||
|
The error should equal ''
|
||||||
|
End
|
||||||
|
|
||||||
|
It 'serializes status read-modify-write operations under the same lock'
|
||||||
|
When call ss_case_status_write_serialization
|
||||||
|
The status should be success
|
||||||
|
The output should equal ''
|
||||||
|
The error should equal ''
|
||||||
|
End
|
||||||
|
|
||||||
|
It 'does not corrupt the previous status when serialization fails'
|
||||||
|
When call ss_case_status_write_failure
|
||||||
|
The status should be success
|
||||||
|
The output should equal ''
|
||||||
|
The error should equal ''
|
||||||
|
End
|
||||||
|
End
|
||||||
@@ -16,6 +16,8 @@ export SS_SPEC_ROOT
|
|||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
. "$SS_SPEC_ROOT/spec/support/lifecycle_cases.sh"
|
. "$SS_SPEC_ROOT/spec/support/lifecycle_cases.sh"
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
|
. "$SS_SPEC_ROOT/spec/support/locking_cases.sh"
|
||||||
|
# shellcheck disable=SC1091
|
||||||
. "$SS_SPEC_ROOT/spec/support/blocklist_cases.sh"
|
. "$SS_SPEC_ROOT/spec/support/blocklist_cases.sh"
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
. "$SS_SPEC_ROOT/spec/support/device_api_cases.sh"
|
. "$SS_SPEC_ROOT/spec/support/device_api_cases.sh"
|
||||||
|
|||||||
@@ -119,6 +119,13 @@ Describe 'SafeShield statistics behavior'
|
|||||||
The error should equal ''
|
The error should equal ''
|
||||||
End
|
End
|
||||||
|
|
||||||
|
It 'prunes expired statistics while preserving the short upload window'
|
||||||
|
When call ss_case_statistics_retention_cache
|
||||||
|
The status should be success
|
||||||
|
The output should equal ''
|
||||||
|
The error should equal ''
|
||||||
|
End
|
||||||
|
|
||||||
It 'loads the statistics collector from focused awk modules'
|
It 'loads the statistics collector from focused awk modules'
|
||||||
When call ss_case_statistics_modules
|
When call ss_case_statistics_modules
|
||||||
The status should be success
|
The status should be success
|
||||||
|
|||||||
@@ -140,6 +140,203 @@ sample-two.example'
|
|||||||
ss_spec_assert_eq "$DNS_CHECK_COUNT" '2'
|
ss_spec_assert_eq "$DNS_CHECK_COUNT" '2'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# A larger, overlapping fixture exercises sorted streaming, tmpfs cleanup and
|
||||||
|
# precedence without downloading any external artifact.
|
||||||
|
ss_case_blocklist_tmpfs() (
|
||||||
|
set -eu
|
||||||
|
TMP_DIR="$(ss_spec_tmpdir)"
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
|
||||||
|
SS_TMP_DIR="$TMP_DIR/tmp"
|
||||||
|
SS_DNSMASQ_DIR="$TMP_DIR/dnsmasq.d"
|
||||||
|
SS_BLOCKLIST_FILE="$SS_DNSMASQ_DIR/safeshield.blocklist"
|
||||||
|
ss_min_valid_line_count=1
|
||||||
|
ss_max_blocklist_file_size_kb=1024
|
||||||
|
ss_dnsmasq_sanity_check=0
|
||||||
|
mkdir -p "$SS_TMP_DIR" "$SS_DNSMASQ_DIR"
|
||||||
|
ss_should_stop() { [ "${STOP_NOW:-0}" = 1 ]; }
|
||||||
|
ss_status_set() { :; }
|
||||||
|
ss_status_add_error() { :; }
|
||||||
|
log_info() { :; }
|
||||||
|
log_error() { :; }
|
||||||
|
ss_blocklist_tmp_path() { printf '%s/.safeshield.blocklist.tmp.%s\n' "$SS_DNSMASQ_DIR" "$$"; }
|
||||||
|
ss_install_blocklist_atomic() { mv -f "$1" "$2"; }
|
||||||
|
DNSMASQ_PATCHED=1
|
||||||
|
ss_dnsmasq_safeshield_block_supported() { [ "$DNSMASQ_PATCHED" = '1' ]; }
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
|
||||||
|
|
||||||
|
awk 'BEGIN { for (i = 1; i <= 6000; i++) printf "ads%05d.example\n", i }' >"$SS_TMP_DIR/api.0.block.txt"
|
||||||
|
awk 'BEGIN { for (i = 4000; i <= 10000; i++) printf "ads%05d.example\n", i }' >"$SS_TMP_DIR/api.1.block.txt"
|
||||||
|
printf '%s\n' 'ads00042.example' 'hub-allow.example' >"$SS_TMP_DIR/api.2.allow.txt"
|
||||||
|
printf '%s\n' 'hub-allow.example' 'local.example' >"$SS_TMP_DIR/local.block.txt"
|
||||||
|
printf '%s\n' 'local.example' 'ads00042.example' >"$SS_TMP_DIR/local.allow.txt"
|
||||||
|
|
||||||
|
# Prior implementations left 7 merge scratch files on tmpfs. Even stale
|
||||||
|
# files from a previous run must be released after this refresh.
|
||||||
|
printf 'stale\n' >"$SS_TMP_DIR/effective-api-blocks.domains"
|
||||||
|
printf 'stale\n' >"$SS_TMP_DIR/final-blocks.domains"
|
||||||
|
ss_merge_lists
|
||||||
|
ss_spec_assert_eq "$(grep -c '^safeshield-block=/' "$SS_BLOCKLIST_FILE")" '10000'
|
||||||
|
ss_spec_assert_eq "$(grep -c '^server=/' "$SS_BLOCKLIST_FILE")" '2'
|
||||||
|
ss_spec_assert_file_contains "$SS_BLOCKLIST_FILE" 'safeshield-block=/hub-allow.example/'
|
||||||
|
ss_spec_assert_file_not_contains "$SS_BLOCKLIST_FILE" 'safeshield-block=/ads00042.example/'
|
||||||
|
ss_spec_assert_file_line "$SS_BLOCKLIST_FILE" 'server=/ads00042.example/#'
|
||||||
|
ss_spec_assert_file_line "$SS_BLOCKLIST_FILE" 'server=/local.example/#'
|
||||||
|
# Cached source shards and local rules must not be deleted by cleanup.
|
||||||
|
ss_spec_assert_exists "$SS_TMP_DIR/api.0.block.txt"
|
||||||
|
ss_spec_assert_exists "$SS_TMP_DIR/local.block.txt"
|
||||||
|
for derived in \
|
||||||
|
api-blocks.domains api-allows.domains \
|
||||||
|
effective-local-blocks.domains effective-api-allows.domains \
|
||||||
|
effective-allows.domains effective-api-blocks.domains final-blocks.domains; do
|
||||||
|
ss_spec_assert_not_exists "$SS_TMP_DIR/$derived"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Stock dnsmasq rules retain identical precedence and order.
|
||||||
|
DNSMASQ_PATCHED=0
|
||||||
|
ss_merge_lists
|
||||||
|
ss_spec_assert_eq "$(grep -c '^address=/' "$SS_BLOCKLIST_FILE")" '10000'
|
||||||
|
ss_spec_assert_file_line "$SS_BLOCKLIST_FILE" 'address=/hub-allow.example/#'
|
||||||
|
ss_spec_assert_file_not_contains "$SS_BLOCKLIST_FILE" 'address=/ads00042.example/#'
|
||||||
|
for derived in api-blocks.domains effective-api-blocks.domains final-blocks.domains; do
|
||||||
|
ss_spec_assert_not_exists "$SS_TMP_DIR/$derived"
|
||||||
|
done
|
||||||
|
|
||||||
|
# A cancelled refresh must not install partial rules or retain scratch.
|
||||||
|
before="$(cksum "$SS_BLOCKLIST_FILE")"
|
||||||
|
STOP_NOW=1
|
||||||
|
result=0
|
||||||
|
ss_merge_lists || result=$?
|
||||||
|
ss_spec_assert_eq "$result" '130'
|
||||||
|
ss_spec_assert_eq "$(cksum "$SS_BLOCKLIST_FILE")" "$before"
|
||||||
|
ss_spec_assert_not_exists "$SS_TMP_DIR/api-blocks.domains"
|
||||||
|
ss_spec_assert_not_exists "$SS_TMP_DIR/effective-api-blocks.domains"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Print an actionable diff if this ordering contract regresses in CI.
|
||||||
|
ss_assert_legacy_blocklist_order() (
|
||||||
|
# Use a subshell to keep these variables scoped under POSIX sh.
|
||||||
|
expected="$1"
|
||||||
|
actual_file="$2"
|
||||||
|
expected_file="${actual_file}.expected.$$"
|
||||||
|
|
||||||
|
printf '%s\n' "$expected" >"$expected_file"
|
||||||
|
if ! cmp -s "$expected_file" "$actual_file"; then
|
||||||
|
diff -u "$expected_file" "$actual_file" >&2 || :
|
||||||
|
rm -f "$expected_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rm -f "$expected_file"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ordering is part of our byte-for-byte legacy blocklist compatibility.
|
||||||
|
# Sorting decorated dnsmasq rule strings changes prefix ordering ("foo" vs
|
||||||
|
# "foo-bar" / "foo.bar"); always compare by the undressed domain field.
|
||||||
|
ss_case_blocklist_legacy_order() (
|
||||||
|
set -eu
|
||||||
|
TMP_DIR="$(ss_spec_tmpdir)"
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
|
||||||
|
SS_TMP_DIR="$TMP_DIR/tmp"
|
||||||
|
SS_DNSMASQ_DIR="$TMP_DIR/dnsmasq.d"
|
||||||
|
SS_BLOCKLIST_FILE="$SS_DNSMASQ_DIR/safeshield.blocklist"
|
||||||
|
ss_min_valid_line_count=0
|
||||||
|
ss_max_blocklist_file_size_kb=1024
|
||||||
|
ss_dnsmasq_sanity_check=0
|
||||||
|
mkdir -p "$SS_TMP_DIR" "$SS_DNSMASQ_DIR"
|
||||||
|
ss_should_stop() { return 1; }
|
||||||
|
ss_status_set() { :; }
|
||||||
|
ss_status_add_error() { :; }
|
||||||
|
log_info() { :; }
|
||||||
|
log_error() { :; }
|
||||||
|
ss_blocklist_tmp_path() { printf '%s/.safeshield.blocklist.tmp.%s\n' "$SS_DNSMASQ_DIR" "$$"; }
|
||||||
|
ss_install_blocklist_atomic() { mv -f "$1" "$2"; }
|
||||||
|
DNSMASQ_PATCHED=1
|
||||||
|
ss_dnsmasq_safeshield_block_supported() { [ "$DNSMASQ_PATCHED" = '1' ]; }
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
|
||||||
|
# API shards are pre-sorted by ss_download_api_artifacts(). Keep this
|
||||||
|
# fixture representative of the actual -m (merge) precondition.
|
||||||
|
printf '%s\n' 'abc.test' 'foo' 'foo-bar' 'foo.bar' 'foo_bar' |
|
||||||
|
LC_ALL=C sort -u >"$SS_TMP_DIR/api.0.block.txt"
|
||||||
|
printf '%s\n' 'abc.test-more' 'abc.test.uk' |
|
||||||
|
LC_ALL=C sort -u >"$SS_TMP_DIR/api.1.block.txt"
|
||||||
|
ss_merge_lists
|
||||||
|
expected='safeshield-block=/abc.test/
|
||||||
|
safeshield-block=/abc.test-more/
|
||||||
|
safeshield-block=/abc.test.uk/
|
||||||
|
safeshield-block=/foo/
|
||||||
|
safeshield-block=/foo-bar/
|
||||||
|
safeshield-block=/foo.bar/
|
||||||
|
safeshield-block=/foo_bar/'
|
||||||
|
ss_assert_legacy_blocklist_order "$expected" "$SS_BLOCKLIST_FILE"
|
||||||
|
DNSMASQ_PATCHED=0
|
||||||
|
ss_merge_lists
|
||||||
|
expected='address=/abc.test/#
|
||||||
|
address=/abc.test-more/#
|
||||||
|
address=/abc.test.uk/#
|
||||||
|
address=/foo/#
|
||||||
|
address=/foo-bar/#
|
||||||
|
address=/foo.bar/#
|
||||||
|
address=/foo_bar/#'
|
||||||
|
ss_assert_legacy_blocklist_order "$expected" "$SS_BLOCKLIST_FILE"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Both low-memory filtering paths must retain the legacy allow precedence and
|
||||||
|
# the original local-block-first rule stream. The final merge sorts afterwards.
|
||||||
|
ss_case_blocklist_low_memory() (
|
||||||
|
set -eu
|
||||||
|
TMP_DIR="$(ss_spec_tmpdir)"
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
|
||||||
|
mkdir -p "$TMP_DIR"
|
||||||
|
SS_TMP_DIR="$TMP_DIR"
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
|
||||||
|
cat >"$TMP_DIR/blocks" <<'DATA'
|
||||||
|
ads.example
|
||||||
|
foo
|
||||||
|
foo-bar
|
||||||
|
sub.ads.example
|
||||||
|
invalid/path
|
||||||
|
CAPS.example
|
||||||
|
valid.example
|
||||||
|
DATA
|
||||||
|
printf '%s\n' 'local.example' >"$TMP_DIR/local"
|
||||||
|
: >"$TMP_DIR/shadow"
|
||||||
|
ss_write_effective_block_rules \
|
||||||
|
"$TMP_DIR/shadow" "$TMP_DIR/local" "$TMP_DIR/blocks" \
|
||||||
|
"$TMP_DIR/result" 'safeshield-block=/' '/'
|
||||||
|
expected='safeshield-block=/local.example/
|
||||||
|
safeshield-block=/ads.example/
|
||||||
|
safeshield-block=/foo/
|
||||||
|
safeshield-block=/foo-bar/
|
||||||
|
safeshield-block=/sub.ads.example/
|
||||||
|
safeshield-block=/valid.example/'
|
||||||
|
ss_spec_assert_eq "$(cat "$TMP_DIR/result")" "$expected"
|
||||||
|
|
||||||
|
# A present allow shadows the matching Hub block and its descendants, but
|
||||||
|
# does not remove an explicit local block or unrelated Hub domains.
|
||||||
|
printf '%s\n' 'foo' 'ads.example' >"$TMP_DIR/shadow"
|
||||||
|
ss_write_effective_block_rules \
|
||||||
|
"$TMP_DIR/shadow" "$TMP_DIR/local" "$TMP_DIR/blocks" \
|
||||||
|
"$TMP_DIR/result" 'address=/' '/#'
|
||||||
|
expected='address=/local.example/#
|
||||||
|
address=/foo-bar/#
|
||||||
|
address=/valid.example/#'
|
||||||
|
ss_spec_assert_eq "$(cat "$TMP_DIR/result")" "$expected"
|
||||||
|
|
||||||
|
# When the local list is empty, the no-allow fast path must still process
|
||||||
|
# Hub block records (including the first record of the second input file).
|
||||||
|
: >"$TMP_DIR/local"
|
||||||
|
: >"$TMP_DIR/shadow"
|
||||||
|
ss_write_effective_block_rules \
|
||||||
|
"$TMP_DIR/shadow" "$TMP_DIR/local" "$TMP_DIR/blocks" \
|
||||||
|
"$TMP_DIR/result" 'address=/' '/#'
|
||||||
|
ss_spec_assert_eq "$(head -n 1 "$TMP_DIR/result")" 'address=/ads.example/#'
|
||||||
|
ss_spec_assert_eq "$(grep -c '^address=/' "$TMP_DIR/result")" '5'
|
||||||
|
BLOCKLIST="$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
|
||||||
|
ss_spec_assert_file_contains "$BLOCKLIST" 'sort -S 1M -u -t/ -k2,2 -o "$final" "$final"'
|
||||||
|
)
|
||||||
|
|
||||||
ss_case_multi_artifact() (
|
ss_case_multi_artifact() (
|
||||||
set -eu
|
set -eu
|
||||||
TMP_DIR="$(ss_spec_tmpdir)"
|
TMP_DIR="$(ss_spec_tmpdir)"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ ss_case_utils() (
|
|||||||
PKG_NAME='safeshield-test'
|
PKG_NAME='safeshield-test'
|
||||||
export PKG_NAME
|
export PKG_NAME
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
|
SS_LOCKING_LIB="$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh"
|
||||||
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/utils.sh"
|
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/utils.sh"
|
||||||
|
|
||||||
command_exists sh
|
command_exists sh
|
||||||
@@ -25,6 +26,7 @@ ss_case_config_validation() (
|
|||||||
PKG_NAME='safeshield-test'
|
PKG_NAME='safeshield-test'
|
||||||
export PKG_NAME
|
export PKG_NAME
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
|
SS_LOCKING_LIB="$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh"
|
||||||
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/utils.sh"
|
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/utils.sh"
|
||||||
|
|
||||||
WARNINGS=''
|
WARNINGS=''
|
||||||
@@ -121,6 +123,7 @@ ss_case_dnsmasq_version() (
|
|||||||
PKG_NAME='safeshield'
|
PKG_NAME='safeshield'
|
||||||
export PKG_NAME
|
export PKG_NAME
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
|
SS_LOCKING_LIB="$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh"
|
||||||
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/utils.sh"
|
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/utils.sh"
|
||||||
DNSMASQ_FEATURE_HEALTH=''
|
DNSMASQ_FEATURE_HEALTH=''
|
||||||
ss_status_set() {
|
ss_status_set() {
|
||||||
@@ -378,6 +381,15 @@ ss_case_status_state() (
|
|||||||
record() { printf '%s\t%s\t%s\n' "$1" "${2:-}" "${3:-}" >>"$CALLS"; }
|
record() { printf '%s\t%s\t%s\n' "$1" "${2:-}" "${3:-}" >>"$CALLS"; }
|
||||||
ss_status_set() { record set "$1" "$2"; }
|
ss_status_set() { record set "$1" "$2"; }
|
||||||
ss_status_add_error() { record error "$1"; }
|
ss_status_add_error() { record error "$1"; }
|
||||||
|
# Capture each value for existing state assertions while recording that a
|
||||||
|
# whole group is now committed through one locked status-store operation.
|
||||||
|
ss_status_set_many() {
|
||||||
|
record batch set_many "$#"
|
||||||
|
while [ "$#" -ge 2 ]; do
|
||||||
|
record set "$1" "$2"
|
||||||
|
shift 2
|
||||||
|
done
|
||||||
|
}
|
||||||
ss_status_apply() {
|
ss_status_apply() {
|
||||||
record apply "$1" "${2:-}"
|
record apply "$1" "${2:-}"
|
||||||
return 0
|
return 0
|
||||||
@@ -388,6 +400,7 @@ ss_case_status_state() (
|
|||||||
[ "$1" -ge 0 ] 2>/dev/null
|
[ "$1" -ge 0 ] 2>/dev/null
|
||||||
}
|
}
|
||||||
ss_status_mark_failure 'artifact_download_failed'
|
ss_status_mark_failure 'artifact_download_failed'
|
||||||
|
ss_spec_assert_eq "$(grep -c "$(printf 'batch\tset_many')" "$CALLS")" '1'
|
||||||
grep -F "$(printf 'set\tstatus\terror')" "$CALLS" >/dev/null
|
grep -F "$(printf 'set\tstatus\terror')" "$CALLS" >/dev/null
|
||||||
grep -F "$(printf 'set\tlast_failure\t1000')" "$CALLS" >/dev/null
|
grep -F "$(printf 'set\tlast_failure\t1000')" "$CALLS" >/dev/null
|
||||||
grep -F "$(printf 'set\tlast_result\terror')" "$CALLS" >/dev/null
|
grep -F "$(printf 'set\tlast_result\terror')" "$CALLS" >/dev/null
|
||||||
@@ -405,6 +418,7 @@ ss_case_status_state() (
|
|||||||
! grep -F "$(printf 'set\tnext_refresh_at')" "$CALLS" >/dev/null
|
! grep -F "$(printf 'set\tnext_refresh_at')" "$CALLS" >/dev/null
|
||||||
: >"$CALLS"
|
: >"$CALLS"
|
||||||
ss_status_reset_health_fields
|
ss_status_reset_health_fields
|
||||||
|
ss_spec_assert_eq "$(grep -c "$(printf 'batch\tset_many')" "$CALLS")" '1'
|
||||||
for key in health_dnsmasq_binary health_dns_runtime health_artifact_download dnsmasq_min_version; do
|
for key in health_dnsmasq_binary health_dns_runtime health_artifact_download dnsmasq_min_version; do
|
||||||
grep -F "$(printf 'set\t%s\t' "$key")" "$CALLS" >/dev/null
|
grep -F "$(printf 'set\t%s\t' "$key")" "$CALLS" >/dev/null
|
||||||
done
|
done
|
||||||
@@ -467,8 +481,45 @@ ss_case_status_version() (
|
|||||||
ss_spec_assert_file_contains "$SS_SPEC_ROOT/Makefile" "echo '\$(PKG_VERSION)-r\$(PKG_RELEASE)' > \$(1)/usr/lib/safeshield/version"
|
ss_spec_assert_file_contains "$SS_SPEC_ROOT/Makefile" "echo '\$(PKG_VERSION)-r\$(PKG_RELEASE)' > \$(1)/usr/lib/safeshield/version"
|
||||||
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/share/rpcd/ucode/safeshield/core.uc" 'fs.readfile(`/usr/lib/${PKG_NAME}/version`)'
|
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/share/rpcd/ucode/safeshield/core.uc" 'fs.readfile(`/usr/lib/${PKG_NAME}/version`)'
|
||||||
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/share/rpcd/ucode/safeshield/status.uc" 'version: PKG_VERSION,'
|
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/share/rpcd/ucode/safeshield/status.uc" 'version: PKG_VERSION,'
|
||||||
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh" 'safeshield_upgrade_required'
|
|
||||||
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh" 'failure_code="$(ss_resolve_error_code)"'
|
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh" 'failure_code="$(ss_resolve_error_code)"'
|
||||||
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh" 'failure_code="$(ss_artifact_download_error_code)"'
|
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh" 'failure_code="$(ss_artifact_download_error_code)"'
|
||||||
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh" 'if [ "$failure_code" != "safeshield_upgrade_required" ]; then'
|
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh" 'No blocklist replacement has happened'
|
||||||
|
)
|
||||||
|
|
||||||
|
# The legacy refresh reset used one ucode process + one locked JSON rewrite
|
||||||
|
# per key. Grouping related updates keeps the same values with fewer writes.
|
||||||
|
ss_case_status_batch_writes() (
|
||||||
|
set -eu
|
||||||
|
TMP_DIR="$(ss_spec_tmpdir)"
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
|
||||||
|
RUNNING_STATUS_FILE="$TMP_DIR/status.json"
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/status.sh"
|
||||||
|
CALLS="$TMP_DIR/calls"
|
||||||
|
: >"$CALLS"
|
||||||
|
ss_status_apply() {
|
||||||
|
printf '%s\n' "$1" >>"$CALLS"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
ss_now() { printf '%s\n' 1000; }
|
||||||
|
is_valid_integer() {
|
||||||
|
case "$1" in '' | *[!0-9]*) return 1 ;; esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ss_status_reset
|
||||||
|
ss_spec_assert_eq "$(wc -l <"$CALLS" | tr -d '[:space:]')" '6'
|
||||||
|
ss_spec_assert_eq "$(grep -c '^set_many$' "$CALLS")" '4'
|
||||||
|
ss_spec_assert_eq "$(grep -c '^reset$' "$CALLS")" '1'
|
||||||
|
ss_spec_assert_eq "$(grep -c '^clear_messages$' "$CALLS")" '1'
|
||||||
|
|
||||||
|
: >"$CALLS"
|
||||||
|
ss_status_mark_success 300
|
||||||
|
ss_spec_assert_eq "$(wc -l <"$CALLS" | tr -d '[:space:]')" '2'
|
||||||
|
ss_spec_assert_file_line "$CALLS" 'set_many'
|
||||||
|
|
||||||
|
: >"$CALLS"
|
||||||
|
ss_status_mark_failure artifact_download_failed
|
||||||
|
ss_spec_assert_eq "$(wc -l <"$CALLS" | tr -d '[:space:]')" '2'
|
||||||
|
ss_spec_assert_file_line "$CALLS" 'add_error'
|
||||||
)
|
)
|
||||||
@@ -138,6 +138,7 @@ ss_case_refresh_state_machine() (
|
|||||||
ss_spec_assert_file_line "$CALLS" 'restore_and_restart'
|
ss_spec_assert_file_line "$CALLS" 'restore_and_restart'
|
||||||
else
|
else
|
||||||
! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null
|
! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null
|
||||||
|
! grep -F 'dnsmasq_restart ' "$CALLS" >/dev/null
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +158,10 @@ ss_case_refresh_state_machine() (
|
|||||||
ss_spec_assert_file_line "$CALLS" 'lock_close'
|
ss_spec_assert_file_line "$CALLS" 'lock_close'
|
||||||
! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null
|
! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null
|
||||||
|
|
||||||
assert_refresh_failure merge merge_failed 1
|
assert_refresh_failure merge merge_failed 0
|
||||||
|
assert_refresh_failure resolve artifact_resolve_failed 0
|
||||||
|
assert_refresh_failure download artifact_download_failed 0
|
||||||
|
assert_refresh_failure local_rules allowlist_build_failed 0
|
||||||
assert_refresh_failure install install_failed 1
|
assert_refresh_failure install install_failed 1
|
||||||
assert_refresh_failure restart dnsmasq_restart_failed 1
|
assert_refresh_failure restart dnsmasq_restart_failed 1
|
||||||
assert_refresh_failure runtime dns_runtime_check_failed 1
|
assert_refresh_failure runtime dns_runtime_check_failed 1
|
||||||
@@ -313,7 +317,12 @@ ss_case_local_apply_state_machine() (
|
|||||||
verify) code='local_blocklist_verification_failed' ;;
|
verify) code='local_blocklist_verification_failed' ;;
|
||||||
esac
|
esac
|
||||||
ss_spec_assert_file_line "$CALLS" "local_failure $code"
|
ss_spec_assert_file_line "$CALLS" "local_failure $code"
|
||||||
ss_spec_assert_file_line "$CALLS" 'restore_and_restart'
|
if [ "$scenario" = 'merge' ]; then
|
||||||
|
! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null
|
||||||
|
! grep -Fx 'dnsmasq_restart' "$CALLS" >/dev/null
|
||||||
|
else
|
||||||
|
ss_spec_assert_file_line "$CALLS" 'restore_and_restart'
|
||||||
|
fi
|
||||||
ss_spec_assert_file_line "$CALLS" 'lock_close'
|
ss_spec_assert_file_line "$CALLS" 'lock_close'
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -333,54 +342,42 @@ ss_case_refresh_locking() (
|
|||||||
set -eu
|
set -eu
|
||||||
TMP="$(ss_spec_tmpdir)"
|
TMP="$(ss_spec_tmpdir)"
|
||||||
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
|
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
|
||||||
RUNNING_STATUS_FILE="$TMP/status.json"
|
|
||||||
SS_REFRESH_LOCK="$TMP/safeshield-refresh.lock"
|
SS_REFRESH_LOCK="$TMP/safeshield-refresh.lock"
|
||||||
SS_REFRESH_LOCK_FD=9
|
# shellcheck disable=SC1090
|
||||||
MOCK_FLOCK_DIR="$TMP/mock-flock.lock"
|
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
# Keep this regression test portable on development hosts such as macOS,
|
|
||||||
# where flock(1) is not installed by default. The production code still
|
|
||||||
# invokes flock -n/-u; this shim emulates those operations with atomic
|
|
||||||
# mkdir/rmdir so contention, release, timeout, and termination paths are
|
|
||||||
# exercised without depending on a host-specific flock binary.
|
|
||||||
flock() {
|
|
||||||
case "${1:-}" in
|
|
||||||
-n)
|
|
||||||
mkdir "$MOCK_FLOCK_DIR" 2>/dev/null
|
|
||||||
;;
|
|
||||||
-u)
|
|
||||||
rmdir "$MOCK_FLOCK_DIR" 2>/dev/null || true
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
return 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/status.sh"
|
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/status.sh"
|
||||||
is_valid_integer() { case "$1" in '' | *[!0-9]*) return 1 ;; *) return 0 ;; esac }
|
is_valid_integer() { case "$1" in '' | *[!0-9]*) return 1 ;; *) return 0 ;; esac }
|
||||||
ss_should_stop() { return 1; }
|
ss_should_stop() { return 1; }
|
||||||
|
|
||||||
mkdir "$MOCK_FLOCK_DIR"
|
ss_refresh_lock_open
|
||||||
|
ss_spec_assert_eq "$(cat "${SS_REFRESH_LOCK}.d/pid")" "$$"
|
||||||
|
# A separate shell must not acquire or release our live lock.
|
||||||
|
! sh -c '. "$1"; ss_lock_try "$2"' sh "$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh" "${SS_REFRESH_LOCK}.d"
|
||||||
|
ss_refresh_lock_close
|
||||||
|
ss_spec_assert_not_exists "${SS_REFRESH_LOCK}.d"
|
||||||
|
|
||||||
|
# Simulate an owner which crashed after writing pid.
|
||||||
|
mkdir "${SS_REFRESH_LOCK}.d"
|
||||||
|
printf '99999999\n' >"${SS_REFRESH_LOCK}.d/pid"
|
||||||
|
ss_refresh_lock_open
|
||||||
|
ss_refresh_lock_close
|
||||||
|
|
||||||
|
# A freshly-created empty lock must not be stolen while the owner writes pid.
|
||||||
|
mkdir "${SS_REFRESH_LOCK}.d"
|
||||||
! ss_refresh_lock_open
|
! ss_refresh_lock_open
|
||||||
! ss_refresh_lock_open_wait 0
|
! ss_refresh_lock_open_wait 0
|
||||||
rmdir "$MOCK_FLOCK_DIR"
|
rmdir "${SS_REFRESH_LOCK}.d"
|
||||||
|
|
||||||
ss_refresh_lock_open
|
|
||||||
ss_spec_assert_exists "$MOCK_FLOCK_DIR"
|
|
||||||
ss_refresh_lock_close
|
|
||||||
ss_spec_assert_not_exists "$MOCK_FLOCK_DIR"
|
|
||||||
|
|
||||||
mkdir "$MOCK_FLOCK_DIR"
|
|
||||||
ss_should_stop() { return 0; }
|
ss_should_stop() { return 0; }
|
||||||
|
mkdir "${SS_REFRESH_LOCK}.d"
|
||||||
|
printf '%s\n' "$$" >"${SS_REFRESH_LOCK}.d/pid"
|
||||||
set +e
|
set +e
|
||||||
ss_refresh_lock_open_wait 10
|
ss_refresh_lock_open_wait 10
|
||||||
rc=$?
|
rc=$?
|
||||||
set -e
|
set -e
|
||||||
rmdir "$MOCK_FLOCK_DIR"
|
|
||||||
ss_spec_assert_eq "$rc" '130'
|
ss_spec_assert_eq "$rc" '130'
|
||||||
|
ss_refresh_lock_close
|
||||||
)
|
)
|
||||||
|
|
||||||
ss_case_service_lifecycle() (
|
ss_case_service_lifecycle() (
|
||||||
|
|||||||
Executable
+114
@@ -0,0 +1,114 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# shellcheck shell=sh
|
||||||
|
|
||||||
|
ss_case_lock_owner_and_contention() (
|
||||||
|
set -eu
|
||||||
|
TMP="$(ss_spec_tmpdir)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh"
|
||||||
|
LOCK="$TMP/collector.lock"
|
||||||
|
ss_lock_try "$LOCK"
|
||||||
|
[ "$(cat "$LOCK/pid")" = "$$" ]
|
||||||
|
# Different process may not acquire the lock or remove its owner.
|
||||||
|
! sh -c '. "$1"; ss_lock_try "$2"' sh "$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh" "$LOCK"
|
||||||
|
! sh -c '. "$1"; ss_lock_release "$2"' sh "$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh" "$LOCK"
|
||||||
|
[ "$(cat "$LOCK/pid")" = "$$" ]
|
||||||
|
ss_lock_release "$LOCK"
|
||||||
|
[ ! -e "$LOCK" ]
|
||||||
|
# Stale pid may be reclaimed by exactly one contender.
|
||||||
|
mkdir "$LOCK"
|
||||||
|
printf '99999999\n' >"$LOCK/pid"
|
||||||
|
ss_lock_try "$LOCK"
|
||||||
|
[ "$(cat "$LOCK/pid")" = "$$" ]
|
||||||
|
ss_lock_release "$LOCK"
|
||||||
|
# Do not seize a newly created lock before its pid has been populated.
|
||||||
|
mkdir "$LOCK"
|
||||||
|
! ss_lock_try "$LOCK"
|
||||||
|
[ -d "$LOCK" ]
|
||||||
|
rmdir "$LOCK"
|
||||||
|
# An abandoned pid-less directory (including a crashed recovery guard)
|
||||||
|
# becomes reclaimable after its initialization grace period.
|
||||||
|
mkdir "$LOCK" "${LOCK}.reclaim"
|
||||||
|
touch -t 202001010000 "$LOCK" "${LOCK}.reclaim"
|
||||||
|
ss_lock_try "$LOCK"
|
||||||
|
ss_lock_release "$LOCK"
|
||||||
|
)
|
||||||
|
|
||||||
|
ss_case_lock_parallel() (
|
||||||
|
set -eu
|
||||||
|
TMP="$(ss_spec_tmpdir)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
|
||||||
|
LIB="$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh"
|
||||||
|
LOCK="$TMP/write.lock"
|
||||||
|
RESULT="$TMP/results"
|
||||||
|
for id in 1 2 3 4 5 6 7 8; do
|
||||||
|
sh -c '
|
||||||
|
. "$1"
|
||||||
|
ss_lock_wait "$2" 15 || exit 1
|
||||||
|
printf "%s\n" "$3" >>"$4"
|
||||||
|
sleep 0.05
|
||||||
|
ss_lock_release "$2"
|
||||||
|
' sh "$LIB" "$LOCK" "$id" "$RESULT" &
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
[ "$(wc -l <"$RESULT")" -eq 8 ]
|
||||||
|
[ ! -e "$LOCK" ]
|
||||||
|
[ ! -e "${LOCK}.reclaim" ]
|
||||||
|
)
|
||||||
|
|
||||||
|
ss_case_status_write_serialization() (
|
||||||
|
set -eu
|
||||||
|
TMP="$(ss_spec_tmpdir)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
|
||||||
|
LIB="$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh"
|
||||||
|
STATUS="$SS_SPEC_ROOT/files/usr/lib/safeshield/status.sh"
|
||||||
|
# Test the real shell writer with a slow stand-in for the ucode serializer.
|
||||||
|
# A serializer must observe all previously committed updates under the same lock.
|
||||||
|
cat >"$TMP/writer.sh" <<'EOF_WRITER'
|
||||||
|
#!/bin/sh
|
||||||
|
. "$1"
|
||||||
|
. "$2"
|
||||||
|
RUNNING_STATUS_FILE="$3"
|
||||||
|
RUNNING_STATUS_LOCK="$4"
|
||||||
|
json_lock_open() { ss_lock_wait "$RUNNING_STATUS_LOCK" 10; }
|
||||||
|
json_lock_close() { ss_lock_release "$RUNNING_STATUS_LOCK"; }
|
||||||
|
ss_status_exec() {
|
||||||
|
key="$2"
|
||||||
|
[ -f "$RUNNING_STATUS_FILE" ] && cat "$RUNNING_STATUS_FILE"
|
||||||
|
sleep 0.05
|
||||||
|
printf '%s\n' "$key"
|
||||||
|
}
|
||||||
|
ss_status_apply set "$5" unused
|
||||||
|
EOF_WRITER
|
||||||
|
for n in 1 2 3 4 5 6 7 8; do
|
||||||
|
sh "$TMP/writer.sh" "$LIB" "$STATUS" "$TMP/status.json" "$TMP/status.lock" "$n" &
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
[ "$(wc -l <"$TMP/status.json")" -eq 8 ]
|
||||||
|
[ ! -e "$TMP/status.lock" ]
|
||||||
|
[ ! -e "$TMP/status.json.tmp" ]
|
||||||
|
)
|
||||||
|
|
||||||
|
ss_case_status_write_failure() (
|
||||||
|
set -eu
|
||||||
|
TMP="$(ss_spec_tmpdir)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/status.sh"
|
||||||
|
RUNNING_STATUS_LOCK="$TMP/status.lock"
|
||||||
|
RUNNING_STATUS_FILE="$TMP/status.json"
|
||||||
|
json_lock_open() { ss_lock_wait "$RUNNING_STATUS_LOCK" 10; }
|
||||||
|
json_lock_close() { ss_lock_release "$RUNNING_STATUS_LOCK"; }
|
||||||
|
printf 'original\n' >"$RUNNING_STATUS_FILE"
|
||||||
|
ss_status_exec() {
|
||||||
|
printf 'partial\n'
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
! ss_status_apply set key value
|
||||||
|
[ "$(cat "$RUNNING_STATUS_FILE")" = 'original' ]
|
||||||
|
[ ! -e "${RUNNING_STATUS_FILE}.tmp.$$" ]
|
||||||
|
[ ! -e "$RUNNING_STATUS_LOCK" ]
|
||||||
|
)
|
||||||
@@ -115,6 +115,7 @@ EOF_IP
|
|||||||
PATH="$TMP/bin:$PATH" \
|
PATH="$TMP/bin:$PATH" \
|
||||||
SS_STATSD_FUNCTIONS_LIB="$TMP/functions.sh" \
|
SS_STATSD_FUNCTIONS_LIB="$TMP/functions.sh" \
|
||||||
SS_STATSD_CORE_LIB="$TMP/core.sh" \
|
SS_STATSD_CORE_LIB="$TMP/core.sh" \
|
||||||
|
SS_STATSD_LOCK_LIB="$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh" \
|
||||||
SS_STATSD_AWK_DIR="$SS_SPEC_ROOT/files/usr/lib/safeshield/statistics" \
|
SS_STATSD_AWK_DIR="$SS_SPEC_ROOT/files/usr/lib/safeshield/statistics" \
|
||||||
SS_STATSD_POLL_COMMAND="$TMP/bin/stats-poll" \
|
SS_STATSD_POLL_COMMAND="$TMP/bin/stats-poll" \
|
||||||
SS_STATSD_GENERATION_ID='runtime-generation' \
|
SS_STATSD_GENERATION_ID='runtime-generation' \
|
||||||
@@ -580,6 +581,7 @@ EOF_CORE
|
|||||||
write_payload "$SS_STATISTICS_UPLOAD_JSON_FILE" 7
|
write_payload "$SS_STATISTICS_UPLOAD_JSON_FILE" 7
|
||||||
upload_pending_create
|
upload_pending_create
|
||||||
ss_spec_assert_file_contains "$SS_STATISTICS_UPLOAD_PENDING_META_FILE" "$(printf 'recent\tgeneration-upload\t7')"
|
ss_spec_assert_file_contains "$SS_STATISTICS_UPLOAD_PENDING_META_FILE" "$(printf 'recent\tgeneration-upload\t7')"
|
||||||
|
ss_should_terminate=0
|
||||||
MOCK_HTTP_MODE='stale'
|
MOCK_HTTP_MODE='stale'
|
||||||
export MOCK_HTTP_MODE
|
export MOCK_HTTP_MODE
|
||||||
upload_send_pending
|
upload_send_pending
|
||||||
|
|||||||
@@ -1272,3 +1272,37 @@ DATA
|
|||||||
! grep -F '"bucket_start":1787950800' "$UPLOAD" >/dev/null
|
! grep -F '"bucket_start":1787950800' "$UPLOAD" >/dev/null
|
||||||
ss_spec_assert_eq "$(sed -n 's/.*"snapshot_seq":\([0-9][0-9]*\).*/\1/p' "$UPLOAD")" "$(sed -n 's/.*"snapshot_seq":\([0-9][0-9]*\).*/\1/p' "$JSON")"
|
ss_spec_assert_eq "$(sed -n 's/.*"snapshot_seq":\([0-9][0-9]*\).*/\1/p' "$UPLOAD")" "$(sed -n 's/.*"snapshot_seq":\([0-9][0-9]*\).*/\1/p' "$JSON")"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Verifies that caching the retention sweep never leaves expired history in
|
||||||
|
# the local JSON, the persisted state, or the short cloud upload projection.
|
||||||
|
ss_case_statistics_retention_cache() (
|
||||||
|
set -eu
|
||||||
|
TMP="$(ss_spec_tmpdir)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
|
||||||
|
STATE="$TMP/state.tsv"
|
||||||
|
JSON="$TMP/statistics.json"
|
||||||
|
UPLOAD="$TMP/upload.json"
|
||||||
|
INPUT="$TMP/input.log"
|
||||||
|
LEASES="$TMP/dhcp.leases"
|
||||||
|
: >"$LEASES"
|
||||||
|
cat >"$INPUT" <<'DATA'
|
||||||
|
daemon.info dnsmasq[1]: query[A] one.example from 192.168.1.1
|
||||||
|
daemon.info dnsmasq[1]: query[A] two.example from 192.168.1.1
|
||||||
|
daemon.info dnsmasq[1]: query[A] three.example from 192.168.1.1
|
||||||
|
DATA
|
||||||
|
ss_statistics_awk \
|
||||||
|
-v state_file="$STATE" -v json_file="$JSON" \
|
||||||
|
-v upload_json_file="$UPLOAD" -v lease_file="$LEASES" \
|
||||||
|
-v snapshot_interval=1 -v retention_hours=2 \
|
||||||
|
-v upload_window_hours=1 -v provisional_retention_hours=2 \
|
||||||
|
-v generation_seed='retention-test' \
|
||||||
|
-v fixed_now=1787950800 -v fixed_step=3600 <"$INPUT"
|
||||||
|
|
||||||
|
# The final event is in the third hour. The two-hour retention window
|
||||||
|
# contains exactly the second and third events.
|
||||||
|
ss_spec_assert_file_contains "$JSON" '"totals":{"queries":2,"blocked":0}'
|
||||||
|
ss_spec_assert_file_contains "$JSON" '"retention_hours":2'
|
||||||
|
ss_spec_assert_file_contains "$UPLOAD" '"totals":{"queries":1,"blocked":0}'
|
||||||
|
! grep -F 'bucket' "$STATE" | grep -F '1787950800' >/dev/null
|
||||||
|
ss_spec_assert_file_contains "$STATE" "$(printf 'bucket\t1787958000\t1\t0')"
|
||||||
|
)
|
||||||
@@ -12,6 +12,8 @@ ss_case_tooling() (
|
|||||||
[ -f "$SS_SPEC_ROOT/spec/statistics_spec.sh" ]
|
[ -f "$SS_SPEC_ROOT/spec/statistics_spec.sh" ]
|
||||||
[ -f "$SS_SPEC_ROOT/spec/ucode_spec.sh" ]
|
[ -f "$SS_SPEC_ROOT/spec/ucode_spec.sh" ]
|
||||||
[ -f "$SS_SPEC_ROOT/spec/tooling_spec.sh" ]
|
[ -f "$SS_SPEC_ROOT/spec/tooling_spec.sh" ]
|
||||||
|
[ -f "$SS_SPEC_ROOT/spec/locking_spec.sh" ]
|
||||||
|
[ -f "$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh" ]
|
||||||
[ -f "$SS_SPEC_ROOT/scripts/lint.sh" ]
|
[ -f "$SS_SPEC_ROOT/scripts/lint.sh" ]
|
||||||
[ -f "$SS_SPEC_ROOT/.github/workflows/lint-shell.yml" ]
|
[ -f "$SS_SPEC_ROOT/.github/workflows/lint-shell.yml" ]
|
||||||
ss_spec_assert_file_contains "$SS_SPEC_ROOT/.pre-commit-config.yaml" 'entry: sh scripts/lint.sh'
|
ss_spec_assert_file_contains "$SS_SPEC_ROOT/.pre-commit-config.yaml" 'entry: sh scripts/lint.sh'
|
||||||
|
|||||||
@@ -48,6 +48,21 @@ ss_case_ucode() (
|
|||||||
out="$("$UCODE_BIN" "$STATUS_STORE" "$status_file" set stage running)"
|
out="$("$UCODE_BIN" "$STATUS_STORE" "$status_file" set stage running)"
|
||||||
printf '%s\n' "$out" >"$status_file"
|
printf '%s\n' "$out" >"$status_file"
|
||||||
printf '%s\n' "$out" | grep -F '"stage":"running"' >/dev/null
|
printf '%s\n' "$out" | grep -F '"stage":"running"' >/dev/null
|
||||||
|
# One invocation must update every pair without dropping existing status.
|
||||||
|
out="$("$UCODE_BIN" "$STATUS_STORE" "$status_file" set_many status ready stage 'done' count 0 optional '')"
|
||||||
|
printf '%s\n' "$out" >"$status_file"
|
||||||
|
printf '%s\n' "$out" | grep -F '"status":"ready"' >/dev/null
|
||||||
|
printf '%s\n' "$out" | grep -F '"stage":"done"' >/dev/null
|
||||||
|
printf '%s\n' "$out" | grep -F '"count":"0"' >/dev/null
|
||||||
|
printf '%s\n' "$out" | grep -F '"optional":""' >/dev/null
|
||||||
|
printf '%s\n' "$out" | grep -F '"stage":"running"' >/dev/null && return 1
|
||||||
|
# Reject malformed pair lists, never serialize partial changes.
|
||||||
|
if "$UCODE_BIN" "$STATUS_STORE" "$status_file" set_many incomplete >/dev/null 2>&1; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if "$UCODE_BIN" "$STATUS_STORE" "$status_file" set_many '' value >/dev/null 2>&1; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
out="$("$UCODE_BIN" "$STATUS_STORE" "$status_file" add_warning dnsmasq_warning)"
|
out="$("$UCODE_BIN" "$STATUS_STORE" "$status_file" add_warning dnsmasq_warning)"
|
||||||
printf '%s\n' "$out" >"$status_file"
|
printf '%s\n' "$out" >"$status_file"
|
||||||
printf '%s\n' "$out" | grep -F '"code":"dnsmasq_warning"' >/dev/null
|
printf '%s\n' "$out" | grep -F '"code":"dnsmasq_warning"' >/dev/null
|
||||||
|
|||||||
@@ -28,3 +28,15 @@ assert(result.devices[0].identified == true, 'device identified flag is normaliz
|
|||||||
assert(result.devices[0].queries == 50 && result.devices[0].blocked == 5, 'device counters are normalized');
|
assert(result.devices[0].queries == 50 && result.devices[0].blocked == 5, 'device counters are normalized');
|
||||||
|
|
||||||
print('ucode statistics tests: ok\n');
|
print('ucode statistics tests: ok\n');
|
||||||
|
|
||||||
|
// Summary is deliberately opt-in. It must not duplicate large per-device
|
||||||
|
// arrays while preserving the health/totals metadata used by lightweight UIs.
|
||||||
|
let summary = statistics.summary();
|
||||||
|
assert(summary.schema.name == result.schema.name && summary.schema.version == result.schema.version, 'summary preserves schema');
|
||||||
|
assert(summary.snapshot_seq == result.snapshot_seq && summary.generation_id == result.generation_id, 'summary preserves freshness');
|
||||||
|
assert(summary.enabled == result.enabled && summary.collector_running == result.collector_running, 'summary preserves service status');
|
||||||
|
assert(summary.source.backend == result.source.backend && summary.source.tracked_clients == result.source.tracked_clients, 'summary preserves source health');
|
||||||
|
assert(summary.totals.queries == result.totals.queries && summary.totals.blocked == result.totals.blocked, 'summary preserves totals');
|
||||||
|
assert(summary.device_limit == result.device_limit && summary.devices_truncated == result.devices_truncated, 'summary preserves device capacity metadata');
|
||||||
|
assert(summary.hourly == null && summary.devices == null, 'summary omits detailed arrays');
|
||||||
|
assert(length(result.hourly) == 2 && length(result.devices) == 1, 'full statistics remains unchanged after summary');
|
||||||
Reference in new issue
Block a user