Gap
Junos schedulers define time-of-day / weekday windows; policies reference them via then scheduler-name foo. The eBPF dataplane has a daemon goroutine that evaluates scheduler windows and toggles each policy rule's Active byte in the BPF policy_rules map. The userspace dataplane has no scheduler plumbing whatsoever — the snapshot DTO doesn't carry SchedulerName, the Rust PolicyRule has no active field, and the daemon's UpdatePolicyScheduleState only writes to BPF maps. Scheduled policies will not activate/deactivate when running on the userspace dataplane.
eBPF implementation (source of truth)
pkg/dataplane/dataplane.go:134 — UpdatePolicyScheduleState(cfg, activeState) interface method
pkg/dataplane/maps.go:1495-1537 — implementation: iterates cfg.Security.Policies, looks up each rule by policySetID*MaxRulesPerPolicy+i, toggles rule.Active, writes back to BPF map
pkg/daemon/daemon_run.go:589 — d.dp.UpdatePolicyScheduleState(activeCfg, activeState) ticks per scheduler window change
pkg/config/compiler.go:471 — config validation: policy %q: scheduler %q not defined
bpf/xdp/xdp_policy.c reads the Active byte; inactive rules are skipped
Userspace-dp gap
pkg/dataplane/userspace/protocol.go:351-360 PolicyRuleSnapshot has no SchedulerName and no Active field
userspace-dp/src/policy.rs:47-83 PolicyRule has no active: bool or scheduler reference; evaluate_policy does not gate on time-of-day
- The userspace
Manager embeds the eBPF DataPlane so UpdatePolicyScheduleState calls land in pkg/dataplane/maps.go:1497 and silently no-op (policy_rules BPF map is loaded but not actually consulted by the userspace XDP shim)
Recommended fix
- Add
SchedulerName string to PolicyRuleSnapshot (Go) and matching Rust field
- Add
Active: bool (or Inactive: bool to keep zero-valued default behavior) to the snapshot; emit current active state on every commit + on scheduler tick
- In
userspace-dp/src/policy.rs::evaluate_policy, skip rules with active == false
- Daemon-side: re-publish a snapshot delta when scheduler windows change, OR add a fast-path
update_policy_active(rule_idx, active) control socket call
- Add capability check: bare-minimum verification that scheduler-referenced policies are admitted under userspace mode
Blocker for #1373
This must land before Phase 4 (BPF source removal) of #1373. The userspace path silently ignores scheduler state today; once eBPF is removed there's no fallback to mask the loss of scheduled access control.
Refined contract (added 2026-05-17 after triple-review of #1384)
See docs/pr/1373-retire-ebpf-dataplane/plan-1378-policy-schedulers.md for the full implementation contract refined through 4 rounds of Claude+Codex+Gemini Pro 3 review. New since the original issue body:
- Risks called out: time-source drift (monotonic clock for window evaluation, NTP rollback must not flap policies), apply-cycle race (scheduler window transition during config apply must produce either fully-old or fully-new policy decisions, not split), userspace/Go propagation latency (window changes must propagate within one snapshot cycle or operators see stale matches).
Gap
Junos
schedulersdefine time-of-day / weekday windows; policies reference them viathen scheduler-name foo. The eBPF dataplane has a daemon goroutine that evaluates scheduler windows and toggles each policy rule'sActivebyte in the BPFpolicy_rulesmap. The userspace dataplane has no scheduler plumbing whatsoever — the snapshot DTO doesn't carrySchedulerName, the RustPolicyRulehas noactivefield, and the daemon'sUpdatePolicyScheduleStateonly writes to BPF maps. Scheduled policies will not activate/deactivate when running on the userspace dataplane.eBPF implementation (source of truth)
pkg/dataplane/dataplane.go:134—UpdatePolicyScheduleState(cfg, activeState)interface methodpkg/dataplane/maps.go:1495-1537— implementation: iteratescfg.Security.Policies, looks up each rule bypolicySetID*MaxRulesPerPolicy+i, togglesrule.Active, writes back to BPF mappkg/daemon/daemon_run.go:589—d.dp.UpdatePolicyScheduleState(activeCfg, activeState)ticks per scheduler window changepkg/config/compiler.go:471— config validation:policy %q: scheduler %q not definedbpf/xdp/xdp_policy.creads theActivebyte; inactive rules are skippedUserspace-dp gap
pkg/dataplane/userspace/protocol.go:351-360PolicyRuleSnapshothas noSchedulerNameand noActivefielduserspace-dp/src/policy.rs:47-83PolicyRulehas noactive: boolor scheduler reference;evaluate_policydoes not gate on time-of-dayManagerembeds the eBPFDataPlanesoUpdatePolicyScheduleStatecalls land inpkg/dataplane/maps.go:1497and silently no-op (policy_rules BPF map is loaded but not actually consulted by the userspace XDP shim)Recommended fix
SchedulerName stringtoPolicyRuleSnapshot(Go) and matching Rust fieldActive: bool(orInactive: boolto keep zero-valued default behavior) to the snapshot; emit current active state on every commit + on scheduler tickuserspace-dp/src/policy.rs::evaluate_policy, skip rules withactive == falseupdate_policy_active(rule_idx, active)control socket callBlocker for #1373
This must land before Phase 4 (BPF source removal) of #1373. The userspace path silently ignores scheduler state today; once eBPF is removed there's no fallback to mask the loss of scheduled access control.
Refined contract (added 2026-05-17 after triple-review of #1384)
See
docs/pr/1373-retire-ebpf-dataplane/plan-1378-policy-schedulers.mdfor the full implementation contract refined through 4 rounds of Claude+Codex+Gemini Pro 3 review. New since the original issue body: