internal/stats samples every minute what the kernel already counts: the
interfaces' counters (per zone too), Housecarl's table (hits per rule,
built-in and policy; drops by chain; quotas; per-device sets), the
connection tracking table, and housecarld's and the host's memory, CPU
and load. Logged drops feed top lists of sources and what they tried.
Aggregates go to a database of their own (stats.db; per minute 7 days,
per hour 90 days, per day 2 years), with the store and queries from
Jarl's.
Traffic per inside device is a setting (stats_per_device): counting sets
stats_up/down_v4/v6 and rules at the top of the forward chain, IPv6 per
/64, carried across reloads. Set names starting with meter_, conn_,
quota_ or stats_ are reserved (set_name_reserved).
GET /stats/{status,summary,timeline,top,hits,heatmap}, hc stats, a
Statistics page with Jarl's charts, two charts on the dashboard, and a
conntrack_full warning. e2e: 188 checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
549 lines
18 KiB
Go
549 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"housecarl/internal/api"
|
|
"housecarl/internal/model"
|
|
)
|
|
|
|
func jsonUnmarshal(b []byte, v any) error { return json.Unmarshal(b, v) }
|
|
|
|
// ---- status
|
|
|
|
func statusCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "status", Short: "Show the firewall's state: ruleset, probation, problems, interfaces", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
st, err := get[api.Status]("/status", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return output(st, func(w io.Writer) {
|
|
fmt.Fprintf(w, "Housecarl:\t%s, up since %s\n", st.Build.Label, st.Started.Local().Format("2006-01-02 15:04"))
|
|
if st.Applied {
|
|
fmt.Fprintln(w, "Ruleset:\tin force (table inet housecarl)")
|
|
} else if st.Unconfigured {
|
|
fmt.Fprintln(w, "Ruleset:\tnone yet: the configuration is empty (add rules, or 'hc import')")
|
|
} else {
|
|
fmt.Fprintf(w, "Ruleset:\tNOT in force: %s\n", st.ApplyError)
|
|
}
|
|
if len(st.Probation.Pending) > 0 {
|
|
n := len(st.Probation.Pending)
|
|
what := map[bool]string{true: "1 change rolls", false: fmt.Sprintf("%d changes roll", n)}[n == 1]
|
|
fmt.Fprintf(w, "Probation:\t%s back in %ds unless confirmed ('hc confirm')\n", what, st.Probation.SecondsLeft)
|
|
}
|
|
if len(st.OtherTables) > 0 {
|
|
fmt.Fprintf(w, "Other tables:\t%s (not Housecarl's; 'hc takeover' deletes them once replaced)\n", strings.Join(st.OtherTables, ", "))
|
|
}
|
|
var counts []string
|
|
for _, k := range model.Kinds {
|
|
counts = append(counts, fmt.Sprintf("%d %ss", st.Counts[k], k))
|
|
}
|
|
fmt.Fprintf(w, "Configuration:\t%s\n", strings.Join(counts, ", "))
|
|
fmt.Fprintln(w, "Interfaces:")
|
|
for _, i := range st.Interfaces {
|
|
state := "down"
|
|
if i.Up {
|
|
state = "up"
|
|
}
|
|
fmt.Fprintf(w, " %s\t%s\tzone %s\t%s\n", i.Name, state, orDash(i.Zone), strings.Join(i.Addresses, ", "))
|
|
}
|
|
if len(st.Problems) > 0 {
|
|
fmt.Fprintln(w, "Problems:")
|
|
for _, p := range st.Problems {
|
|
fmt.Fprintf(w, " %s:\t%s: %s [%s]\n", p.Severity, p.Object, p.Message, p.Code)
|
|
}
|
|
}
|
|
})
|
|
},
|
|
}
|
|
}
|
|
|
|
func interfaceCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "interfaces", Aliases: []string{"interface"}, Short: "List the machine's interfaces and their zones", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
ifs, err := get[[]api.HostInterface]("/interfaces", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return output(ifs, func(w io.Writer) {
|
|
fmt.Fprintln(w, "INTERFACE\tSTATE\tZONE\tADDRESSES")
|
|
for _, i := range ifs {
|
|
state := "down"
|
|
if i.Up {
|
|
state = "up"
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", i.Name, state, orDash(i.Zone), orDash(strings.Join(i.Addresses, ", ")))
|
|
}
|
|
})
|
|
},
|
|
}
|
|
}
|
|
|
|
// ---- settings
|
|
|
|
func settingsCmd() *cobra.Command {
|
|
cmd := &cobra.Command{Use: "settings", Short: "Policies, built-in rules and other firewall-wide settings"}
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: "show", Short: "Show the settings", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
s, err := get[model.Settings]("/settings", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return output(s, func(w io.Writer) {
|
|
fmt.Fprintf(w, "Policies:\tinput %s, forward %s, output %s\n", s.InputPolicy, s.ForwardPolicy, s.OutputPolicy)
|
|
fmt.Fprintf(w, "Log policy drops:\t%s\n", yesNo(s.LogPolicyDrops))
|
|
b, _ := json.Marshal(s.Builtins)
|
|
var m map[string]bool
|
|
json.Unmarshal(b, &m)
|
|
var names []string
|
|
for n := range m {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
for i, n := range names {
|
|
label := ""
|
|
if i == 0 {
|
|
label = "Built-ins:"
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s %s\n", label, n, map[bool]string{true: "on", false: "off"}[m[n]])
|
|
}
|
|
fmt.Fprintf(w, "Own prefixes:\t%s\n", list(s.OwnPrefixes))
|
|
fmt.Fprintf(w, "Antispoof IPv4:\t%s\n", list(s.AntispoofV4))
|
|
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()))
|
|
fmt.Fprintf(w, "Traffic per device:\t%s\n", map[bool]string{true: "counted (statistics)", false: "not counted"}[s.StatsPerDevice])
|
|
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, autoConf, perDevice string
|
|
var builtins, own, flowtable, spoof4, spoof6 []string
|
|
set := &cobra.Command{
|
|
Use: "set [flags]", Short: "Change settings (only the flags given)", Args: cobra.NoArgs,
|
|
Long: `Changes the settings. Built-ins are rules Housecarl adds itself; each can be
|
|
turned off: loopback, established, drop_invalid, icmpv6, ping_internal,
|
|
dhcpv6_client, antispoof, mss_clamp.
|
|
|
|
hc settings set --builtin mss_clamp=on --own-prefix 2001:db8::/48
|
|
hc settings set --forward-policy drop --log-drops on
|
|
hc settings set --flowtable wan --flowtable eth1
|
|
hc settings set --stats-per-device on`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
fl := cmd.Flags()
|
|
p := patch{}
|
|
for flag, field := range map[string]string{"input-policy": "input_policy", "forward-policy": "forward_policy", "output-policy": "output_policy"} {
|
|
if fl.Changed(flag) {
|
|
v, _ := fl.GetString(flag)
|
|
p[field] = v
|
|
}
|
|
}
|
|
if fl.Changed("log-drops") {
|
|
on, err := onOff("log-drops", logDrops)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p["log_policy_drops"] = on
|
|
}
|
|
if len(builtins) > 0 {
|
|
b := map[string]bool{}
|
|
for _, x := range builtins {
|
|
k, v, ok := strings.Cut(x, "=")
|
|
on, err := onOff("builtin "+k, v)
|
|
if !ok || err != nil {
|
|
return fmt.Errorf("--builtin %q: write it as NAME=on or NAME=off", x)
|
|
}
|
|
b[strings.ReplaceAll(k, "-", "_")] = on
|
|
}
|
|
p["builtins"] = b
|
|
}
|
|
for flag, v := range map[string][]string{"own-prefix": own, "flowtable": flowtable, "antispoof-v4": spoof4, "antispoof-v6": spoof6} {
|
|
if fl.Changed(flag) {
|
|
p[map[string]string{"own-prefix": "own_prefixes", "flowtable": "flowtable_devices", "antispoof-v4": "antispoof_v4", "antispoof-v6": "antispoof_v6"}[flag]] = v
|
|
}
|
|
}
|
|
if fl.Changed("stats-per-device") {
|
|
on, err := onOff("stats-per-device", perDevice)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p["stats_per_device"] = on
|
|
}
|
|
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 {
|
|
return err
|
|
}
|
|
p["probation_seconds"] = n
|
|
}
|
|
return change("PATCH", "/settings", p)
|
|
},
|
|
}
|
|
f := set.Flags()
|
|
f.StringVar(&input, "input-policy", "", "accept or drop what no rule decided, to the firewall")
|
|
f.StringVar(&forward, "forward-policy", "", "… through it")
|
|
f.StringVar(&output, "output-policy", "", "… from it")
|
|
f.StringVar(&logDrops, "log-drops", "", "log what the input and forward policies drop: on or off")
|
|
f.StringArrayVar(&builtins, "builtin", nil, "turn a built-in on or off: NAME=on|off (repeat)")
|
|
f.StringArrayVar(&own, "own-prefix", nil, "your public prefixes, for antispoofing (repeat; replaces)")
|
|
f.StringArrayVar(&flowtable, "flowtable", nil, "fast-path devices (repeat; replaces; none: off)")
|
|
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(&perDevice, "stats-per-device", "", "count traffic per inside device, for the statistics' top devices: on or off")
|
|
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
|
|
}
|
|
|
|
func onOff(name, v string) (bool, error) {
|
|
switch strings.ToLower(v) {
|
|
case "on", "yes", "true":
|
|
return true, nil
|
|
case "off", "no", "false":
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("--%s: use on or off", name)
|
|
}
|
|
|
|
// ---- the ruleset, counters, the log
|
|
|
|
func rulesetCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "ruleset", Short: "Print the nftables ruleset Housecarl builds", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var text string
|
|
if err := c.Do(ctx(), "GET", "/ruleset", nil, nil, &text); err != nil {
|
|
return err
|
|
}
|
|
fmt.Print(text)
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func countersCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "counters", Short: "Hits of every rule, NAT rule and built-in", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cs, err := get[api.Counters]("/counters", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return output(cs, func(w io.Writer) {
|
|
fmt.Fprintf(w, "Since %s\n", cs.Since.Local().Format("2006-01-02 15:04"))
|
|
fmt.Fprintln(w, "MARK\tPACKETS\tBYTES")
|
|
var marks []string
|
|
for m := range cs.Counters {
|
|
marks = append(marks, m)
|
|
}
|
|
sort.Strings(marks)
|
|
for _, m := range marks {
|
|
c := cs.Counters[m]
|
|
fmt.Fprintf(w, "%s\t%d\t%d\n", m, c.Packets, c.Bytes)
|
|
}
|
|
})
|
|
},
|
|
}
|
|
}
|
|
|
|
func quotasCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "quotas", Aliases: []string{"quota"}, Short: "What the rules' quotas used, and when they reset", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
qs, err := get[[]api.QuotaInfo]("/quotas", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return output(qs, func(w io.Writer) {
|
|
fmt.Fprintln(w, "RULE\tUSED\tOF\tRESETS\tNEXT RESET")
|
|
for _, q := range qs {
|
|
resets := q.Quota.Reset
|
|
if resets == "" {
|
|
resets = "by hand"
|
|
} else if resets == "monthly" && q.Quota.Day > 1 {
|
|
resets = fmt.Sprintf("monthly (%d)", q.Quota.Day)
|
|
}
|
|
next := "-"
|
|
if q.NextReset != nil {
|
|
next = q.NextReset.Local().Format("2006-01-02 15:04")
|
|
}
|
|
of := humanBytes(q.Quota.Bytes)
|
|
if q.Quota.PerSource || q.Quota.PerDestination {
|
|
of += " each"
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", q.Rule, humanBytes(q.Used), of, resets, next)
|
|
for _, a := range q.Addresses {
|
|
fmt.Fprintf(w, " %s\t%s\t\t\t\n", a.Address, humanBytes(a.Used))
|
|
}
|
|
}
|
|
})
|
|
},
|
|
}
|
|
}
|
|
|
|
func logCmd() *cobra.Command {
|
|
var follow bool
|
|
var limit int
|
|
var search string
|
|
cmd := &cobra.Command{
|
|
Use: "log", Short: "Packets the ruleset logged (dropped by a rule or a policy)", Args: cobra.NoArgs,
|
|
Long: `Shows the packets rules with --log and the policies (settings
|
|
--log-drops) logged, newest last. -f keeps watching.`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var last time.Time
|
|
print := func(es []api.LogEntry) {
|
|
sort.Slice(es, func(i, j int) bool { return es[i].Time.Before(es[j].Time) })
|
|
for _, e := range es {
|
|
last = e.Time
|
|
src, dst := e.Source, e.Dest
|
|
if e.SrcPort != 0 {
|
|
src = joinPort(src, e.SrcPort)
|
|
}
|
|
if e.DestPort != 0 {
|
|
dst = joinPort(dst, e.DestPort)
|
|
}
|
|
what := e.Protocol
|
|
if e.ICMPType != "" {
|
|
what += " " + e.ICMPType
|
|
}
|
|
if e.TCPFlags != "" {
|
|
what += " " + e.TCPFlags
|
|
}
|
|
fmt.Printf("%s %-28s %-6s %-6s %s -> %s %s\n", e.Time.Local().Format("15:04:05"), strings.TrimSpace(e.Prefix), orDash(e.In), orDash(e.Out), src, dst, what)
|
|
}
|
|
}
|
|
for {
|
|
var es []api.LogEntry
|
|
query := q("limit", fmt.Sprint(limit), "q", search)
|
|
if !last.IsZero() {
|
|
query.Set("after", last.Format(time.RFC3339Nano))
|
|
}
|
|
if err := c.Get(ctx(), "/log", query, &es); err != nil {
|
|
return err
|
|
}
|
|
if flagOutput == "json" || flagOutput == "yaml" {
|
|
return output(es, nil)
|
|
}
|
|
print(es)
|
|
if !follow {
|
|
if len(es) == 0 {
|
|
fmt.Fprintln(os.Stderr, "nothing logged yet (log with 'hc rule update ID --log' or 'hc settings set --log-drops on')")
|
|
}
|
|
return nil
|
|
}
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
},
|
|
}
|
|
cmd.Flags().BoolVarP(&follow, "follow", "f", false, "keep watching")
|
|
cmd.Flags().IntVar(&limit, "limit", 50, "at most this many")
|
|
cmd.Flags().StringVar(&search, "search", "", "only entries containing this (address, interface, prefix)")
|
|
return cmd
|
|
}
|
|
|
|
func joinPort(addr string, port int) string {
|
|
if strings.Contains(addr, ":") {
|
|
return fmt.Sprintf("[%s]:%d", addr, port)
|
|
}
|
|
return fmt.Sprintf("%s:%d", addr, port)
|
|
}
|
|
|
|
// ---- probation
|
|
|
|
func confirmCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "confirm", Short: "Keep the changes on probation (otherwise they roll back)", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var out map[string]int
|
|
if err := c.Do(ctx(), "POST", "/confirm", nil, nil, &out); err != nil {
|
|
return err
|
|
}
|
|
if out["confirmed"] == 0 {
|
|
fmt.Println("nothing was waiting to be confirmed")
|
|
} else if out["confirmed"] == 1 {
|
|
fmt.Println("confirmed 1 change; it is kept and loaded at boot")
|
|
} else {
|
|
fmt.Printf("confirmed %d changes; they are kept and loaded at boot\n", out["confirmed"])
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func rollbackCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "rollback", Short: "Undo the changes on probation now", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var out map[string]int
|
|
if err := c.Do(ctx(), "POST", "/rollback", nil, nil, &out); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("rolled back %d changes\n", out["rolled_back"])
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// ---- import and takeover
|
|
|
|
func importCmd() *cobra.Command {
|
|
var replace bool
|
|
var table, defines, textFile string
|
|
cmd := &cobra.Command{
|
|
Use: "import [FILE|-]",
|
|
Short: "Import an nftables ruleset (nft -j JSON) as Housecarl's configuration",
|
|
Long: `Turns an existing nftables ruleset into zones, sets, services, rules, port
|
|
forwards and NAT. The input is nft's JSON; on the firewall:
|
|
|
|
sudo hc import --defines /etc/nftables.conf -n
|
|
|
|
Without a FILE, hc runs 'nft -j list ruleset' and 'nft -a list ruleset'
|
|
itself (root on the firewall); from elsewhere, give the JSON as FILE and
|
|
the text with --text. --defines reads the ruleset file's define lines, to
|
|
name the sets as you did (admins, lan_net, …). -n shows the result without
|
|
saving; what can't be expressed becomes raw rules, listed with notes.
|
|
Once it looks right, import without -n, check it with 'hc ruleset', and
|
|
'hc takeover inet main' deletes the old table.`,
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
var data, text []byte
|
|
var err error
|
|
switch {
|
|
case len(args) == 0:
|
|
data, err = exec.Command("nft", "-j", "list", "ruleset").Output()
|
|
if err != nil {
|
|
return fmt.Errorf("running 'nft -j list ruleset': %v (give a FILE, or run as root on the firewall)", err)
|
|
}
|
|
text, _ = exec.Command("nft", "-a", "list", "ruleset").Output()
|
|
case args[0] == "-":
|
|
data, err = io.ReadAll(os.Stdin)
|
|
default:
|
|
data, err = os.ReadFile(args[0])
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if textFile != "" {
|
|
if text, err = os.ReadFile(textFile); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
in := api.ImportInput{JSON: data, Text: string(text), Table: table, Replace: replace}
|
|
if defines != "" {
|
|
b, err := os.ReadFile(defines)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
in.Defines = string(b)
|
|
}
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var res api.ImportResult
|
|
if err := c.Do(ctx(), "POST", "/import/nft", nil, in, &res); err != nil {
|
|
return err
|
|
}
|
|
if flagOutput == "json" || flagOutput == "yaml" {
|
|
return output(res, nil)
|
|
}
|
|
st := res.State
|
|
fmt.Printf("%d zones, %d sets, %d services, %d rules, %d port forwards, %d source NAT rules, %d redirects\n",
|
|
len(st.Zones), len(st.Sets), len(st.Services), len(st.Rules), len(st.Forwards), len(st.SNATs), len(st.Redirects))
|
|
for _, n := range res.Notes {
|
|
fmt.Println("note:", n)
|
|
}
|
|
for _, r := range res.Raw {
|
|
fmt.Println("raw rule:", r)
|
|
}
|
|
return report(&res.MutationResponse)
|
|
},
|
|
}
|
|
cmd.Flags().BoolVar(&replace, "replace", false, "replace Housecarl's configuration (needed when it has one)")
|
|
cmd.Flags().StringVar(&table, "table", "", "import only this table (\"inet main\"; default: all but Housecarl's)")
|
|
cmd.Flags().StringVar(&defines, "defines", "", "the ruleset file, whose defines name the sets")
|
|
cmd.Flags().StringVar(&textFile, "text", "", "'nft -a list ruleset' output, to keep what can't be expressed as raw rules")
|
|
return cmd
|
|
}
|
|
|
|
func takeoverCmd() *cobra.Command {
|
|
var yes bool
|
|
cmd := &cobra.Command{
|
|
Use: "takeover TABLE...",
|
|
Short: "Delete the tables Housecarl replaces (\"inet main\"), on probation",
|
|
Long: `Deletes other nftables tables once Housecarl's ruleset replaces them (after
|
|
'hc import'). Until then both apply: a packet must get past both. Over the
|
|
network this is on probation like a change: unless confirmed, the tables
|
|
come back. Don't forget the boot: disable nftables.service, or empty
|
|
/etc/nftables.conf, or it brings the old ruleset back.
|
|
|
|
hc takeover "inet main"`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if !confirm("Delete "+strings.Join(args, ", ")+"?", yes) {
|
|
return fmt.Errorf("nothing deleted")
|
|
}
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var out map[string]any
|
|
if err := c.Do(ctx(), "POST", "/takeover", nil, api.TakeoverInput{Tables: args}, &out); err != nil {
|
|
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
|
|
},
|
|
}
|
|
cmd.Flags().BoolVarP(&yes, "yes", "y", false, "don't ask")
|
|
return cmd
|
|
}
|