mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-09-04 13:36:35 +02:00
all: permit ctrld's own endpoints in Firewall Mode
Firewall Mode permits only what ctrld resolved through its own listener. The API transport resolves api.controld.com through the OS nameservers and falls back to hardcoded addresses, so nothing ever teaches the allowlist about it and ctrld's own block-all filters deny its control-plane socket. The upgrade download server has the same shape: performUpgrade spawns a detached child process, and the WFP filters carry no process condition, so the service blocks its own upgrade. Permit both permanently, at startup and on reload. For the API that means the resolved addresses and the transport's direct fallbacks - the fallbacks are what it dials when DNS is unusable, which is the state a blocked ctrld is in. For the download server only the fallback IP is needed, since its hostname lookup does go through the listener and is learned. APIDomain/APIEndpointIPs are exported so the permitted set and the dialed set cannot drift apart. Call initPlatformFirewall on reload even when enforcement is already up. AddPermanent fires no change callback, so an address permitted by a reload reached memory only while the platform never heard about it. Each platform's re-entry is a refresh: Windows reinstalls the permanent filters it is missing, macOS returns early. Also keep every dial attempt in the transport's error. It returned only the last stage, an unroutable IPv6 address reporting "no route to host", hiding the IPv4 WSAEACCES that named the real cause. The direct IPs are still always dialed, so the API stays reachable without DNS; only a duplicate dial of an address the resolver already returned is dropped.
This commit is contained in:
@@ -619,3 +619,50 @@ func TestAllowedDestinationLogsKeepAddressesOutOfWarnings(t *testing.T) {
|
||||
t.Errorf("the wide entry %q appears in no Debug line", wide)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFirewallPermanentAllowListPermitsControlDEndpoints is the regression guard
|
||||
// for the Windows lockout described on controld.APIEndpointIPs.
|
||||
//
|
||||
// Firewall Mode learns destinations from queries ctrld's own listener answered.
|
||||
// The addresses asserted here are the ones each endpoint falls back to when DNS
|
||||
// does not work at all - which is the state a ctrld blocked by its own filters is
|
||||
// in - so they are exactly the ones no lookup can ever teach it.
|
||||
func TestFirewallPermanentAllowListPermitsControlDEndpoints(t *testing.T) {
|
||||
for _, dev := range []bool{false, true} {
|
||||
t.Run(map[bool]string{false: "prod", true: "dev"}[dev], func(t *testing.T) {
|
||||
origDev := cdDev
|
||||
cdDev = dev
|
||||
t.Cleanup(func() { cdDev = origDev })
|
||||
|
||||
al := firewall.New()
|
||||
p := &prog{cfg: &ctrld.Config{}}
|
||||
p.logger.Store(mainLog.Load())
|
||||
p.initFirewallAllowList(context.Background(), al)
|
||||
|
||||
apiIPs := controld.APIEndpointIPs(dev)
|
||||
if len(apiIPs) == 0 {
|
||||
t.Fatal("no ControlD API addresses to permit")
|
||||
}
|
||||
|
||||
endpoints := map[string][]string{
|
||||
// The API transport's direct addresses.
|
||||
"API": apiIPs,
|
||||
// The upgrade download server's fallback. performUpgrade runs the
|
||||
// download in a detached child process, and WFP's block-all filters
|
||||
// carry no process condition, so the service blocks its own upgrade.
|
||||
"download server": {downloadServerIp},
|
||||
}
|
||||
for what, ips := range endpoints {
|
||||
for _, ipStr := range ips {
|
||||
ip, err := netip.ParseAddr(ipStr)
|
||||
if err != nil {
|
||||
t.Fatalf("the %s address %q does not parse: %v", what, ipStr, err)
|
||||
}
|
||||
if !al.Contains(ip) {
|
||||
t.Errorf("the ControlD %s address %s is not permitted; Firewall Mode would block ctrld's own socket to it", what, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user