Guard: watch logs, ban misbehaving addresses in Housecarl
Files and the journal, patterns with one <IP>, bounties within a window, bans in a Housecarl ban list with repeat offenders' bans growing; presets for sshd, jarl, housecarl, steward, bind-xfr, postfix-sasl, dovecot, nginx-auth and housecarl-drops; guard run/check/test/status/bans/ban/unban; a Debian package with a sandboxed unit; an end-to-end test with Housecarl. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
d06bf8d531
47 files changed
+4614
No files matched your search
@@ -0,0 +1,690 @@
|
||||
// Command guard watches logs and bans the addresses that misbehave in
|
||||
// them, at the firewall, through Housecarl's ban lists. One binary: the
|
||||
// daemon (guard run) and the commands to check, test and see it.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"guard/internal/config"
|
||||
"guard/internal/daemon"
|
||||
"guard/internal/housecarl"
|
||||
"guard/internal/match"
|
||||
"guard/internal/source"
|
||||
"guard/internal/state"
|
||||
"guard/internal/version"
|
||||
)
|
||||
|
||||
var (
|
||||
flagConfig string
|
||||
flagJSON bool
|
||||
)
|
||||
|
||||
func main() {
|
||||
root := &cobra.Command{
|
||||
Use: "guard",
|
||||
Short: "Guard watches logs and bans misbehaving addresses at the firewall (Housecarl)",
|
||||
Long: `Guard watches log files and the systemd journal for misdeeds (failed
|
||||
logins, scans), counts each address's bounty, and once it reaches a
|
||||
watch's hits bans the address in a Housecarl ban list: the firewall then
|
||||
keeps it from this machine (or from what the list says).
|
||||
|
||||
guard check check guard.yaml and reach Housecarl
|
||||
guard test ssh /var/log/auth.log what a watch's patterns find in a log
|
||||
guard status the watches, their highest bounties, recent bans
|
||||
|
||||
Exit codes: 0 ok, 1 error.`,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
Version: version.String(),
|
||||
}
|
||||
root.PersistentFlags().StringVarP(&flagConfig, "config", "c", config.DefaultPath, "configuration file")
|
||||
root.PersistentFlags().BoolVar(&flagJSON, "json", false, "print JSON instead of tables")
|
||||
root.AddCommand(runCmd(), checkCmd(), configCmd(), presetsCmd(), testCmd(), statusCmd(), bansCmd(), banCmd(), unbanCmd(), versionCmd())
|
||||
if err := root.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "guard:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func load() (*config.Config, error) { return config.Load(flagConfig) }
|
||||
|
||||
// client makes the Housecarl client of a configuration.
|
||||
func client(cfg *config.Config) (*housecarl.Client, error) {
|
||||
h := cfg.Housecarl
|
||||
tok := h.Token
|
||||
if h.TokenFile != "" {
|
||||
b, err := os.ReadFile(h.TokenFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the token: %w", err)
|
||||
}
|
||||
tok = strings.TrimSpace(string(b))
|
||||
}
|
||||
return housecarl.New(housecarl.Options{URL: h.URL, Socket: h.Socket, Token: tok, CACert: h.CACert, Insecure: h.Insecure})
|
||||
}
|
||||
|
||||
func logger(cfg *config.Config) *slog.Logger {
|
||||
var lv slog.Level
|
||||
lv.UnmarshalText([]byte(cfg.Log.Level))
|
||||
opts := &slog.HandlerOptions{Level: lv}
|
||||
if cfg.Log.Format == "json" {
|
||||
return slog.New(slog.NewJSONHandler(os.Stderr, opts))
|
||||
}
|
||||
return slog.New(slog.NewTextHandler(os.Stderr, opts))
|
||||
}
|
||||
|
||||
func table(f func(w io.Writer)) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
f(w)
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
func printJSON(v any) error {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(v)
|
||||
}
|
||||
|
||||
// ctx is the commands' context: each request has the client's timeout.
|
||||
func ctx() context.Context { return context.Background() }
|
||||
|
||||
// ---- run
|
||||
|
||||
func runCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "run", Short: "Watch the logs and ban (the daemon; systemd runs it)", Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log := logger(cfg)
|
||||
hc, err := client(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st, err := state.Load(cfg.State)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the state %s: %w", cfg.State, err)
|
||||
}
|
||||
d, err := daemon.New(cfg, hc, hc, st, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
log.Info("starting", "version", version.String(), "config", flagConfig)
|
||||
return d.Run(c)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ---- check
|
||||
|
||||
func checkCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "check", Short: "Check the configuration, and that Housecarl answers", Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s: ok\n", flagConfig)
|
||||
lists := map[string]bool{}
|
||||
drops := false
|
||||
table(func(w io.Writer) {
|
||||
fmt.Fprintln(w, "WATCH\tSOURCE\tPATTERNS\tBAN AFTER\tLIST\tPORTS")
|
||||
for _, x := range cfg.Watch {
|
||||
if x.Disabled {
|
||||
fmt.Fprintf(w, "%s\t(disabled)\t\t\t\t\n", x.Name)
|
||||
continue
|
||||
}
|
||||
lists[x.Ban.List] = true
|
||||
drops = drops || x.Kind() == config.SourceHousecarlDrops
|
||||
fmt.Fprintf(w, "%s\t%s\t%d\t%d in %s\t%s\t%s\n", x.Name, daemon.Describe(x), len(x.Match), x.Hits, x.Within, x.Ban.List, ports(x.Ban.Protocol, x.Ban.Port))
|
||||
}
|
||||
})
|
||||
hc, err := client(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bad := false
|
||||
for l := range lists {
|
||||
bl, err := hc.BanList(ctx(), l)
|
||||
switch {
|
||||
case err != nil:
|
||||
fmt.Printf("ban list %s: %v\n", l, err)
|
||||
bad = true
|
||||
case bl.Disabled:
|
||||
fmt.Printf("ban list %s: disabled in Housecarl (bans aren't in force)\n", l)
|
||||
default:
|
||||
fmt.Printf("ban list %s: ok (scope %s, IPv6 banned by /%d)\n", l, bl.Scope, bl.V6Group())
|
||||
}
|
||||
}
|
||||
if drops {
|
||||
if _, err := hc.Log(ctx(), time.Now()); err != nil {
|
||||
fmt.Printf("Housecarl's log (housecarl-drops): %v (the token needs log:read)\n", err)
|
||||
bad = true
|
||||
} else {
|
||||
fmt.Println("Housecarl's log (housecarl-drops): ok")
|
||||
}
|
||||
}
|
||||
if bad {
|
||||
return errors.New("Housecarl isn't ready for these watches")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ports(proto string, p []string) string {
|
||||
if len(p) == 0 {
|
||||
return "all"
|
||||
}
|
||||
if proto == "" {
|
||||
return strings.Join(p, ",")
|
||||
}
|
||||
return strings.ToLower(proto) + "/" + strings.Join(p, ",")
|
||||
}
|
||||
|
||||
// ---- config
|
||||
|
||||
func configCmd() *cobra.Command {
|
||||
var example bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "config", Short: "Print the configuration as read (--example: a commented one)", Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if example {
|
||||
fmt.Print(config.Example())
|
||||
return nil
|
||||
}
|
||||
cfg, err := load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printJSON(cfg)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&example, "example", false, "print a commented example configuration")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ---- presets
|
||||
|
||||
func presetsCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "presets [PRESET]", Short: "List the presets, or show one's patterns", Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ps := config.Presets()
|
||||
if len(args) == 1 {
|
||||
p, ok := ps[args[0]]
|
||||
if !ok {
|
||||
return fmt.Errorf("there is no preset %q", args[0])
|
||||
}
|
||||
if flagJSON {
|
||||
return printJSON(p)
|
||||
}
|
||||
w := &config.Watch{Preset: args[0], File: p.File, Journal: p.Journal, Source: p.Source}
|
||||
fmt.Printf("%s: %s\nSource: %s\n", args[0], p.Description, daemon.Describe(w))
|
||||
for _, m := range p.Match {
|
||||
fmt.Printf("Match: %s\n", m)
|
||||
}
|
||||
for _, m := range p.Ignore {
|
||||
fmt.Printf("Ignore: %s\n", m)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var names []string
|
||||
for n := range ps {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
if flagJSON {
|
||||
return printJSON(ps)
|
||||
}
|
||||
table(func(w io.Writer) {
|
||||
fmt.Fprintln(w, "PRESET\tSOURCE\tWHAT")
|
||||
for _, n := range names {
|
||||
p := ps[n]
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", n, daemon.Describe(&config.Watch{File: p.File, Journal: p.Journal, Source: p.Source, Protocol: "tcp"}), p.Description)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ---- test
|
||||
|
||||
func testCmd() *cobra.Command {
|
||||
var lines int
|
||||
var all bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "test WATCH [FILE|-]",
|
||||
Short: "Run a watch's patterns over a log: what they match, whom they'd ban",
|
||||
Long: `Runs a watch's patterns over a log, from its start: a file, standard
|
||||
input (-), or for a journal watch without FILE its last --lines entries.
|
||||
It prints each matching line's address, and the addresses that would be
|
||||
banned. Nothing is banned.
|
||||
|
||||
A file's lines carry no time Guard reads, so for a file every match
|
||||
counts, however far apart; the journal's entries have their times, and
|
||||
the watch's window (within) is used.`,
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var w *config.Watch
|
||||
for _, x := range cfg.Watch {
|
||||
if strings.EqualFold(x.Name, args[0]) {
|
||||
w = x
|
||||
}
|
||||
}
|
||||
if w == nil {
|
||||
return fmt.Errorf("there is no watch %q in %s", args[0], flagConfig)
|
||||
}
|
||||
if w.Kind() == config.SourceHousecarlDrops {
|
||||
return errors.New("a housecarl-drops watch has no patterns to test: every dropped packet counts")
|
||||
}
|
||||
m, err := match.Compile(w.Match, w.Ignore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var in []source.Line
|
||||
timed := false
|
||||
switch {
|
||||
case len(args) == 2:
|
||||
in, err = readLines(args[1])
|
||||
case w.Kind() == "journal":
|
||||
in, err = readJournal(w, lines)
|
||||
timed = true
|
||||
default:
|
||||
in, err = readLines(w.File)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
never := daemon.NewNeverBan(cfg.NeverBan, nil)
|
||||
type hit struct {
|
||||
count int
|
||||
ban bool
|
||||
}
|
||||
hits := map[string]*hit{}
|
||||
order := []string{}
|
||||
tr := map[string][]time.Time{}
|
||||
matched := 0
|
||||
for _, l := range in {
|
||||
a, ok := m.Match(l.Text)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
matched++
|
||||
why := never.Why(a)
|
||||
if all || matched <= 200 {
|
||||
note := ""
|
||||
if why != "" {
|
||||
note = " (never banned: " + why + ")"
|
||||
}
|
||||
fmt.Printf("%-39s %s%s\n", a, clip(l.Text, 160), note)
|
||||
}
|
||||
if why != "" {
|
||||
continue
|
||||
}
|
||||
g := match.Group(a, 64).String()
|
||||
h := hits[g]
|
||||
if h == nil {
|
||||
h = &hit{}
|
||||
hits[g] = h
|
||||
order = append(order, g)
|
||||
}
|
||||
h.count++
|
||||
if timed {
|
||||
ts := append(tr[g], l.Time)
|
||||
for len(ts) > 0 && ts[0].Before(l.Time.Add(-w.Within.D())) {
|
||||
ts = ts[1:]
|
||||
}
|
||||
tr[g] = ts
|
||||
h.ban = h.ban || len(ts) >= w.Hits
|
||||
} else {
|
||||
h.ban = h.ban || h.count >= w.Hits
|
||||
}
|
||||
}
|
||||
if matched > 200 && !all {
|
||||
fmt.Printf("… %d more matching lines (--all shows them)\n", matched-200)
|
||||
}
|
||||
fmt.Printf("\n%d lines, %d matched, %d addresses (IPv6 by /64)\n", len(in), matched, len(order))
|
||||
sort.SliceStable(order, func(i, j int) bool { return hits[order[i]].count > hits[order[j]].count })
|
||||
banned := 0
|
||||
for _, g := range order {
|
||||
if hits[g].ban {
|
||||
banned++
|
||||
}
|
||||
}
|
||||
if banned == 0 {
|
||||
fmt.Printf("nobody would be banned (%d hits within %s)\n", w.Hits, w.Within)
|
||||
return nil
|
||||
}
|
||||
window := "within " + w.Within.String()
|
||||
if !timed {
|
||||
window = "in all (a file's lines have no time)"
|
||||
}
|
||||
fmt.Printf("would ban %d (%d hits %s):\n", banned, w.Hits, window)
|
||||
for _, g := range order {
|
||||
if hits[g].ban {
|
||||
fmt.Printf(" %-39s %d matches\n", g, hits[g].count)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().IntVarP(&lines, "lines", "n", 5000, "a journal watch without FILE: the last this many entries")
|
||||
cmd.Flags().BoolVar(&all, "all", false, "print every matching line, not the first 200")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func readLines(path string) ([]source.Line, error) {
|
||||
var r io.Reader = os.Stdin
|
||||
if path != "-" {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
r = f
|
||||
}
|
||||
var out []source.Line
|
||||
sc := bufio.NewScanner(r)
|
||||
sc.Buffer(make([]byte, 64<<10), 1<<20)
|
||||
for sc.Scan() {
|
||||
out = append(out, source.Line{Text: sc.Text()})
|
||||
}
|
||||
return out, sc.Err()
|
||||
}
|
||||
|
||||
func readJournal(w *config.Watch, n int) ([]source.Line, error) {
|
||||
c := exec.Command(source.JournalCommand(), source.JournalArgs(w.Journal.Unit, w.Journal.Identifier, false, "", n)...)
|
||||
c.Stderr = os.Stderr
|
||||
outp, err := c.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ch := make(chan source.Line, 64)
|
||||
done := make(chan []source.Line)
|
||||
go func() {
|
||||
var out []source.Line
|
||||
for l := range ch {
|
||||
out = append(out, l)
|
||||
}
|
||||
done <- out
|
||||
}()
|
||||
source.ReadJournal(context.Background(), outp, ch, "")
|
||||
close(ch)
|
||||
lines := <-done
|
||||
if err := c.Wait(); err != nil {
|
||||
return nil, fmt.Errorf("journalctl: %w (Guard's user needs the systemd-journal group)", err)
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
func clip(s string, n int) string {
|
||||
if len(s) > n {
|
||||
return s[:n-1] + "…"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ---- status
|
||||
|
||||
func statusCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "status", Short: "The running Guard: its watches, highest bounties, recent bans", Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hc := &http.Client{Timeout: 10 * time.Second, Transport: &http.Transport{DialContext: func(c context.Context, _, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(c, "unix", cfg.Socket)
|
||||
}}}
|
||||
resp, err := hc.Get("http://guard/status")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Guard isn't running, or %s can't be reached (as root, or the guard user): %w", cfg.Socket, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var s daemon.Status
|
||||
if err := json.NewDecoder(resp.Body).Decode(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
if flagJSON {
|
||||
return printJSON(s)
|
||||
}
|
||||
fmt.Printf("Running since %s (%s)\n\n", s.Started.Local().Format("2006-01-02 15:04"), config.FormatDuration(time.Since(s.Started).Round(time.Second)))
|
||||
table(func(w io.Writer) {
|
||||
fmt.Fprintln(w, "WATCH\tSOURCE\tLINES\tMATCHES\tBANS\tBAN AFTER\tHIGHEST BOUNTIES")
|
||||
for _, x := range s.Watches {
|
||||
var top []string
|
||||
for i, b := range x.Top {
|
||||
if i == 3 {
|
||||
break
|
||||
}
|
||||
top = append(top, fmt.Sprintf("%s %d/%d", b.Address, b.Bounty, x.Hits))
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%d\t%d\t%d\t%d in %s\t%s\n", x.Name, x.Source, x.Lines, x.Matches, x.Bans, x.Hits, x.Within, strings.Join(top, ", "))
|
||||
}
|
||||
})
|
||||
if len(s.Pending) > 0 {
|
||||
fmt.Printf("\n%d bans wait for Housecarl\n", len(s.Pending))
|
||||
}
|
||||
if len(s.Recent) > 0 {
|
||||
fmt.Println("\nRecent bans:")
|
||||
table(func(w io.Writer) {
|
||||
fmt.Fprintln(w, "TIME\tWATCH\tADDRESS\tLIST\tPORTS\tFOR\tRESULT")
|
||||
for i, b := range s.Recent {
|
||||
if i == 20 {
|
||||
break
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", b.Time.Local().Format("01-02 15:04"), b.Watch, b.Address, b.List,
|
||||
ports(b.Protocol, splitComma(b.Ports)), config.FormatDuration(time.Duration(b.Seconds)*time.Second), clip(b.Result, 60))
|
||||
}
|
||||
})
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func splitComma(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(s, ",")
|
||||
}
|
||||
|
||||
// ---- bans in Housecarl
|
||||
|
||||
func listFlag(cfg *config.Config, list string) (string, error) {
|
||||
if list != "" {
|
||||
return list, nil
|
||||
}
|
||||
if cfg.Housecarl.List == "" {
|
||||
return "", errors.New("which ban list? Give --list, or set housecarl.list")
|
||||
}
|
||||
return cfg.Housecarl.List, nil
|
||||
}
|
||||
|
||||
func bansCmd() *cobra.Command {
|
||||
var list string
|
||||
cmd := &cobra.Command{
|
||||
Use: "bans", Short: "The active bans in Housecarl (of the ban list, or --list)", Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l, err := listFlag(cfg, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hc, err := client(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bans, err := hc.Bans(ctx(), l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if flagJSON {
|
||||
return printJSON(bans)
|
||||
}
|
||||
table(func(w io.Writer) {
|
||||
fmt.Fprintln(w, "ID\tADDRESS\tPORT\tENDS IN\tBY\tSOURCE")
|
||||
for _, b := range bans {
|
||||
port := "all"
|
||||
if b.Protocol != "" {
|
||||
port = b.Protocol + "/" + strconv.Itoa(b.Port)
|
||||
}
|
||||
ends := "never"
|
||||
if b.Timeout > 0 {
|
||||
ends = config.FormatDuration(time.Duration(b.Timeout) * time.Second)
|
||||
}
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\n", b.ID, b.Address, port, ends, b.Who, b.Source)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&list, "list", "", "the ban list (default: housecarl.list)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func banCmd() *cobra.Command {
|
||||
var list, t, reason, proto string
|
||||
var port []string
|
||||
cmd := &cobra.Command{
|
||||
Use: "ban ADDRESS", Short: "Ban an address by hand", Args: cobra.ExactArgs(1),
|
||||
Long: `Bans an address in Housecarl, as a watch would: every port, or with
|
||||
--protocol and --port some (numbers or service names).
|
||||
|
||||
guard ban 203.0.113.9 --time 1d
|
||||
guard ban 203.0.113.9 --protocol tcp --port 22`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l, err := listFlag(cfg, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := netip.ParseAddr(strings.Trim(args[0], "[]")); err != nil {
|
||||
return fmt.Errorf("%q isn't an address", args[0])
|
||||
}
|
||||
in := housecarl.BanInput{Address: args[0], Protocol: proto, Ports: strings.Join(port, ","), Source: "guard (by hand)", Reason: reason}
|
||||
if t != "" {
|
||||
d, err := config.ParseDuration(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
in.Time = int(d / time.Second)
|
||||
}
|
||||
hc, err := client(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := hc.Ban(ctx(), l, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if flagJSON {
|
||||
return printJSON(res)
|
||||
}
|
||||
for _, b := range res.Bans {
|
||||
what := "banned"
|
||||
if b.Existing {
|
||||
what = "renewed"
|
||||
}
|
||||
port := "every port"
|
||||
if b.Protocol != "" {
|
||||
port = b.Protocol + "/" + strconv.Itoa(b.Port)
|
||||
}
|
||||
fmt.Printf("%s %s, %s, in %s, for %s\n", what, b.Address, port, b.List, config.FormatDuration(time.Duration(b.Timeout)*time.Second))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
f := cmd.Flags()
|
||||
f.StringVar(&list, "list", "", "the ban list (default: housecarl.list)")
|
||||
f.StringVar(&proto, "protocol", "", "tcp or udp (with --port)")
|
||||
f.StringArrayVar(&port, "port", nil, "a port or service (repeat; default: every port)")
|
||||
f.StringVar(&t, "time", "", "how long (1h, 1d; default: the list's)")
|
||||
f.StringVar(&reason, "reason", "", "why, for the record")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func unbanCmd() *cobra.Command {
|
||||
var list string
|
||||
cmd := &cobra.Command{
|
||||
Use: "unban ADDRESS", Short: "Lift an address's bans in the ban list", Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l, err := listFlag(cfg, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hc, err := client(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := hc.Unban(ctx(), l, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%d bans lifted\n", n)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&list, "list", "", "the ban list (default: housecarl.list)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func versionCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version", Short: "Print the version", Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if flagJSON {
|
||||
printJSON(version.Info())
|
||||
return
|
||||
}
|
||||
fmt.Println("guard", version.String())
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user