go-firewall/cmd/go-firewall/info.go
2026-07-08 15:54:48 -05:00

138 lines
3.7 KiB
Go

package main
import (
"context"
"fmt"
"io"
"os"
"reflect"
"sort"
fw "github.com/grmrgecko/firewall"
)
// This file holds the read-only/info subcommands: status (backend + caps),
// zone (resolve an interface's zone), and reload (force a backend reload).
// StatusCmd reports the detected backend and its capabilities. It is the
// default thing to run when orienting on an unfamiliar host.
type StatusCmd struct{}
// Run reports the detected backend and its capabilities.
func (c *StatusCmd) Run(g *Globals) error {
mgr, cleanup, err := g.manager()
if err != nil {
return err
}
defer cleanup()
caps := mgr.Capabilities()
type statusOut struct {
Backend string `json:"backend"`
Prefix string `json:"prefix"`
Capabilities fw.Capabilities `json:"capabilities"`
}
out := statusOut{
Backend: mgr.Type(),
Prefix: g.Prefix,
Capabilities: caps,
}
return g.emit(out, func() error {
fmt.Printf("backend: %s\n", out.Backend)
fmt.Printf("prefix: %s\n", out.Prefix)
fmt.Println()
fmt.Println("capabilities:")
printCapabilities(os.Stdout, caps)
return nil
})
}
// printCapabilities lists the capability booleans in a stable, readable order.
// It walks the struct via reflection so a new bool field in fw.Capabilities is
// surfaced automatically without a parallel slice here. Non-bool fields are
// skipped so a future non-bool capability cannot panic the reflection walk.
func printCapabilities(w io.Writer, caps fw.Capabilities) {
v := reflect.ValueOf(caps)
t := v.Type()
type field struct {
name string
ok bool
}
fields := make([]field, 0, t.NumField())
for i := 0; i < t.NumField(); i++ {
if v.Field(i).Kind() != reflect.Bool {
continue
}
fields = append(fields, field{
name: t.Field(i).Name,
ok: v.Field(i).Bool(),
})
}
sort.Slice(fields, func(i, j int) bool { return fields[i].name < fields[j].name })
maxLen := 0
for _, f := range fields {
if len(f.name) > maxLen {
maxLen = len(f.name)
}
}
for _, f := range fields {
mark := "-"
if f.ok {
mark = "yes"
}
_, _ = fmt.Fprintf(w, " %-*s %s\n", maxLen, f.name, mark)
}
}
// ZoneCmd resolves the zone an interface belongs to. On backends without zones
// (iptables, nftables, pf, WFP) the returned zone is empty and that is the
// correct answer — it just means the backend has no zone abstraction.
type ZoneCmd struct {
Interface string `arg:"" name:"interface" help:"Network interface to resolve (e.g. eth0)."`
}
// Run resolves and prints the zone the interface belongs to.
func (c *ZoneCmd) Run(g *Globals) error {
mgr, cleanup, err := g.manager()
if err != nil {
return err
}
defer cleanup()
zone, err := mgr.GetZone(context.Background(), c.Interface)
if err != nil {
return fmt.Errorf("resolving zone for %q: %w", c.Interface, err)
}
type zoneOut struct {
Interface string `json:"interface"`
Zone string `json:"zone"`
}
out := zoneOut{Interface: c.Interface, Zone: zone}
return g.emit(out, func() error {
// A zoneless backend returns an empty zone; show a human placeholder here
// only, so the JSON form keeps the honest empty string.
display := out.Zone
if display == "" {
display = "(none)"
}
fmt.Printf("%s -> %s\n", out.Interface, display)
return nil
})
}
// ReloadCmd forces a backend reload, activating any staged rules. Useful after
// a series of --no-reload mutations, or to pick up changes made out of band.
type ReloadCmd struct{}
// Run forces a backend reload, activating any staged rules.
func (c *ReloadCmd) Run(g *Globals) error {
mgr, cleanup, err := g.manager()
if err != nil {
return err
}
defer cleanup()
if err := mgr.Reload(context.Background()); err != nil {
return fmt.Errorf("reload: %w", err)
}
return g.emitStatus("reloaded")
}