Repository navigation
Gatekeeper v1.2
Added
- Anti-Randomization feature flag (
block_randomized_macs) - Newgatekeeper.main.block_randomized_macsUCI option (default0). When enabled, devices using a locally-administered (randomized) MAC address are denied outright instead of prompting for approval. Per IEEE 802.11, a MAC is locally administered when its second hex digit is2,6,A, orE(e.g.x2:xx:xx:xx:xx:xx) — the bit iOS Private Wi-Fi Address and Android MAC randomization always set. Randomized clients are added to thedenied_macsnftables set with a 24h timeout (so the firewall drops them and they don't re-trigger notifications on every DHCP renewal), logged asblocked-randomized-mac, and announced via a Telegram message. Static DHCP leases are always honored, so trusted devices keep working even if they happen to use a randomized address. The gate runs before blacklist auto-approve and schedule auto-approve so a randomized MAC can never slip through those paths. Toggle via TelegramRANDBLOCK ON|OFF|STATUS, or via the LuCI Settings page (new toggle) / Overview mode badge. Covered bytests/test_randomized_mac.sh. - OUI vendor enrichment (maclookup.app API) - On every new-device notification,
gatekeeper.shlooks up the issuing OUI vendor via the maclookup.app public API (https://api.maclookup.app/v2/macs/<mac>) and appends the company + ISO country code to the Telegram⚠️ New Device Connectionmessage. When the device reports no hostname of its own, the vendor name is cached in/tmp/mac_namessoSTATUS/DSTATUSlist it by vendor instead of falling back toUnknown Device/Guest(and the LuCI Devices page benefits too, since it reads the same cache). The API classifies a MAC as randomized (isRand) using the same locally-administered bit as the offlineis_randomized_macheuristic; randomized MACs are skipped (no vendor). Results are cached per-OUI under/tmp/gatekeeper_oui(one API call per vendor, ever — well within the 10 req/s rate limit), only definitive (success:true) responses are cached so transient errors retry, and any network/parse failure degrades gracefully (the notification still goes out without vendor info). The lookup helperlookup_mac_vendorlives ingatekeeper.shand is mirrored intests/test_maclookup.sh(stubbedcurl, offline). Requiresjq, already a runtime dependency. - Telegram
RESTARTcommand - Authorized Telegram chat can now restart only Gatekeeper services without rebooting the router. The bot acknowledges first, then restartsgatekeeper_init,gatekeeper_trigger_listener, and finallytg_gatekeeperso the polling process can exit after the response is sent. Covered by the sandboxed Telegram system-command test, which asserts the router reboot path is not called. - Telegram
REBOOTcommand - Authorized Telegram chat can now reboot the router with a guarded two-step flow: sendREBOOT, then replyREBOOT YESto the confirmation prompt within 2 minutes. Pending confirmation state is stored in/tmp/gatekeeper_reboot_pending, cleared after confirmation or expiry, and covered bytests/test_reboot_command.shusing a sandboxed fake reboot executable. - Changelog-based GitHub release notes - Tag builds now extract release body text from
CHANGELOG.mdbefore callingsoftprops/action-gh-release: the workflow prefers a section matching the pushed tag version (for example## [1.1.0]) and falls back to## [Unreleased]if no matching version section exists. The extracted text is passed throughbody_path, while GitHub-generated notes remain enabled for the automatic compare summary.
Fixed
- Scheduled auto-approve silently no-op'd on the router.
window_active_now(ingatekeeper.sh,tg_bot.sh, and the test helper) calleddate -d "today $stop" +%s/date -d "tomorrow $stop" +%s— GNU coreutils relative-date syntax that BusyBoxdatedoes NOT support. BusyBox printeddate: invalid date 'today 23:59'to stderr and wrote nothing to stdout, socheck_active_schedule_for_macsaw an emptyend_epoch, skipped every schedule, andgatekeeper.shstep 3.6 fell through to the regular notification path. Effect: every scheduled MAC sent a Telegram approval request inside its window instead of being silently auto-approved.scheduler_tickintg_bot.shwas equally broken, so/tmp/sched_activestayed empty and SCHEDLIST never showed the⏰ activetag. Fix: switched todate -d "$(date +%Y-%m-%d) HH:MM:00" +%s(theYYYY-MM-DD hh:mm:ssform is in BusyBox's documented-dgrammar AND is GNU-compatible). Cross-midnight branch now usestoday_stop_epoch + 86400instead ofdate -d "tomorrow $stop". All 28 schedule-helper unit tests still pass. Bothgatekeeper.shandtg_bot.shmust be redeployed for the fix to take effect.
Added
tests/test_busybox_compat.sh- Static-analysis test that scans every router-side script for known BusyBox-incompatible patterns (GNUdate -d "today/tomorrow/...",[[ ]],${var,,}/${var^^}, process substitution<(...),functionkeyword, bash arrays,grep -P, and the#!/bin/bashshebang). Run before every commit that touches a router-side script. Each match cites the file, line, exact content, and a one-line "why" explaining the BusyBox limitation. Catches the exact class of regression that shipped the brokendate -d "today $stop"call — would have failed CI/local before merge.
Added
-
LuCI Web UI (
luci-app-gatekeeper) - Major new feature. Browser-based admin interface as a sibling ipk to the runtime package. Independent of the Telegram bot — installs alongside, reads/writes the same UCI + nftables state, and works with the bot running, stopped, or never installed at all.- Six pages under
Services → Gatekeeper: Overview, Devices, Blacklist, Schedules, Backup / Restore, Settings. - Overview: status cards (bot daemon / firewall / NTP clock / mode flags), 5 count cards (active/denied/static/blacklist/schedules), live tail of
/tmp/gatekeeper.log, optional auto-refresh every 5 s. - Devices: separate tables for active (approved_macs), denied (denied_macs), and static (static_macs). Per-row Approve / Deny / +30m / +1h / +4h / Revoke buttons. ⏰ tag indicates schedule-driven approvals; hostname column resolves via the same chain the STATUS bot command uses.
- Blacklist: large slide-toggle for
blacklist_mode, MAC list editor with online indicator (matched against/tmp/dhcp.leases), bulk Clear All. - Schedules: table + modal CRUD with day-preset selector (Daily / Weekdays / Weekends / Custom), browser-native time pickers, hyphen-to-underscore name correction hints (UCI section names disallow hyphens), pause/resume toggle. Active windows show end-time inline.
- Backup / Restore: browser-native download (with secrets / NO secrets) and drag-and-drop file upload. Two-step preview-then-apply restore flow showing the same merge plan the Telegram RESTORE flow produces — additive merge, never overwrites token/chat_id, atomic apply with
uci reverton any failure. - Settings: form for token (masked + show toggle), chat_id, blacklist_mode, schedule_notify, disabled flag. "Test bot connection" button calls Telegram
getMeand reports username/first-name. Sync MAC sets and Clear logs maintenance buttons. - Backend is a single rpcd exec plugin at
/usr/libexec/rpcd/gatekeeperexposing 31 ubus methods (POSIX/bin/sh, BusyBox-compatible). Frontend is 6 ES moduleview/<page>.jsfiles in the modern client-side LuCI pattern. - Auth is the router's existing admin password (LuCI's standard ACL); no separate credential store.
- GitHub Actions workflow (
.github/workflows/makefile.yml) extended to build the LuCI ipk in the same job as the runtime package. Both.ipkfiles are uploaded as workflow artifacts on every push and PR, and attached to the GitHub Release on everyv*tag push.
- Six pages under
-
Shared
restore_helpers.shlibrary (/usr/lib/gatekeeper/restore_helpers.sh) - Refactor that pullsmac_hostname,is_valid_backup,restore_parse_to_records,restore_build_plan, and theRESTORE_*state-file constants out oftg_bot.sh(~250 lines) into a shared sourceable library. Bothtg_bot.shand the new LuCI rpcd backend source it — single source of truth for the restore parser/merge engine. The schedule-helper trio remains intentionally three-copy-duplicated per its existing contract. -
deploy.sh --luci/--luci-onlyflags - New optional flags that deploy the LuCI app files (rpcd backend, ACL, menu manifest, all six frontend views) and restartrpcdso it discovers the new plugin.--luciis additive (runtime + LuCI in one shot);--luci-onlyskips the runtime files entirely for fast iteration on LuCI-only changes. Documented inREADME.md,CLAUDE.md, andDEPLOY.md. -
deploy.sh --ask-passwordflag - For routers without SSH key auth, prompts once at the start for the root password and reuses it for everyssh/scpcall in the run, eliminating the per-call password prompts. Routes ssh/scp throughsshpass -e, holding the password in theSSHPASSenv var only — it never appears in argv (ps/shell history stay clean). Requiressshpass(brew install hudochenkov/sshpass/sshpasson macOS,apt-get install sshpasson Debian/Ubuntu). Combines with any other flag (e.g.--luci-only --ask-password). For unattended use, SSH keys (ssh-copy-id) remain preferred. -
Scheduled Auto-Approval - Major feature for time-window-based MAC auto-approval
- Define recurring time windows per MAC (e.g., kid's tablet weekdays 16:00-20:00)
- Multiple schedules per MAC supported (e.g., separate weekday and weekend windows)
- Day-of-week selection:
daily,weekdays,weekends, or comma-separatedmon,tue,wed,thu,fri,sat,sun - Cross-midnight windows (e.g.,
22:00-06:00) - Hybrid behavior:
scheduler_tickproactively pushes/popsapproved_macsat window boundaries (one tick pertg_bot.shpolling iteration);gatekeeper.shstep 3.6 reactively catches mid-window DHCP events - Self-healing across
fw4 reloadevents; idempotent reconciliation - Stable UCI section names; persists across reboots
- Manual
REVOKEduring a window adds the MAC todenied_macsfor 30 min and the scheduler skips re-push for that period - Optional
schedule_notifyflag: when set, the bot sends a "✅ Scheduled Auto-Approve" info message on each schedule-triggered approval - New Telegram commands:
SCHEDADD,SCHEDLIST,SCHEDSHOW,SCHEDREMOVE,SCHEDOFF/SCHEDON,SCHEDNOTIFY - New UCI section type
config schedule '<name>'and newoption schedule_notifyinmain - New ephemeral state:
/tmp/sched_active,/tmp/sched_lock
-
Configuration Backup - Telegram-driven config snapshot
- New
BACKUPcommand produces a plain-text UCI dump and uploads it as a Telegram document viasendDocument - Includes
/etc/config/gatekeeper(main, blacklist, schedules) plus the static DHCP host entries from/etc/config/dhcp BACKUP NOSECRETSvariant blankstokenandchat_idbefore upload for files you might share more broadly- Filename:
gatekeeper-backup-<hostname>-<YYYYMMDD-HHMM>-<pid>.txt(PID suffix prevents concurrent-backup collisions) - Temp file deleted from
/tmpimmediately after upload regardless of success or failure (Telegram chat history is the archive)
- New
-
Configuration Restore - Telegram-driven additive merge from a backup file
- Reply
RESTOREto a backup file in the chat; bot validates, computes a merge plan, replies with a preview - Reply
YESto the preview message within 10 minutes to apply the plan in a single UCI transaction - Additive merge semantics: skip duplicate blacklist MACs (case-insensitive by value); skip existing schedule sections (by name); restore non-secret main options if values differ; never touch
tokenorchat_id - Two-phase apply with
uci reverton any failure - Post-apply hooks:
scheduler_tickfor newly-restored active schedules;blacklist_macsnftables re-sync from new UCI state - 5-check validation gate (header, schema v1, both section markers, package line, file size cap) before any UCI mutation
- Friendly preview rendering uses the same hostname-resolution chain as
STATUSto show device names alongside MACs - All
/tmp/restore_*state cleaned up on success, failure, expiry, or supersession by another RESTORE
- Reply
-
Blacklist Mode - Inverts approval logic so only blacklisted MACs require approval, others are auto-approved for 24 hours
- Persistent configuration via UCI (survives reboots)
- New
blacklist_macsnftables set for efficient filtering - Auto-approval messages include MAC, IP, and hostname information
- New Telegram commands:
BLON/BLOFF/BLSTATUS/BLADD [MAC]/BLREMOVE [MAC]/BLCLEAR(plusBLACKLIST_*prefix aliases)
-
Dev-only test harness - Pure-shell unit tests for portable helper logic
tests/test_schedule_helpers.sh(28 assertions) coversexpand_days,hm_to_min,window_active_now(same-day, cross-midnight, comma-list days, single-anchored-day cross-midnight); auto-detectsgdate(GNU coreutils) on macOS, falls back todateon Linuxtests/test_backup_helpers.sh(5 assertions) covers the awk host-extractor and the sed secret-strippertests/test_restore_helpers.sh(9 assertions) covers the awk parser and theis_valid_backuppredicate- All run on macOS / Linux dev machines without a router or Telegram bot
-
Automated deployment (
deploy.sh) - SCP-based deploy script for development iteration- Supports
--dry-run,--no-restart,--restart-only,--config-only,--scripts-only,--no-configmodes - Automatic permission-setting and service restart
- Built-in connectivity testing and verification
- Flag-conflict detection rejects nonsensical combinations (e.g.
--config-only --luci-only) - Color-coded output
- Supports
-
Improved synchronization (
gatekeeper_sync.sh) - Manual sync utility supports both static and blacklist MACsgatekeeper_initsyncs both at boot- Parameter support for selective sync (
static,blacklist, orall) - Bot's
SYNCcommand and LuCI's "Sync MAC sets" button both callgatekeeper_sync.sh allfor unified scope
Fixed
- BusyBox
trPOSIX-character-class incompatibility - Older BusyBoxtrdoes not interpret[:upper:]/[:lower:]as POSIX character classes; it treats them as literal characters. Surfaced asSCHEDADD … LivingRoomTVbeing rejected with "Invalid name" because the input was never lowercased. Replaced all 21 occurrences intg_bot.shandgatekeeper.shwith thetr 'A-Z' 'a-z'ASCII-range form per the CLAUDE.md convention. - SCHEDADD invalid-name error is now actionable - Names with hyphens like
living-room-tvpreviously hit a generic regex error with no clue why (UCI section names disallow hyphens). The error now detects the hyphen case specifically and suggests the underscore alternative (living_room_tv) so the user can copy-paste a working command. - REVOKE/DREVOKE not appearing in logread - Added
logger -t tg_botcalls to REVOKE and DREVOKE so they appear inlogread -f | grep tg_botalongside other bot operations - BLADD allows duplicate MACs - BLADD now checks if MAC already exists in
blacklist_macsbefore adding to UCI and nftables; responds with an info message if already present deploy.shskippedrestore_helpers.sh- The shared library at/usr/lib/gatekeeper/restore_helpers.shis sourced bytg_bot.shat startup; without it the bot crashed before the polling loop even began.deploy.shnow creates/usr/lib/gatekeeper/and copies the file as part of the standard runtime deploy. Manual deployment instructions inDEPLOY.mdupdated similarly.- Bot
SYNCscope mismatch with LuCI - BotSYNConly re-syncedstatic_macs, while LuCI's "Sync MAC sets" button re-syncs bothstatic_macsandblacklist_macsviagatekeeper_sync.sh all. The bot now calls the same helper script (with a fallback to inline static-only sync if the script is missing). Both surfaces produce identical results and the same Telegram report (🔄 Synced N static and M blacklist MACs.). HELP text and header comment updated. - nftables timeout-update no-ops in 5 places -
nft add element ... timeout 30msilently no-ops if the MAC is already in the target set; the existing element keeps its old timeout. Addednft delete element ...immediately before eachaddin: bot inline approve callback (tg_bot.sh:357), bot REVOKE handler (tg_bot.sh:735), bot DREVOKE handler (tg_bot.sh:664), rpcdrevokemethod, rpcddenied_revoke_approvemethod. The CLAUDE.md "Critical Implementation Details" section already documented the rule; this propagates the fix to every callsite that violated it. - LuCI ipk had no version floor on runtime gatekeeper -
Depends: luci-base, gatekeeper, rpcdwould happily install with an older runtime that lacksrestore_helpers.sh, breaking the rpcd backend at every call. Now pinned togatekeeper (>= ${PKG_VERSION}-${PKG_RELEASE}); opkg refuses the install unless the runtime is at least the same version as the LuCI ipk being shipped. - LuCI ipk lacked
postinst- Afteropkg install luci-app-gatekeeper_*.ipk,rpcddidn't auto-discover the new/usr/libexec/rpcd/gatekeeperplugin until manual/etc/init.d/rpcd restart. Added apostinstthat runs the restart, withIPKG_INSTROOTguard so chroot/image-build installs are unaffected. - Method count claims - All five doc references claimed
~28 methods; the rpcd plugin actually exposes 31 (verified bygatekeeper list | jq 'length'). Removed the deadsched_showmethod (declared, ACL'd, never called by frontend; redundant withsched_listfiltered by name) from the plugin, ACL, and CLAUDE.md "Status reads" list. Updated all five method-count references. - Menu path "Network → Services → Gatekeeper" was wrong - The LuCI menu JSON registers under
admin/services/gatekeeper, which renders as a top-levelServices → Gatekeeperin standard OpenWrt LuCI. All five docs (README.md,CHANGELOG.md,CLAUDE.md,DEPLOY.md,QUICK_REFERENCE.md) updated. - CHANGELOG nftables-set-count typo - The blacklist-mode entry said "Updated from 4 to 5 nftables sets"; the actual transition was 3 → 4 (the initial v1.0.0 release had 3 sets —
static_macs,approved_macs,denied_macs— andblacklist_macswas added later). v1.0.0 entry also corrected from "4 sets" to "3 sets". LOGcommand comment said "10 entries" - Code usestail -n 20. Comments intg_bot.sh(header docstring + handler comment) corrected to match.- Dead
countsRowcode inoverview.js- A duplicate count-card row variable was constructed but never appended to the DOM; the actual rendered counts row uses ID-based DOM updates from the polling loop. Removed. - Schedules page delay note referenced internal script name - The user-facing note on the Schedules page mentioned
tg_bot.shandscheduler_tick, which is meaningless to a LuCI admin who never opened the bot's source. Reworded to "schedule changes can take up to 30 seconds to take effect".
Changed
gatekeeper.shvalidation order - Added new step 3.6 between blacklist mode (3.5) and notification (4): if the MAC has an active schedule, auto-approve until window end and optionally notify (controlled bygatekeeper.main.schedule_notify). New ordering: static lease →denied_macs→approved_macs→ disabled-flag → blacklist mode → active schedule → notification.- Telegram Bot HELP - New
📅 Schedulessubsection listing all SCHED commands;BACKUPandRESTOREadded under Maintenance; STATUS now tags schedule-driven approvals with⏰ <schedule-name>. /etc/config/gatekeeperschema - Newoption schedule_notifyinmain(default0). Newconfig schedule '<name>'section type withmac/days/start/stop/label/enabledoptions.- Blacklist command names - Renamed
BL_ON/BL_OFF/BL_STATUS/BL_ADD/BL_REMOVE/BL_CLEARtoBLON/BLOFF/BLSTATUS/BLADD/BLREMOVE/BLCLEAR(shorter, no underscore);BLACKLIST_prefix aliases still work - Firewall architecture - Updated from 3 to 4 nftables sets (added
blacklist_macs); firewall comments and documentation updated - UCI configuration - Added
blacklist_modeoption togatekeeper.mainand ablacklistsection type for the MAC list; default config template updated - Telegram menu layout - Removed "Log" and "Clear" buttons from the keyboard menu; cleaner 2-row layout (Status/DStatus/Help and Sync/Enable/Disable). Commands still accessible via text input.
- Help command - Reorganized into sections (Device Management, Blacklist Mode, Schedules, Maintenance, System Control); includes all new commands
Documentation
- New design specs in
docs/superpowers/specs/:2026-04-28-scheduled-approval-design.md,2026-04-28-config-backup-design.md,2026-04-28-config-restore-design.md. - New implementation plans in
docs/superpowers/plans/: matching files for each of the three features. CLAUDE.mdupdated with: new state-files rows (/tmp/sched_active,/tmp/sched_lock), validation-order step 6 (active-schedule), and three new command subsections (Schedules, Backup, Restore).README.mdandQUICK_REFERENCE.mdupdated with all new commands.- Fixed README and QUICK_REFERENCE: removed non-existent
bypass_switchnftables set; corrected emergency disable/enable commands to useDISABLE/ENABLEbot commands ornft flush chain/fw4 reload
Full Changelog: v1.1...v1.2