warp-window: open Mon/Thu/Fri 08:00-16:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWYHStUbKpAMzi4hdEV
This commit is contained in:
Krzysztof Rudnicki 2026-11-05 09:15:17 +01:00
parent e2bccc4b3c
commit 8ec00f2b89
9 changed files with 86 additions and 83 deletions

View File

@ -120,8 +120,8 @@ with no internet. So the laptop runs `warp-home` (user timer, every 30 s): at
home with no window open it turns WARP off, and plain internet with the home with no window open it turns WARP off, and plain internet with the
blocklist comes back. On any other network (office) it never touches WARP. blocklist comes back. On any other network (office) it never touches WARP.
- Mon/Fri 09:00-15:30 at home: `warp-home on` asks the PC, then connects. - Mon/Thu/Fri 08:00-16:00 at home: `warp-home on` asks the PC, then connects.
The PC opens WARP for the LAN until 15:30 sharp, then closes by itself. The PC opens WARP for the LAN until 16:00 sharp, then closes by itself.
Outside that window the PC refuses. `warp-home off` / `warp-home status`. Outside that window the PC refuses. `warp-home off` / `warp-home status`.
- Window closed early (the PC firewall was re-applied): `warp-home on` again. - Window closed early (the PC firewall was re-applied): `warp-home on` again.
- PC side by hand: `sudo warp-window open|close|status`. - PC side by hand: `sudo warp-window open|close|status`.
@ -139,4 +139,4 @@ blocklist comes back. On any other network (office) it never touches WARP.
real IPv6 address); Firefox DoH canary + DoH hostnames blocked; DHCP lease real IPv6 address); Firefox DoH canary + DoH hostnames blocked; DHCP lease
12h -> 2h; gateway mode added (nextcloud-pi and the P110 plug exempt). 12h -> 2h; gateway mode added (nextcloud-pi and the P110 plug exempt).
- 2026-10-25: WARP tunnel dropped at the gateway (work laptop bypassed the - 2026-10-25: WARP tunnel dropped at the gateway (work laptop bypassed the
DNS block through it); Mon/Fri 09:00-15:30 window via `warp-home on`. DNS block through it); Mon/Thu/Fri 08:00-16:00 window via `warp-home on`.

View File

@ -0,0 +1,14 @@
{
"title": "WARP home window: add Thursday, widen to 08:00-16:00",
"objective": "kuhy can run `warp-home on` on the work laptop at home Mon/Thu/Fri 08:00-16:00 (was Mon/Fri 09:00-15:30); the PC's clock still decides, and WARP stays blocked at home outside that window.",
"acceptance_criteria": [
"warp_window.sh open succeeds only Mon/Thu/Fri 08:00:00-15:59:59 and fills warp_open with a timeout ending at 16:00; Tue/Wed/Sat/Sun stay closed (test_warp_window.sh)",
"warp_home.sh messages name Mon/Thu/Fri 08:00-16:00 and the 16:00 cut-off (test_warp_home.sh)",
"Live: on Thursday `warp-home on` from the laptop opens the PC window until 16:00 and WARP connects"
],
"out_of_scope": [
"Install path, sudoers entry and authorized_keys forced command (unchanged)",
"Jail-only suites in features/lib/tests (dwm, nc_php, rpi_nc, dot_resolver_install, website_admin)"
],
"verifier": "linux_configuration/features/lib/tests/test_warp_window.sh; linux_configuration/features/lib/tests/test_warp_home.sh; on the laptop: warp-home on && warp-home status"
}

View File

