mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-09-04 13:36:35 +02:00
cmd/cli: scope ctrld's WFP objects to a dynamic session on Windows
ctrld opened the WFP engine with a plain FWPM_SESSION0, so every filter and sublayer it installed was persistent for the engine's boot lifetime: the kernel kept enforcing them after the installing process was gone. Any exit that did not run the shutdown path - kill, crash, or a service stop during upgrade - left them behind. In hard intercept mode that orphaned the DNS block filters. With Firewall Mode enabled it orphaned machine-wide block-all filters that carry no process or SID condition, so the entire host lost outbound traffic: browsers, other users, and a replacement ctrld's own API bootstrap alike, with no way back short of a reboot. Set FWPM_SESSION_FLAG_DYNAMIC on both engine sessions (hard intercept and loopback protect). Windows then deletes everything the session owns when the handle closes, including on abnormal termination, so ctrld's enforcement can no longer outlive the process that installed it. This removes the cause. The next commit adds startup self-heal for hosts already carrying orphaned filters from a build that predates this change.
This commit is contained in:
@@ -66,15 +66,29 @@ func ConfigureWindowsServiceFailureActions(serviceName string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Then proceed with existing actions, e.g. setting failure actions
|
||||
// Recovery policy for a service that carries enforcement.
|
||||
//
|
||||
// ctrld's WFP session is dynamic, so Windows removes its filters when the process
|
||||
// dies - a host with no ctrld is unfiltered rather than locked out. That makes the
|
||||
// restart budget part of the enforcement story: three restarts five seconds apart
|
||||
// with a two-minute reset window could be spent inside fifteen seconds, after which
|
||||
// the service stays stopped and the host stays unfiltered until an operator acts.
|
||||
//
|
||||
// The delays back off instead, and the reset window is long enough that a burst
|
||||
// cannot exhaust the budget faster than the backoff allows. A genuine crash loop
|
||||
// still ends in a stopped service - that is the point of a bounded policy - but it
|
||||
// takes minutes rather than seconds, and the third restart survives a transient
|
||||
// failure that repeats.
|
||||
actions := []mgr.RecoveryAction{
|
||||
{Type: mgr.ServiceRestart, Delay: time.Second * 5}, // 5 seconds
|
||||
{Type: mgr.ServiceRestart, Delay: time.Second * 5}, // 5 seconds
|
||||
{Type: mgr.ServiceRestart, Delay: time.Second * 5}, // 5 seconds
|
||||
{Type: mgr.ServiceRestart, Delay: time.Second * 5},
|
||||
{Type: mgr.ServiceRestart, Delay: time.Second * 30},
|
||||
{Type: mgr.ServiceRestart, Delay: time.Minute * 2},
|
||||
}
|
||||
|
||||
// Set the recovery actions (3 restarts, reset period = 120).
|
||||
err = s.SetRecoveryActions(actions, 120)
|
||||
// Reset the failure count only after the service has stayed up longer than the whole
|
||||
// backoff schedule, so repeated failures keep escalating instead of restarting the
|
||||
// count from the first five-second delay.
|
||||
err = s.SetRecoveryActions(actions, uint32((10 * time.Minute).Seconds()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user