mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
A maintenance fallback start holds no resolver config, and Allowed Destinations (a master-only Firewall Mode feature) are not persisted with the configuration on disk. Under Firewall Mode the exception set is therefore empty until the API answers, so raw-IP destinations the organization allowed stay blocked for the window, and a persisted pf table from the previous run is replaced by the empty set. Keep that fail-closed on purpose: the list is the organization's network topology and is kept out of persisted state. Journal a Warn when a fallback start applies the empty set under Firewall Mode so the blocked destinations are explained, and pin that the first answer after recovery re-applies the list. Also capitalize the two managed-config marker log messages to match the rest of master.
97 lines
3.4 KiB
Go
97 lines
3.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"net/netip"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
"github.com/Control-D-Inc/ctrld/internal/controld"
|
|
)
|
|
|
|
// TestMaintenanceFallbackFirewallAllowedDestinations covers Firewall Mode on a
|
|
// maintenance fallback start. The organization's Allowed Destinations exist
|
|
// only in the API answer, which such a run does not have, so the exception set
|
|
// is empty until the API returns. That is a deliberate fail-closed state: it
|
|
// must be named in the retained journal, and the first answer after recovery
|
|
// must re-apply the list even though nothing else is compared as changed.
|
|
func TestMaintenanceFallbackFirewallAllowedDestinations(t *testing.T) {
|
|
allowed := netip.MustParseAddr("203.0.113.10")
|
|
|
|
t.Run("fallback start journals that allowed destinations are not enforced", func(t *testing.T) {
|
|
logs := captureDebugMainLog(t)
|
|
p := progWithAllowList()
|
|
p.logger.Store(mainLog.Load())
|
|
p.startedInAPIMaintenance.Store(true)
|
|
|
|
p.syncAllowedDestinations()
|
|
|
|
events := jsonLogEvents(t, logs, "Firewall: organization allowed destinations are not enforced until the Control D API answers; started on the configuration on disk during maintenance")
|
|
if len(events) != 1 {
|
|
t.Fatalf("fail-closed warnings: got %d, want 1", len(events))
|
|
}
|
|
if events[0]["level"] != "warn" {
|
|
t.Errorf("level = %v, want warn", events[0]["level"])
|
|
}
|
|
if events[0]["reason"] != "api_maintenance" {
|
|
t.Errorf("reason = %v, want api_maintenance", events[0]["reason"])
|
|
}
|
|
if events[0]["journal"] != true {
|
|
t.Error("the fail-closed warning is not retained in the journal")
|
|
}
|
|
if p.allowList.Contains(allowed) {
|
|
t.Fatalf("%s allowed with no resolver config", allowed)
|
|
}
|
|
})
|
|
|
|
t.Run("a normal start with no resolver config stays quiet", func(t *testing.T) {
|
|
logs := captureDebugMainLog(t)
|
|
p := progWithAllowList()
|
|
p.logger.Store(mainLog.Load())
|
|
|
|
p.syncAllowedDestinations()
|
|
|
|
if got := len(jsonLogEvents(t, logs, "Firewall: organization allowed destinations are not enforced until the Control D API answers; started on the configuration on disk during maintenance")); got != 0 {
|
|
t.Fatalf("fail-closed warnings outside maintenance: got %d, want 0", got)
|
|
}
|
|
})
|
|
|
|
t.Run("firewall mode off stays quiet", func(t *testing.T) {
|
|
logs := captureDebugMainLog(t)
|
|
p := &prog{}
|
|
p.logger.Store(mainLog.Load())
|
|
p.startedInAPIMaintenance.Store(true)
|
|
|
|
p.syncAllowedDestinations()
|
|
|
|
if got := len(jsonLogEvents(t, logs, "Firewall: organization allowed destinations are not enforced until the Control D API answers; started on the configuration on disk during maintenance")); got != 0 {
|
|
t.Fatalf("fail-closed warnings with Firewall Mode off: got %d, want 0", got)
|
|
}
|
|
})
|
|
|
|
t.Run("pending recovery re-applies the list", func(t *testing.T) {
|
|
p := progWithAllowList()
|
|
p.apiReloadCh = make(chan *ctrld.Config, 1)
|
|
p.startedInAPIMaintenance.Store(true)
|
|
// The fallback start applied an empty set.
|
|
p.syncAllowedDestinations()
|
|
if p.allowList.Contains(allowed) {
|
|
t.Fatalf("%s allowed before recovery", allowed)
|
|
}
|
|
|
|
p.applyFetchedResolverConfig(context.Background(), discardLogger(), &controld.ResolverConfig{
|
|
DestinationIPs: []string{"203.0.113.10"},
|
|
}, false, time.Now().Unix())
|
|
|
|
if !p.allowList.Contains(allowed) {
|
|
t.Fatalf("recovery did not re-apply the organization list: %s blocked", allowed)
|
|
}
|
|
select {
|
|
case <-p.apiReloadCh:
|
|
default:
|
|
t.Fatal("pending recovery did not deliver the answer to the apply path")
|
|
}
|
|
})
|
|
}
|