@ -1,51 +0,0 @@
{
"intent": "Make `git push` return in well under a second instead of 22s-to-minutes, by taking the CI mirror off the push's critical path without giving up the guarantee it exists for.",
"scope": [
"meta/scripts/ci_mirror_async.sh - new: verdict bookkeeping, the detached worker, and the green-tree short-circuit moved here",
"meta/scripts/ci_mirror.sh - main() gains three modes (worker / sync / async); the gates themselves are unchanged",
".pre-commit-config.yaml - hook name now describes the background behaviour",
"AGENTS.md (CLAUDE.md symlinks to it) - documents the new pre-push contract",
"meta/requirements.txt - matplotlib 3.11.2, which went red on main after release",
"Non-goal: changing which gates run or what they check. A verified tree is verified by exactly the same commands as before."
],
"changes": [
"`git push` spawns a detached, niced (nice -n 19, ionice -c 3) worker for HEAD's tree and returns. Measured at 0.028s.",
"The worker records a verdict of ok or fail for the tree it checked, plus the full log, under .ci-mirror-venv/verdicts/.",
"A recorded failure refuses the NEXT push, printing the last 40 lines of the failing log and the foreground escape hatch, so a broken tree can reach GitHub at most once - where CI sees the same commit within minutes.",
"CI_MIRROR_SYNC=1 restores the old blocking behaviour for a push that must be verified before it lands.",
"Workers serialise on a flock rather than skipping when one is already in flight: skipping would have left a newly pushed tree permanently unverified, which was a hole in the first draft of this patch.",
"The green-tree short-circuit is unchanged in behaviour - a tree that already passed still costs nothing to re-push."
],
"verification": [
{
"command": "PRE_COMMIT_FROM_REF=42e41319~1 PRE_COMMIT_TO_REF=42e41319 bash meta/scripts/ci_mirror.sh # async push path",
"result": "pass",
"evidence": "0.028s total, printing 'verification running in the background (tree 68346b01e46a)'; `pgrep` confirmed the detached worker was running after the command returned."
},
{
"command": "block_on_previous_failure with a synthetic 'fail' verdict in an isolated VENV_DIR",
"result": "pass",
"evidence": "exit 1, the failing log's tail echoed to stderr, and the CI_MIRROR_SYNC=1 hint printed. With the verdict flipped to 'ok' the same call passes straight through."
},
{
"command": "timing baseline before the change: bash meta/scripts/ci_mirror.sh with the green marker cleared",
"result": "pass",
"evidence": "22.6s for a 6-file scoped range with a warm venv; 70.8s when requirements.txt had changed and the clean venv was rebuilt. The clean worktree checkout itself is only 0.048s, so the cost is the verification work, not overhead."
},
{
"command": "shellcheck -x -S warning meta/scripts/ci_mirror.sh meta/scripts/ci_mirror_async.sh",
"result": "pass",
"evidence": "no findings; shfmt applied. ci_mirror.sh is 244 lines, under the repo's 250-line cap."
}
],
"risks": [
"A commit that fails the mirror can reach GitHub once, before the verdict lands. This is the tradeoff kuhy chose explicitly over a 22s-to-minutes blocking push; GitHub CI and the next push both catch it.",
"Several pushes in quick succession queue workers that serialise on the lock, so a verdict can lag the push by the length of the runs ahead of it. Each worker still checks its own tree.",
"The worker is detached with setsid, so it survives the terminal that started it. It is niced and ionice idle, so it yields to interactive work, but it will still be running after the push prompt returns.",
"Paths in ci_mirror_async.sh are computed in functions because the file is sourced before ci_mirror.sh defines VENV_DIR; defining them as readonly constants there would silently resolve to /verdicts."
],
"rollback": [
"git revert the commit. ci_mirror.sh's main() returns to calling ensure_venv / run_gates_in_clean_worktree / record_tree_green inline, and pushes block again.",
"After rollback, delete .ci-mirror-venv/verdicts/, which is then unused, and confirm `git push` still runs the gate in the foreground."
]
}

View File

@ -0,0 +1,37 @@
{
"intent": "The home WARP window for the work laptop opens Mon/Thu/Fri 08:00-16:00 instead of Mon/Fri 09:00-15:30.",
"scope": [
"linux_configuration/features/warp_window.sh (PC, enforces the window)",
"linux_configuration/features/warp_home.sh (laptop, messages only)",
"lib/wg_gateway.sh comment, DOCS-lan-blocker-contingency.md, both test files",
"No change to install, sudoers or the ssh forced command"
],
"changes": [
"WINDOW_DAYS ' 1 5 ' -> ' 1 4 5 '; OPEN_SEC 09:00 -> 08:00; CLOSE_SEC 15:30 -> 16:00; refusal/open messages updated",
"test_warp_window.sh asserts the 07:59:59/08:00/15:59:59/16:00 edges, Thursday open, Tue/Wed/Sat/Sun closed, 21600 s timeout at Mon 10:00"
],
"verification": [
{
"command": "linux_configuration/features/lib/tests/test_warp_window.sh (before the change)",
"result": "fail",
"evidence": "red first: 27 passed, 8 failed (Thursday closed, 08:00 closed, 12600 instead of 14400)"
},
{
"command": "linux_configuration/features/lib/tests/test_warp_window.sh; test_warp_home.sh",
"result": "pass",
"evidence": "35 passed, 0 failed; 35 passed, 0 failed"
},
{
"command": "sudo install -m 0755 warp_window.sh /usr/local/sbin/warp-window; laptop 192.168.1.21: ~/.local/bin/warp-home replaced (sha256 bf242be5...), then warp-home on; warp-home status",
"result": "pass",
"evidence": "Thu 2026-11-05 09:11: 'Home window ends at 16:00', pc: open (expires 6h48m), warp Connected, example.com 200; kuhy confirmed"
}
],
"risks": [
"Laptop copy is installed by hand; a later warp_home.sh install overwrites it with the repo version (same content now)"
],
"rollback": [
"PC: sudo cp -a /usr/local/sbin/warp-window.bak-2026-11-05 /usr/local/sbin/warp-window; laptop: restore ~/.local/bin/warp-home.bak-2026-11-05",
"Then: sudo warp-window status shows closed outside Mon/Fri 09:00-15:30"
]
}

