fix(parental): resolve daily quota from profile in nft/singbox rules and harden cron tick invocation

This commit is contained in:
Dushnilin
2026-09-12 18:52:46 +03:00
parent d72d3de706
commit fadfbc2511
5 changed files with 93 additions and 8 deletions
+7 -3
View File
@@ -363,12 +363,16 @@ function run_agent(user_args) {
}
function main() {
let command = ARGV[0] || "";
let start_idx = 0;
while (start_idx < length(ARGV) && trim(as_string(ARGV[start_idx])) == "")
start_idx++;
let command = ARGV[start_idx] || "";
// agent command gets all remaining args passed verbatim
if (command == "agent") {
let user_args = [];
for (let i = 1; i < length(ARGV); i++)
for (let i = start_idx + 1; i < length(ARGV); i++)
push(user_args, ARGV[i]);
return run_agent(user_args);
}
@@ -380,7 +384,7 @@ function main() {
}
let user_args = [];
for (let i = 1; i < length(ARGV); i++)
for (let i = start_idx + 1; i < length(ARGV); i++)
push(user_args, ARGV[i]);
return run_spec(command, spec, user_args);
+21 -2
View File
@@ -959,6 +959,25 @@ function resolve_schedule_devices(schedule, profiles) {
return result;
}
function resolve_schedule_quota_minutes(schedule, profiles) {
let q = int(option(schedule, "daily_quota_minutes", 0));
if (q > 0) return q;
let prof_names = list_option(schedule, "profile");
if (length(prof_names) == 0) {
let single_p = option(schedule, "profile", "");
if (single_p != "") prof_names = [ single_p ];
}
for (let p_name in prof_names) {
for (let p in (profiles || [])) {
if (as_string(object_or_empty(p)[".name"]) == p_name && bool_option(p, "enabled", true)) {
let pq = int(option(p, "daily_quota_minutes", 0));
if (pq > 0) return pq;
}
}
}
return 0;
}
function nft_add_profile_doh_block_rules(profiles, table) {
if (!profiles || length(profiles) == 0)
return true;
@@ -1055,7 +1074,7 @@ function nft_add_schedule_rules_from_schedules(schedules, sections, table, profi
[ "meta", "hour", sprintf("\"%s\"-\"%s\"", interval[0], interval[1]) ] : null;
let base_match = nft_schedule_rule_base_match(dev_str, is_mac, family, days_args, time_args);
let quota_minutes = int(option(schedule, "daily_quota_minutes", 0));
let quota_minutes = resolve_schedule_quota_minutes(schedule, profiles);
let has_sched_domains = length(list_option(schedule, "blocked_domains")) > 0 || option(schedule, "blocked_domains", "") != "";
if ((target == "all" || (length(target_sec_names) == 0 && target != "sections" && target != "domains")) && !has_sched_domains && target != "domains") {
if (always_on && quota_minutes > 0)
@@ -1163,7 +1182,7 @@ function nft_add_dns_block_rules_from_schedules(schedules, table, profiles) {
let intervals = nft_schedule_time_intervals(start_time, end_time);
let days_args = nft_schedule_days_match_args(schedule);
let always_on = length(intervals) == 0;
let quota_minutes = int(option(schedule, "daily_quota_minutes", 0));
let quota_minutes = resolve_schedule_quota_minutes(schedule, profiles);
if (always_on && quota_minutes > 0)
continue;
@@ -239,6 +239,8 @@ function write_state(state) {
}
function device_active(ident) {
if (!ident || trim(as_string(ident)) == "")
return false;
for (let line in split(command_output_from_args([ "ip", "neigh", "show" ]), "\n")) {
line = as_string(line);
if (index(lc(line), ident) < 0)
@@ -289,12 +291,20 @@ function nft_insert_quota_rule(chain, match_type) {
return rc == 0;
}
function nft_chain_exists(chain) {
let rc = command_status(command_from_args([ "nft", "list", "chain", "inet", NFT_TABLE_NAME, chain ]) + " >/dev/null 2>&1");
return rc == 0;
}
function ensure_nft_rules() {
// The whole table disappears on every Tachyon nft rebuild; recreate our
// pieces on demand so ticks self-heal without touching apply.uc ordering.
if (!nft_table_exists())
return false;
if (!nft_chain_exists("parental_control") || !nft_chain_exists("parental_forward"))
return false;
// Create quota sets if missing
if (!nft_set_exists(SET_ETHER))
nft_create_set(SET_ETHER, "ether_addr");
@@ -343,10 +353,13 @@ function cron_line() {
function write_crontab(lines) {
let text = "";
for (let i = 0; i < length(lines); i++) {
if (as_string(lines[i]) == "" && i == length(lines) - 1)
let l = trim(as_string(lines[i]));
if (l == "")
continue;
text += as_string(lines[i]) + "\n";
text += l + "\n";
}
if (text == "")
text = "\n";
let tmp = "/tmp/tachyon-parental-cron." + as_string(int(time()));
if (!fs.writefile(tmp, text))
return false;
@@ -559,7 +572,11 @@ function reset_quotas() {
return 0;
}
let mode = ARGV[0] || "";
let mode = "";
for (let a in ARGV) {
let t = trim(as_string(a));
if (t != "") { mode = t; break; }
}
if (mode == "tick")
exit(tick());
@@ -945,8 +945,22 @@ function schedule_blocked_domains(schedule) {
function enabled_content_block_schedules() {
let result = [];
let profiles_by_name = {};
ctx.uci_cursor().foreach(CONFIG_NAME, "profile", function(profile) {
if (section_enabled(profile))
profiles_by_name[profile[".name"]] = profile;
});
ctx.uci_cursor().foreach(CONFIG_NAME, "schedule", function(schedule) {
let quota_minutes = int(option(schedule, "daily_quota_minutes", 0));
if (quota_minutes <= 0) {
for (let prof_ref in list_option(schedule, "profile")) {
let p = profiles_by_name[prof_ref];
if (p) {
let pq = int(option(p, "daily_quota_minutes", 0));
if (pq > 0) { quota_minutes = pq; break; }
}
}
}
let has_interval = trim(option(schedule, "start_time", "")) != "" || trim(option(schedule, "end_time", "")) != "";
if (section_enabled(schedule) && length(schedule_blocked_domains(schedule)) > 0) {
if (!has_interval && quota_minutes > 0)
+31
View File
@@ -322,5 +322,36 @@ if grep -Eq "parental_(forward|control).*192\.168\.1\.181.*counter.*drop" "$NFT_
fail "Schedule with target=domains must NOT add global counter drop in parental_forward/parental_control"
fi
# ─── 24/7 Schedule referencing Profile with daily_quota_minutes must NOT drop statically
cat >"$WORK_DIR/quota_schedule_fixture.json" <<'JSON'
{
"profile": [
{
".name": "prof_quota_only",
"enabled": "1",
"device_ip": [ "192.168.1.222" ],
"daily_quota_minutes": "120"
}
],
"schedule": [
{
".name": "quota_24_7_sched",
"enabled": "1",
"profile": [ "prof_quota_only" ],
"target": "all",
"action": "block"
}
]
}
JSON
rm -f "$NFT_LOG"
touch "$NFT_LOG"
nft_ucode nft-add-schedule-rules-fixture "$WORK_DIR/quota_schedule_fixture.json" "tachyon"
if grep -Eq "parental_(forward|control).*192\.168\.1\.222.*counter.*drop" "$NFT_LOG"; then
fail "24/7 Schedule with profile daily_quota_minutes must NOT add static counter drop"
fi
printf 'Parental control, profiles and schedule tests passed\n'