Confirm changes automatically from the client that made them

A change made over the network answers with a one-time confirm_token
and closes its connection (HTTP/2: a GOAWAY). The web UI and hc send the
token straight back to /confirm, which accepts it only for the newest
change, from the same user and API token, the same address, and over a
new connection: proof that the client can still reach the API through
the new ruleset (an established connection would get through anyway). A
change that cuts its client off can't be confirmed so, and rolls back by
itself. The setting manual_confirm (hc settings set --auto-confirm off)
and hc --no-confirm leave confirming to a person. Changes through the
local socket still need no confirmation. e2e: 136 checks, among them a
lockout change rolled back without anyone confirming.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-19 13:23:28 +01:00
1 parent fc390eb57c
commit e4efdf9c78
19 files changed
+384 -50

No files matched your search

+21 -1
View File
@@ -125,10 +125,15 @@ func settingsCmd() *cobra.Command {
fmt.Fprintf(w, "Antispoof IPv6:\t%s\n", list(s.AntispoofV6))
fmt.Fprintf(w, "Flowtable:\t%s\n", list(s.FlowtableDevices))
fmt.Fprintf(w, "Probation:\t%s\n", fmtSeconds(s.Probation()))
if s.ManualConfirm {
fmt.Fprintln(w, "Confirmation:\tby a person ('hc confirm', the web UI's banner)")
} else {
fmt.Fprintln(w, "Confirmation:\tautomatic, by the client that made the change, once it reaches Housecarl again over a new connection")
}
})
},
})
var input, forward, output, logDrops, probation string
var input, forward, output, logDrops, probation, autoConf string
var builtins, own, flowtable, spoof4, spoof6 []string
set := &cobra.Command{
Use: "set [flags]", Short: "Change settings (only the flags given)", Args: cobra.NoArgs,
@@ -172,6 +177,13 @@ dhcpv6_client, antispoof, mss_clamp.
p[map[string]string{"own-prefix": "own_prefixes", "flowtable": "flowtable_devices", "antispoof-v4": "antispoof_v4", "antispoof-v6": "antispoof_v6"}[flag]] = v
}
}
if fl.Changed("auto-confirm") {
on, err := onOff("auto-confirm", autoConf)
if err != nil {
return err
}
p["manual_confirm"] = !on
}
if fl.Changed("probation") {
n, err := parseSeconds(probation)
if err != nil {
@@ -193,6 +205,7 @@ dhcpv6_client, antispoof, mss_clamp.
f.StringArrayVar(&spoof4, "antispoof-v4", nil, "IPv4 sources dropped from the internet (replaces)")
f.StringArrayVar(&spoof6, "antispoof-v6", nil, "IPv6 sources dropped from the internet (replaces)")
f.StringVar(&probation, "probation", "", "how long a network change has to be confirmed (2m)")
f.StringVar(&autoConf, "auto-confirm", "", "on: the client that made a change confirms it once it reaches Housecarl again over a new connection; off: a person confirms")
cmd.AddCommand(set)
return cmd
}
@@ -510,6 +523,13 @@ come back. Don't forget the boot: disable nftables.service, or empty
return err
}
fmt.Println(out["message"])
if tok, _ := out["confirm_token"].(string); tok != "" && !flagNoConfirm {
if err := autoConfirm(tok); err != nil {
fmt.Printf("couldn't confirm it (%v): confirm with 'hc confirm', or the tables come back\n", err)
} else {
fmt.Println("confirmed: this client reached Housecarl again over a new connection")
}
}
return nil
},
}