View File

@ -120,7 +120,7 @@ _t_not "the early-closed window is forgotten" test -e "$OPEN_UNTIL"
SSH_OUT="seconds_left=600" SSH_OUT="seconds_left=600"
printf '\n-- on: a refusal connects nothing --\n' printf '\n-- on: a refusal connects nothing --\n'
SSH_OUT="WARP window is Mon/Fri 09:00-15:30 only" SSH_OUT="WARP window is Mon/Thu/Fri 08:00-16:00 only"
SSH_RC=1 SSH_RC=1
reset_calls reset_calls
on_refused() { cmd_on; } on_refused() { cmd_on; }

View File

@ -2,7 +2,7 @@
# Tests for warp_window.sh -- the PC side of the work-from-home WARP window. # Tests for warp_window.sh -- the PC side of the work-from-home WARP window.
# #
# Gateway mode drops the Cloudflare WARP tunnel because it carries everything # Gateway mode drops the Cloudflare WARP tunnel because it carries everything
# past the DNS block. kuhy may open it only Mon/Fri 09:00-15:30; the laptop # past the DNS block. kuhy may open it only Mon/Thu/Fri 08:00-16:00; the laptop
# asks over an SSH key whose forced command can run nothing but open/status. # asks over an SSH key whose forced command can run nothing but open/status.
# The window edges and the forced-command filter are the security boundary, # The window edges and the forced-command filter are the security boundary,
# so both are asserted exactly. The clock and nft are stubbed. # so both are asserted exactly. The clock and nft are stubbed.
@ -28,23 +28,26 @@ left_at() {
window_seconds_left || printf 'closed' window_seconds_left || printf 'closed'
} }
printf '\n-- the window is Mon/Fri 09:00-15:30, nothing else --\n' printf '\n-- the window is Mon/Thu/Fri 08:00-16:00, nothing else --\n'
_t_eq "closed" "$(left_at '1 08 59 59')" "Monday 08:59:59 is closed" _t_eq "closed" "$(left_at '1 07 59 59')" "Monday 07:59:59 is closed"
_t_eq "23400" "$(left_at '1 09 00 00')" "Monday 09:00 opens until 15:30" _t_eq "28800" "$(left_at '1 08 00 00')" "Monday 08:00 opens until 16:00"
_t_eq "1" "$(left_at '1 15 29 59')" "Monday 15:29:59 has one second left" _t_eq "1" "$(left_at '1 15 59 59')" "Monday 15:59:59 has one second left"
_t_eq "closed" "$(left_at '1 15 30 00')" "Monday 15:30 is closed" _t_eq "closed" "$(left_at '1 16 00 00')" "Monday 16:00 is closed"
_t_eq "12600" "$(left_at '5 12 00 00')" "Friday noon is open" _t_eq "14400" "$(left_at '5 12 00 00')" "Friday noon is open"
for day in 2 3 4 6 7; do _t_eq "28800" "$(left_at '4 08 00 00')" "Thursday 08:00 opens until 16:00"
_t_eq "closed" "$(left_at '4 16 00 00')" "Thursday 16:00 is closed"
for day in 2 3 6 7; do
_t_eq "closed" "$(left_at "$day 12 00 00")" "weekday $day noon is closed" _t_eq "closed" "$(left_at "$day 12 00 00")" "weekday $day noon is closed"
done done
_t_eq "closed" "$(left_at '5 08 09 00')" "a leading-zero hour is decimal, not octal" _t_eq "28260" "$(left_at '5 08 09 00')" "a leading-zero hour is decimal, not octal"
printf '\n-- open fills the set with a timeout ending at 15:30 --\n' printf '\n-- open fills the set with a timeout ending at 16:00 --\n'
FAKE_DATE="1 10 00 00" FAKE_DATE="1 10 00 00"
out="$(cmd_open)" out="$(cmd_open)"
_t_called 'nft add element inet filter warp_open { 0.0.0.0/0 timeout 19800s }' \ _t_called 'nft add element inet filter warp_open { 0.0.0.0/0 timeout 21600s }' \
"open adds the catch-all element with the time left" "open adds the catch-all element with the time left"
_t_has "$out" "seconds_left=19800" "open reports the seconds left for the laptop" _t_has "$out" "seconds_left=21600" "open reports the seconds left for the laptop"
_t_has "$out" "until 16:00" "open names the 16:00 cut-off"
printf '\n-- outside the window open refuses and touches nothing --\n' printf '\n-- outside the window open refuses and touches nothing --\n'
: >"$TEST_TMPDIR/calls.log" : >"$TEST_TMPDIR/calls.log"

