diff --git a/README.md b/README.md index f40956c..aabd046 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Flags: --auth-user string username for basic auth, also F2BD_AUTH_USER -c, --cache-dir string directory to cache GeoIP data, also F2BD_CACHE_DIR (default current working directory) -h, --help help for fail2ban-dashboard + --log-level string log level (trace, debug, info, warn, error), also F2BD_LOG_LEVEL (default "info") -s, --socket string fail2ban socket, also F2BD_SOCKET (default "/var/run/fail2ban/fail2ban.sock") Use "fail2ban-dashboard [command] --help" for more information about a command. diff --git a/cmd/fail2ban-dashboard/fail2ban-dashboard.go b/cmd/fail2ban-dashboard/fail2ban-dashboard.go index 3e70390..d929515 100644 --- a/cmd/fail2ban-dashboard/fail2ban-dashboard.go +++ b/cmd/fail2ban-dashboard/fail2ban-dashboard.go @@ -78,6 +78,13 @@ func init() { os.Exit(1) } + flags.String("log-level", "info", "log level (trace, debug, info, warn, error), also F2BD_LOG_LEVEL") + logLevelErr := viper.BindPFlag("log-level", flags.Lookup("log-level")) + if logLevelErr != nil { + fmt.Printf("Could not bind log-level flag: %s\n", logLevelErr) + os.Exit(1) + } + rootCmd.AddCommand(versionCmd) } @@ -96,6 +103,26 @@ func run(_ *cobra.Command, _ []string) { user := viper.GetString("auth-user") password := viper.GetString("auth-password") cacheDir := viper.GetString("cache-dir") + logLevel := viper.GetString("log-level") + + // Set log level + switch logLevel { + case "trace": + log.SetLevel(log.LevelTrace) + case "debug": + log.SetLevel(log.LevelDebug) + case "info": + log.SetLevel(log.LevelInfo) + case "warn": + log.SetLevel(log.LevelWarn) + case "error": + log.SetLevel(log.LevelError) + default: + log.SetLevel(log.LevelInfo) + log.Warnf("Invalid log level '%s', using 'info'", logLevel) + } + + log.Debugf("Log level set to %s", logLevel) log.Infof("Will use socket at %s for fail2ban connection", socketPath) f2bc, socketError := client.NewFail2BanClient(socketPath) @@ -111,7 +138,7 @@ func run(_ *cobra.Command, _ []string) { panic(versionError) } - fmt.Printf("fail2ban version found: %s\n", detectedFail2banVersion) + log.Infof("fail2ban version found: %s\n", detectedFail2banVersion) versionIsOk := false for _, supportedVersion := range supportedVersions { @@ -120,7 +147,7 @@ func run(_ *cobra.Command, _ []string) { } } if !versionIsOk { - fmt.Printf("fail2ban version %s not supported\n", detectedFail2banVersion) + log.Errorf("fail2ban version %s not supported\n", detectedFail2banVersion) os.Exit(1) } @@ -163,7 +190,7 @@ func run(_ *cobra.Command, _ []string) { serveError := server.Serve(Version, fail2banVersion, dataStore, geoIP, configuration) if serveError != nil { - fmt.Printf("Could not start server: %s\n", serveError) + log.Errorf("Could not start server: %s\n", serveError) os.Exit(1) } } diff --git a/e2e/README.md b/e2e/README.md index 2a24214..802ad10 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -20,4 +20,6 @@ Then use the following commands to check the status or ban some addresses: - `fail2ban-client set apache-badbots banip 18.55.23.99` - `fail2ban-client set apache-noscript banip 221.123.11.2` - `fail2ban-client set apache-overflows banip 32.23.1.2` -- `fail2ban-client set sshd banip 103.59.94.155 196.251.84.225 218.92.0.247 && fail2ban-client set apache-auth bantime 20000 && fail2ban-client set apache-auth banip 78.88.88.99 && fail2ban-client set apache-badbots banip 18.55.23.99 && fail2ban-client set apache-noscript banip 221.123.11.2 && fail2ban-client set apache-overflows banip 32.23.1.2` \ No newline at end of file +- `fail2ban-client set sshd banip 103.59.94.155 196.251.84.225 218.92.0.247 && fail2ban-client set apache-auth bantime 20000 && fail2ban-client set apache-auth banip 78.88.88.99 && fail2ban-client set apache-badbots banip 18.55.23.99 && fail2ban-client set apache-noscript banip 221.123.11.2 && fail2ban-client set apache-overflows banip 32.23.1.2` + +Start fail2ban-dashboard with ` ./files/bin/fail2ban-dashboard -a 0.0.0.0:3000` \ No newline at end of file diff --git a/fail2ban-client/fail2ban-client.go b/fail2ban-client/fail2ban-client.go index b8539a0..a1a7a4a 100644 --- a/fail2ban-client/fail2ban-client.go +++ b/fail2ban-client/fail2ban-client.go @@ -5,14 +5,16 @@ import ( "bytes" "errors" "fmt" - ogórek "github.com/kisielk/og-rek" - "github.com/nlpodyssey/gopickle/pickle" - "github.com/nlpodyssey/gopickle/types" "net" "regexp" "strings" "sync" "time" + + "github.com/gofiber/fiber/v2/log" + ogórek "github.com/kisielk/og-rek" + "github.com/nlpodyssey/gopickle/pickle" + "github.com/nlpodyssey/gopickle/types" ) const ( @@ -58,11 +60,14 @@ type Fail2BanClient struct { } func NewFail2BanClient(address string) (*Fail2BanClient, error) { + log.Tracef("Attempting to connect to fail2ban socket at %s", address) socket, err := net.Dial("unix", address) if err != nil { + log.Errorf("Failed to connect to fail2ban socket at %s: %v", address, err) return nil, err } + log.Debugf("Successfully connected to fail2ban socket at %s", address) encoder := ogórek.NewEncoder(socket) return &Fail2BanClient{ @@ -72,26 +77,34 @@ func NewFail2BanClient(address string) (*Fail2BanClient, error) { } func (f2bc *Fail2BanClient) GetVersion() (string, error) { + log.Trace("GetVersion: Fetching fail2ban version") result, err := f2bc.sendCommand([]string{versionCommand}) if err != nil { + log.Errorf("GetVersion: Failed to get version: %v", err) return "", err } + log.Tracef("GetVersion: Received result type: %T", result) if versionTuple, tupleOk := result.(*types.Tuple); tupleOk { if versionStr, versionOk := versionTuple.Get(1).(string); versionOk { + log.Debugf("GetVersion: Successfully retrieved version: %s", versionStr) return versionStr, nil } } + log.Error("GetVersion: Failed to parse version from response") return "", errors.New("fetching version failed") } func (f2bc *Fail2BanClient) GetJailNames() ([]string, error) { + log.Trace("GetJailNames: Fetching jail names") result, err := f2bc.sendCommand([]string{statusCommand}) if err != nil { + log.Errorf("GetJailNames: Failed to get status: %v", err) return []string{}, err } + log.Tracef("GetJailNames: Received result type: %T", result) expectedJailCount := 0 if statusTuple, tupleOk := result.(*types.Tuple); tupleOk { @@ -118,8 +131,10 @@ func (f2bc *Fail2BanClient) GetJailNames() ([]string, error) { } if len(jailNames) != expectedJailCount { + log.Errorf("GetJailNames: Jail count mismatch - expected %d, got %d", expectedJailCount, len(jailNames)) return []string{}, errors.New("number of jails did not match") } + log.Debugf("GetJailNames: Successfully retrieved %d jails: %v", len(jailNames), jailNames) return jailNames, nil } else { return []string{}, errors.New("jail list could not be read") @@ -134,12 +149,14 @@ func (f2bc *Fail2BanClient) GetJailNames() ([]string, error) { } func (f2bc *Fail2BanClient) GetJailInfo(jailName string) (*JailInfo, error) { + log.Tracef("GetJailInfo: Fetching info for jail '%s'", jailName) result, err := f2bc.sendCommand([]string{statusCommand, jailName}) if err != nil { + log.Errorf("GetJailInfo: Failed to get info for jail '%s': %v", jailName, err) return nil, err } - // fmt.Printf("Result: %#v - %v\n", result, result) + log.Tracef("GetJailInfo: Received result type: %T", result) currentlyFailedResult := 0 totalFailedResult := 0 @@ -205,30 +222,39 @@ func (f2bc *Fail2BanClient) GetJailInfo(jailName string) (*JailInfo, error) { } } - return &JailInfo{ + jailInfo := &JailInfo{ CurrentlyFailed: currentlyFailedResult, TotalFailed: totalFailedResult, CurrentlyBanned: currentlyBannedResult, TotalBanned: totalBannedResult, - }, nil + } + log.Debugf("GetJailInfo: Successfully retrieved info for jail '%s': Failed=%d/%d, Banned=%d/%d", + jailName, currentlyFailedResult, totalFailedResult, currentlyBannedResult, totalBannedResult) + return jailInfo, nil } func (f2bc *Fail2BanClient) GetBanned(jailName string) (*JailEntry, error) { + log.Tracef("GetBanned: Fetching banned IPs for jail '%s'", jailName) var bannedEntries []*BanEntry result, err := f2bc.sendCommand([]string{getCommand, jailName, "banip", "--with-time"}) if err != nil { + log.Errorf("GetBanned: Failed to get banned IPs for jail '%s': %v", jailName, err) return nil, err } + log.Tracef("GetBanned: Received result type: %T", result) + if getBanTuple, getBanOk := result.(*types.Tuple); getBanOk { if banList, banListOk := getBanTuple.Get(1).(*types.List); banListOk { banListLen := banList.Len() for index := 0; index < banListLen; index++ { if listEnty, listEntyOk := banList.Get(index).(string); listEntyOk { + log.Tracef("GetBanned: Parsing ban entry: %s", listEnty) re := regexp.MustCompile(`^(\d{1,3}(?:\.\d{1,3}){3}) \t(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \+ (\d+) = (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})$`) matches := re.FindStringSubmatch(listEnty) if matches == nil { + log.Errorf("GetBanned: Failed to parse ban entry: %s", listEnty) return nil, errors.New("could not parse banned IPs entry") } @@ -261,59 +287,97 @@ func (f2bc *Fail2BanClient) GetBanned(jailName string) (*JailEntry, error) { } } + log.Debugf("GetBanned: Successfully retrieved %d banned IPs for jail '%s'", len(bannedEntries), jailName) return &JailEntry{Name: jailName, BannedEntries: bannedEntries}, nil } func (f2bc *Fail2BanClient) sendCommand(command []string) (interface{}, error) { + log.Tracef("Sending command to fail2ban: %v", command) err := f2bc.write(command) if err != nil { + log.Errorf("Failed to write command %v: %v", command, err) return nil, err } - return f2bc.read() + + result, err := f2bc.read() + if err != nil { + log.Errorf("Failed to read response for command %v: %v", command, err) + return nil, err + } + + log.Tracef("Command %v completed successfully", command) + return result, nil } func (f2bc *Fail2BanClient) write(command []string) error { f2bc.mutex.Lock() defer f2bc.mutex.Unlock() + + log.Tracef("Writing command to socket: %v", command) err := f2bc.encoder.Encode(command) if err != nil { + log.Errorf("Failed to encode command %v: %v", command, err) return err } + + log.Tracef("Writing command terminator: %s", commandTerminator) _, err = f2bc.socket.Write([]byte(commandTerminator)) if err != nil { + log.Errorf("Failed to write command terminator: %v", err) return err } + + log.Tracef("Command written successfully: %v", command) return nil } func (f2bc *Fail2BanClient) read() (interface{}, error) { f2bc.mutex.RLock() defer f2bc.mutex.RUnlock() + + log.Trace("Starting to read response from socket") reader := bufio.NewReader(f2bc.socket) data := []byte{} + readIterations := 0 for { buf := make([]byte, socketReadBufferSize) - _, err := reader.Read(buf) + n, err := reader.Read(buf) if err != nil { + log.Errorf("Error reading from socket after %d bytes: %v", len(data), err) return nil, err } - data = append(data, buf...) + readIterations++ + data = append(data, buf[:n]...) + log.Tracef("Read iteration %d: received %d bytes, total %d bytes", readIterations, n, len(data)) + containsTerminator := bytes.Contains(data, []byte(commandTerminator)) if containsTerminator { + log.Tracef("Command terminator found after %d iterations, %d total bytes", readIterations, len(data)) break } } + log.Tracef("Raw response data (first 200 bytes): %q", string(data[:min(200, len(data))])) + bufReader := bytes.NewReader(data) unpickler := pickle.NewUnpickler(bufReader) unpickler.FindClass = func(module, name string) (interface{}, error) { + log.Tracef("Unpickler FindClass called: module=%s, name=%s", module, name) if (module == "builtins" || module == "__builtin__") && name == "str" { return &Py_builtins_str{}, nil } return nil, fmt.Errorf("class not found: [%s] %s", module, name) } - return unpickler.Load() + log.Trace("Unpickling response data") + result, err := unpickler.Load() + if err != nil { + log.Errorf("Failed to unpickle response: %v", err) + return nil, err + } + + log.Tracef("Successfully unpickled response: %T", result) + return result, nil } diff --git a/server/server.go b/server/server.go index 146530e..3f9ca5b 100644 --- a/server/server.go +++ b/server/server.go @@ -67,8 +67,6 @@ type indexData struct { func Serve(version string, fail2banVersion string, store *store.DataStore, geoIP *geoip.GeoIP, configuration *Configuration) error { - log.SetLevel(log.LevelInfo) - templateFunctions := template.FuncMap{ "safe": func(s string) template.URL { safe := template.URL(s)