Skip to content
Snippets Groups Projects
main.go 4.67 KiB
package main

import (
	"bufio"
	"bytes"
	"context"
	"flag"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"time"

	"go.science.ru.nl/log"
	"go.science.ru.nl/promfmt"
)

var (
	flagWrite    = flag.Bool("w", true, "write to /var/lib/prometheus/node-exporter/f2bne.prom")
	flagDuration = flag.Uint("t", 60, "default duration to export in seconds")
	flagDebug    = flag.Bool("d", false, "enable debug logging")
)

const promfile = "/var/lib/prometheus/node-exporter/f2bne.prom"

func main() {
	flag.Parse()
	if *flagDebug {
		log.D.Set()
	}
	doit()
}

type jail struct {
	name string

	failedTotal float64
	bannedTotal float64
	failedNow   float64
	bannedNow   float64
}

func fail2ban(args ...string) ([]byte, error) {
	ctx := context.TODO()
	cmd := exec.CommandContext(ctx, "fail2ban-client", args...)
	log.Debugf("running fail2ban %v", cmd.Args)

	out, err := cmd.CombinedOutput()
	if len(out) > 0 {
		log.Debug(string(out))
	}

	return out, err
}

// parseJailList parses f2b-client status and return the 'jails' it has configured.
func parseJailList(data []byte) []string {
	// output:
	//  Status
	//  |- Number of jail:	1
	//  `- Jail list:	cyrus
	jails := []string{}
	scanner := bufio.NewScanner(bytes.NewReader(data))
	for scanner.Scan() {
		text := strings.ToLower(scanner.Text())
		if strings.Contains(text, "jail list:") {
			k := strings.Index(text, "jail list:")
			leftover := text[k+len("jail list:"):]
			jails = strings.Split(leftover, ",")
		}
	}

	if scanner.Err() != nil {
		return nil
	}
	for i := range jails {
		jails[i] = strings.TrimSpace(jails[i])

	}
	return jails
}

func parseJail(data []byte, jl string) *jail {
	// output:
	// Status for the jail: sshd
	// |- Filter
	// |  |- Currently failed:	0
	// |  |- Total failed:	0
	// |  `- File list:	/var/log/auth.log
	// `- Actions
	//    |- Currently banned:	0
	//    |- Total banned:	0
	//    `- Banned IP list:
	scanner := bufio.NewScanner(bytes.NewReader(data))
	j := &jail{name: jl}
	const (
		failed    = "total failed:"
		banned    = "total banned:"
		failednow = "currently failed:"
		bannednow = "currently banned:"
	)
	for scanner.Scan() {
		text := strings.ToLower(scanner.Text())
		switch {
		case strings.Contains(text, failed):
			i, err := parseInt(text, failed)
			if err != nil {
				log.Warning(err.Error())
				return nil
			}
			j.failedTotal = float64(i)

		case strings.Contains(text, banned):
			i, err := parseInt(text, banned)
			if err != nil {
				log.Warning(err.Error())
				return nil
			}
			j.bannedTotal = float64(i)

		case strings.Contains(text, failednow):
			i, err := parseInt(text, failednow)
			if err != nil {
				log.Warning(err.Error())
				return nil
			}
			j.failedNow = float64(i)

		case strings.Contains(text, bannednow):
			i, err := parseInt(text, bannednow)
			if err != nil {
				log.Warning(err.Error())
				return nil
			}
			j.bannedNow = float64(i)
		}
	}

	// fail2ban outputs _all_ addreses blocked, this leads to token too long, but we actually don't parse that line
	// just return the jail stuff we found.
	if err := scanner.Err(); err != nil {
		log.Warning(err.Error())
	}

	return j
}

func doit() {
	out, err := fail2ban("status")
	if err != nil {
		// don't update metrics so timestamp will age.
		log.Warningf("Failed to run fail2ban status")
		return
	}
	names := parseJailList(out)
	jails := []*jail{}
	for i := range names {
		out, err := fail2ban("status", names[i])
		if err != nil {
			// don't update metrics so timestamp will age.
			log.Warningf("Failed to run fail2ban status ...")
			continue
		}
		j := parseJail(out, names[i])
		if j == nil {
			log.Warningf("Failed to parse fail2ban jail output")
			continue
		}
		jails = append(jails, j)
	}
	if len(jails) != len(names) {
		log.Warningf("Not all jails are found or parsed %d != %d", len(jails), len(names))
		return
	}
	log.Infof("Getting data for %d jails", len(jails))
	for i := range jails {
		j := jails[i]
		metricFailedTotal.WithLabelValues(j.name).Set(float64(j.failedTotal))
		metricBannedTotal.WithLabelValues(j.name).Set(float64(j.bannedTotal))
		metricFailedNow.WithLabelValues(j.name).Set(float64(j.failedNow))
		metricBannedNow.WithLabelValues(j.name).Set(float64(j.bannedNow))
	}

	metricLastRunTimestamp.Set(float64(time.Now().Unix()))
	metricRunDuration.Set(float64(*flagDuration))

	if !*flagWrite {
		promfmt.Fprint(os.Stdout, promfmt.NewPrefixFilter("f2bne_"))
		return
	}

	if err := promfmt.WriteFile(promfile, promfmt.NewPrefixFilter("f2bne_")); err != nil {
		log.Fatalf("Failed to write to prom file: %s", err)
	}
}

func parseInt(text, substring string) (int64, error) {
	k := strings.Index(text, substring)
	num := strings.TrimSpace(text[k+len(substring):])
	i, err := strconv.ParseInt(num, 10, 64)
	if err != nil {
		return 0, err
	}
	return i, nil
}