View File

@ -26,7 +26,7 @@ readonly WARP_V4="162.159.192.0/24, 162.159.193.0/24, 162.159.197.0/24"
# No LAN-wide drop of UDP 500/1701/4500: that would break IPsec for everyone. # No LAN-wide drop of UDP 500/1701/4500: that would break IPsec for everyone.
readonly WARP_UDP_V4="162.159.192.0/22, 162.159.204.0/22, 188.114.96.0/22" readonly WARP_UDP_V4="162.159.192.0/22, 162.159.204.0/22, 188.114.96.0/22"
# Sources in warp_open skip both WARP drops. Empty by default; warp_window.sh # Sources in warp_open skip both WARP drops. Empty by default; warp_window.sh
# fills it Mon/Fri 09:00-15:30 with an element that times out by itself. Any # fills it Mon/Thu/Fri 08:00-16:00 with an element that times out by itself. Any
# re-apply of this ruleset empties it again. # re-apply of this ruleset empties it again.
# Fill in LAN_IFACE/LAN_IP from the default route unless the caller set them. # Fill in LAN_IFACE/LAN_IP from the default route unless the caller set them.

View File

@ -9,7 +9,7 @@
# plain internet (with the DNS blocklist) comes back. On any other network, # plain internet (with the DNS blocklist) comes back. On any other network,
# e.g. the office, it never touches WARP. # e.g. the office, it never touches WARP.
# #
# warp-home on Mon/Fri 09:00-15:30 at home: ask the PC, then connect. # warp-home on Mon/Thu/Fri 08:00-16:00 at home: ask the PC, then connect.
# Anywhere else: just connect. # Anywhere else: just connect.
# warp-home off disconnect and forget the window # warp-home off disconnect and forget the window
# warp-home status network, window, WARP and the PC's view # warp-home status network, window, WARP and the PC's view
@ -23,7 +23,7 @@
set -euo pipefail set -euo pipefail
readonly PC_IP="192.168.1.43" PC_MAC="9c:6b:00:5c:c5:6d" PC_USER="kuhy" readonly PC_IP="192.168.1.43" PC_MAC="9c:6b:00:5c:c5:6d" PC_USER="kuhy"
readonly WARN_SEC=300 # warn five minutes before the 15:30 cut-off readonly WARN_SEC=300 # warn five minutes before the 16:00 cut-off
KEY="$HOME/.ssh/warp_window_ed25519" KEY="$HOME/.ssh/warp_window_ed25519"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/warp-home" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/warp-home"
OPEN_UNTIL="$STATE_DIR/open-until" OPEN_UNTIL="$STATE_DIR/open-until"
@ -70,17 +70,17 @@ warp_off_because() {
cmd_tick() { cmd_tick() {
at_home || return 0 at_home || return 0
window_open || { window_open || {
warp_off_because "WARP turned off at home. 'warp-home on' works Mon/Fri 09:00-15:30." warp_off_because "WARP turned off at home. 'warp-home on' works Mon/Thu/Fri 08:00-16:00."
return 0 return 0
} }
# Any re-apply of the PC firewall empties its set before 15:30. Only an # Any re-apply of the PC firewall empties its set before 16:00. Only an
# explicit "closed" counts: an unreachable PC proves nothing. # explicit "closed" counts: an unreachable PC proves nothing.
if [[ $(pc status 2>/dev/null) == closed ]]; then if [[ $(pc status 2>/dev/null) == closed ]]; then
warp_off_because "The PC closed the WARP window early; run 'warp-home on' again." warp_off_because "The PC closed the WARP window early; run 'warp-home on' again."
return 0 return 0
fi fi
if (($(<"$OPEN_UNTIL") - $(date +%s) <= WARN_SEC)) && [[ ! -e $WARNED ]]; then if (($(<"$OPEN_UNTIL") - $(date +%s) <= WARN_SEC)) && [[ ! -e $WARNED ]]; then
notify "WARP closes at 15:30, in five minutes." notify "WARP closes at 16:00, in five minutes."
touch "$WARNED" touch "$WARNED"
fi fi
} }
@ -96,7 +96,7 @@ cmd_on() {
printf '%s\n' "$(($(date +%s) + left))" >"$OPEN_UNTIL" printf '%s\n' "$(($(date +%s) + left))" >"$OPEN_UNTIL"
fi fi
warp connect >/dev/null warp connect >/dev/null
printf 'WARP connecting.%s\n' "$(at_home && printf ' Home window ends at 15:30.')" printf 'WARP connecting.%s\n' "$(at_home && printf ' Home window ends at 16:00.')"
} }
cmd_off() { cmd_off() {

View File

@ -4,9 +4,9 @@
# #
# Gateway mode (setup_dns_blocker.sh gateway) drops the WARP tunnel, because # Gateway mode (setup_dns_blocker.sh gateway) drops the WARP tunnel, because
# WARP carries all traffic past the DNS block. This opens it for every LAN # WARP carries all traffic past the DNS block. This opens it for every LAN
# device, but ONLY Mon/Fri 09:00-15:30 by this PC's clock: the open adds one # device, but ONLY Mon/Thu/Fri 08:00-16:00 by this PC's clock: the open adds one
# element to the nft set warp_open whose timeout ends at 15:30, so the kernel # element to the nft set warp_open whose timeout ends at 16:00, so the kernel
# closes it again with nothing left to fail. The office days (Tue-Thu) need # closes it again with nothing left to fail. The office days (Tue/Wed) need
# nothing here: the home gateway never touches the work network. # nothing here: the home gateway never touches the work network.
# #
# The laptop (warp_home.sh) asks over an SSH key whose forced command is # The laptop (warp_home.sh) asks over an SSH key whose forced command is
@ -22,8 +22,8 @@
set -euo pipefail set -euo pipefail
readonly WINDOW_DAYS=" 1 5 " # date +%u: Monday, Friday readonly WINDOW_DAYS=" 1 4 5 " # date +%u: Monday, Thursday, Friday
readonly OPEN_SEC=$((9 * 3600)) CLOSE_SEC=$((15 * 3600 + 30 * 60)) readonly OPEN_SEC=$((8 * 3600)) CLOSE_SEC=$((16 * 3600))
readonly NFT_SET="inet filter warp_open" readonly NFT_SET="inet filter warp_open"
readonly KEY_FROM="192.168.1.0/24" # the key works only from the LAN readonly KEY_FROM="192.168.1.0/24" # the key works only from the LAN
INSTALL_PATH="/usr/local/sbin/warp-window" INSTALL_PATH="/usr/local/sbin/warp-window"
@ -36,8 +36,8 @@ die() {
exit 1 exit 1
} }
# Seconds until 15:30 while the window is open; fails when it is closed. # Seconds until 16:00 while the window is open; fails when it is closed.
# Plain arithmetic, not `date -d 15:30`: DST never changes on a Mon/Fri day. # Plain arithmetic, not `date -d 16:00`: DST never changes on a window day.
window_seconds_left() { window_seconds_left() {
local day h m s now local day h m s now
read -r day h m s <<<"$(date '+%u %H %M %S')" read -r day h m s <<<"$(date '+%u %H %M %S')"
@ -49,13 +49,13 @@ window_seconds_left() {
cmd_open() { cmd_open() {
local left local left
left="$(window_seconds_left)" || die "WARP window is Mon/Fri 09:00-15:30 only." left="$(window_seconds_left)" || die "WARP window is Mon/Thu/Fri 08:00-16:00 only."
# Re-opening must refresh the timeout, and add keeps an old one: flush first. # Re-opening must refresh the timeout, and add keeps an old one: flush first.
# shellcheck disable=SC2086 # NFT_SET is three words on purpose # shellcheck disable=SC2086 # NFT_SET is three words on purpose
nft flush set $NFT_SET 2>/dev/null || die "no warp_open set: is gateway mode on?" nft flush set $NFT_SET 2>/dev/null || die "no warp_open set: is gateway mode on?"
# shellcheck disable=SC2086 # shellcheck disable=SC2086
nft add element $NFT_SET "{ 0.0.0.0/0 timeout ${left}s }" nft add element $NFT_SET "{ 0.0.0.0/0 timeout ${left}s }"
printf 'WARP open for the LAN until 15:30.\nseconds_left=%s\n' "$left" printf 'WARP open for the LAN until 16:00.\nseconds_left=%s\n' "$left"
} }
cmd_close() { cmd_close() {