2023-12-15 16:35:41 +01:00
|
|
|
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
|
|
|
//
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
2023-12-14 12:09:01 +01:00
|
|
|
package logranger
|
|
|
|
|
|
|
|
import (
|
2023-12-22 15:24:58 +01:00
|
|
|
"errors"
|
2023-12-14 12:09:01 +01:00
|
|
|
"fmt"
|
2023-12-22 15:24:58 +01:00
|
|
|
"io"
|
2023-12-14 12:09:01 +01:00
|
|
|
"log/slog"
|
|
|
|
"net"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
2023-12-22 15:24:58 +01:00
|
|
|
"time"
|
2023-12-22 01:44:50 +01:00
|
|
|
|
|
|
|
"github.com/wneessen/go-parsesyslog"
|
|
|
|
_ "github.com/wneessen/go-parsesyslog/rfc3164"
|
|
|
|
_ "github.com/wneessen/go-parsesyslog/rfc5424"
|
2023-12-23 20:29:38 +01:00
|
|
|
|
|
|
|
"github.com/wneessen/logranger/plugins/actions"
|
|
|
|
_ "github.com/wneessen/logranger/plugins/actions/all"
|
2023-12-14 12:09:01 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// LogErrKey is the keyword used in slog for error messages
|
|
|
|
LogErrKey = "error"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Server is the main server struct
|
|
|
|
type Server struct {
|
|
|
|
// conf is a pointer to the config.Config
|
|
|
|
conf *Config
|
|
|
|
// listener is a listener that satisfies the net.Listener interface
|
|
|
|
listener net.Listener
|
|
|
|
// log is a pointer to the slog.Logger
|
|
|
|
log *slog.Logger
|
2023-12-22 15:24:58 +01:00
|
|
|
// parser is a parsesyslog.Parser
|
|
|
|
parser parsesyslog.Parser
|
2023-12-22 17:31:41 +01:00
|
|
|
// ruleset is a pointer to the ruleset
|
|
|
|
ruleset *Ruleset
|
2023-12-14 12:09:01 +01:00
|
|
|
// wg is a sync.WaitGroup
|
|
|
|
wg sync.WaitGroup
|
|
|
|
}
|
|
|
|
|
2023-12-19 20:17:56 +01:00
|
|
|
// New creates a new instance of Server based on the provided Config
|
2023-12-23 20:29:38 +01:00
|
|
|
func New(c *Config) (*Server, error) {
|
2023-12-14 12:09:01 +01:00
|
|
|
s := &Server{
|
|
|
|
conf: c,
|
|
|
|
}
|
2023-12-23 20:29:38 +01:00
|
|
|
|
2023-12-14 12:09:01 +01:00
|
|
|
s.setLogLevel()
|
|
|
|
|
2023-12-23 20:29:38 +01:00
|
|
|
if err := s.setRules(); err != nil {
|
|
|
|
return s, err
|
2023-12-14 12:09:01 +01:00
|
|
|
}
|
2023-12-22 17:31:41 +01:00
|
|
|
|
2023-12-22 15:24:58 +01:00
|
|
|
p, err := parsesyslog.New(s.conf.internal.ParserType)
|
|
|
|
if err != nil {
|
2023-12-23 20:29:38 +01:00
|
|
|
return s, fmt.Errorf("failed to initialize syslog parser: %w", err)
|
2023-12-22 15:24:58 +01:00
|
|
|
}
|
|
|
|
s.parser = p
|
2023-12-22 17:31:41 +01:00
|
|
|
|
2023-12-23 20:29:38 +01:00
|
|
|
if len(actions.Actions) <= 0 {
|
|
|
|
return s, fmt.Errorf("no action plugins found/configured")
|
2023-12-22 17:31:41 +01:00
|
|
|
}
|
|
|
|
|
2023-12-23 20:29:38 +01:00
|
|
|
return s, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run starts the logranger Server by creating a new listener using the NewListener
|
|
|
|
// method and calling RunWithListener with the obtained listener.
|
|
|
|
func (s *Server) Run() error {
|
|
|
|
l, err := NewListener(s.conf)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-12-14 12:09:01 +01:00
|
|
|
return s.RunWithListener(l)
|
|
|
|
}
|
|
|
|
|
2023-12-19 20:17:56 +01:00
|
|
|
// RunWithListener sets the listener for the server and performs some additional
|
|
|
|
// tasks for initializing the server. It creates a PID file, writes the process ID
|
|
|
|
// to the file, and listens for connections. It returns an error if any of the
|
|
|
|
// initialization steps fail.
|
2023-12-14 12:09:01 +01:00
|
|
|
func (s *Server) RunWithListener(l net.Listener) error {
|
|
|
|
s.listener = l
|
|
|
|
|
|
|
|
// Create PID file
|
|
|
|
pf, err := os.Create(s.conf.Server.PIDFile)
|
|
|
|
if err != nil {
|
|
|
|
s.log.Error("failed to create PID file", LogErrKey, err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
2023-12-22 01:44:50 +01:00
|
|
|
pid := os.Getpid()
|
|
|
|
s.log.Debug("creating PID file", slog.String("pid_file", pf.Name()),
|
|
|
|
slog.Int("pid", pid))
|
|
|
|
_, err = pf.WriteString(fmt.Sprintf("%d", pid))
|
2023-12-14 12:09:01 +01:00
|
|
|
if err != nil {
|
|
|
|
s.log.Error("failed to write PID to PID file", LogErrKey, err)
|
|
|
|
_ = pf.Close()
|
|
|
|
}
|
|
|
|
if err = pf.Close(); err != nil {
|
|
|
|
s.log.Error("failed to close PID file", LogErrKey, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Listen for connections
|
|
|
|
s.wg.Add(1)
|
2023-12-22 01:44:50 +01:00
|
|
|
go s.Listen()
|
2023-12-14 12:09:01 +01:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-12-22 15:24:58 +01:00
|
|
|
// Listen handles incoming connections and processes log messages.
|
2023-12-22 01:44:50 +01:00
|
|
|
func (s *Server) Listen() {
|
|
|
|
defer s.wg.Done()
|
|
|
|
s.log.Info("listening for new connections", slog.String("listen_addr", s.listener.Addr().String()))
|
|
|
|
for {
|
|
|
|
c, err := s.listener.Accept()
|
|
|
|
if err != nil {
|
|
|
|
s.log.Error("failed to accept new connection", LogErrKey, err)
|
|
|
|
continue
|
|
|
|
}
|
2023-12-22 15:24:58 +01:00
|
|
|
s.log.Debug("accepted new connection",
|
|
|
|
slog.String("remote_addr", c.RemoteAddr().String()))
|
2023-12-22 01:44:50 +01:00
|
|
|
conn := NewConnection(c)
|
|
|
|
s.wg.Add(1)
|
|
|
|
go func(co *Connection) {
|
2023-12-22 16:08:16 +01:00
|
|
|
s.HandleConnection(co)
|
2023-12-22 01:44:50 +01:00
|
|
|
s.wg.Done()
|
|
|
|
}(conn)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-22 15:24:58 +01:00
|
|
|
// HandleConnection handles a single connection by parsing and processing log messages.
|
|
|
|
// It logs debug information about the connection and measures the processing time.
|
|
|
|
// It closes the connection when done, and logs any error encountered during the process.
|
2023-12-22 01:44:50 +01:00
|
|
|
func (s *Server) HandleConnection(c *Connection) {
|
2023-12-22 15:24:58 +01:00
|
|
|
defer func() {
|
|
|
|
if err := c.conn.Close(); err != nil {
|
|
|
|
s.log.Error("failed to close connection", LogErrKey, err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2023-12-22 16:08:16 +01:00
|
|
|
ReadLoop:
|
|
|
|
for {
|
|
|
|
if err := c.conn.SetDeadline(time.Now().Add(s.conf.Parser.Timeout)); err != nil {
|
|
|
|
s.log.Error("failed to set processing deadline", LogErrKey, err,
|
|
|
|
slog.Duration("timeout", s.conf.Parser.Timeout))
|
2023-12-22 15:24:58 +01:00
|
|
|
return
|
2023-12-22 16:08:16 +01:00
|
|
|
}
|
|
|
|
lm, err := s.parser.ParseReader(c.rb)
|
|
|
|
if err != nil {
|
|
|
|
var ne *net.OpError
|
|
|
|
switch {
|
|
|
|
case errors.As(err, &ne):
|
|
|
|
if s.conf.Log.Extended {
|
|
|
|
s.log.Error("network error while processing message", LogErrKey,
|
|
|
|
ne.Error())
|
|
|
|
}
|
|
|
|
return
|
|
|
|
case errors.Is(err, io.EOF):
|
|
|
|
if s.conf.Log.Extended {
|
|
|
|
s.log.Error("message could not be processed", LogErrKey,
|
|
|
|
"EOF received")
|
|
|
|
}
|
|
|
|
return
|
|
|
|
default:
|
|
|
|
s.log.Error("failed to parse message", LogErrKey, err,
|
|
|
|
slog.String("parser_type", s.conf.Parser.Type))
|
|
|
|
continue ReadLoop
|
2023-12-22 15:24:58 +01:00
|
|
|
}
|
|
|
|
}
|
2023-12-27 17:07:25 +01:00
|
|
|
s.wg.Add(1)
|
|
|
|
go s.processMessage(lm)
|
2023-12-23 20:29:38 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-25 21:30:44 +01:00
|
|
|
// processMessage processes a log message by matching it against the ruleset and executing
|
|
|
|
// the corresponding actions if a match is found. It takes a parsesyslog.LogMsg as input
|
|
|
|
// and returns an error if there was an error while processing the actions.
|
|
|
|
// The method first checks if the ruleset is not nil. If it is nil, no actions will be
|
|
|
|
// executed. For each rule in the ruleset, it checks if the log message matches the
|
|
|
|
// rule's regular expression.
|
2023-12-27 17:07:25 +01:00
|
|
|
func (s *Server) processMessage(lm parsesyslog.LogMsg) {
|
|
|
|
defer s.wg.Done()
|
2023-12-23 20:29:38 +01:00
|
|
|
if s.ruleset != nil {
|
|
|
|
for _, r := range s.ruleset.Rule {
|
|
|
|
if !r.Regexp.MatchString(lm.Message.String()) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if r.HostMatch != nil && !r.HostMatch.MatchString(lm.Hostname) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
mg := r.Regexp.FindStringSubmatch(lm.Message.String())
|
|
|
|
for n, a := range actions.Actions {
|
2023-12-25 21:30:44 +01:00
|
|
|
bt := time.Now()
|
2023-12-25 21:08:03 +01:00
|
|
|
if err := a.Config(r.Actions); err != nil {
|
|
|
|
s.log.Error("failed to config action", LogErrKey, err,
|
|
|
|
slog.String("action", n), slog.String("rule_id", r.ID))
|
2023-12-25 21:08:54 +01:00
|
|
|
continue
|
2023-12-25 21:08:03 +01:00
|
|
|
}
|
2023-12-25 21:30:44 +01:00
|
|
|
s.log.Debug("log message matches rule, executing action",
|
|
|
|
slog.String("action", n), slog.String("rule_id", r.ID))
|
2023-12-25 21:08:03 +01:00
|
|
|
if err := a.Process(lm, mg); err != nil {
|
2023-12-23 20:29:38 +01:00
|
|
|
s.log.Error("failed to process action", LogErrKey, err,
|
|
|
|
slog.String("action", n), slog.String("rule_id", r.ID))
|
|
|
|
}
|
2023-12-25 21:30:44 +01:00
|
|
|
if s.conf.Log.Extended {
|
|
|
|
pt := time.Since(bt)
|
|
|
|
s.log.Debug("action processing benchmark",
|
|
|
|
slog.Duration("processing_time", pt),
|
|
|
|
slog.String("processing_time_human", pt.String()),
|
|
|
|
slog.String("action", n), slog.String("rule_id", r.ID))
|
|
|
|
}
|
2023-12-23 20:29:38 +01:00
|
|
|
}
|
|
|
|
}
|
2023-12-22 01:44:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-19 20:17:56 +01:00
|
|
|
// setLogLevel sets the log level based on the value of `s.conf.Log.Level`.
|
|
|
|
// It creates a new `slog.HandlerOptions` and assigns the corresponding `slog.Level`
|
|
|
|
// based on the value of `s.conf.Log.Level`. If the value is not one of the valid levels,
|
|
|
|
// `info` is used as the default level.
|
|
|
|
// It then creates a new `slog.JSONHandler` with `os.Stdout` and the handler options.
|
|
|
|
// Finally, it creates a new `slog.Logger` with the JSON handler and sets the `s.log` field
|
|
|
|
// of the `Server` struct to the logger, with a context value of "logranger".
|
2023-12-14 12:09:01 +01:00
|
|
|
func (s *Server) setLogLevel() {
|
|
|
|
lo := slog.HandlerOptions{}
|
|
|
|
switch strings.ToLower(s.conf.Log.Level) {
|
|
|
|
case "debug":
|
|
|
|
lo.Level = slog.LevelDebug
|
|
|
|
case "info":
|
|
|
|
lo.Level = slog.LevelInfo
|
|
|
|
case "warn":
|
|
|
|
lo.Level = slog.LevelWarn
|
|
|
|
case "error":
|
|
|
|
lo.Level = slog.LevelError
|
|
|
|
default:
|
|
|
|
lo.Level = slog.LevelInfo
|
|
|
|
}
|
|
|
|
lh := slog.NewJSONHandler(os.Stdout, &lo)
|
|
|
|
s.log = slog.New(lh).With(slog.String("context", "logranger"))
|
|
|
|
}
|
2023-12-23 20:29:38 +01:00
|
|
|
|
|
|
|
// setRules initializes/updates the ruleset for the logranger Server by
|
|
|
|
// calling NewRuleset with the config and assigns the returned ruleset
|
|
|
|
// to the Server's ruleset field.
|
|
|
|
// It returns an error if there is a failure in reading or loading the ruleset.
|
|
|
|
func (s *Server) setRules() error {
|
|
|
|
rs, err := NewRuleset(s.conf)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to read ruleset: %w", err)
|
|
|
|
}
|
|
|
|
s.ruleset = rs
|
|
|
|
return nil
|
|
|
|
}
|
2023-12-27 19:59:49 +01:00
|
|
|
|
|
|
|
// ReloadConfig reloads the configuration of the Server with the specified
|
|
|
|
// path and filename.
|
|
|
|
// It creates a new Config using the NewConfig method and updates the Server's
|
|
|
|
// conf field. It also reloads the configured Ruleset.
|
|
|
|
// If an error occurs while reloading the configuration, an error is returned.
|
|
|
|
func (s *Server) ReloadConfig(p, f string) error {
|
|
|
|
c, err := NewConfig(p, f)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to reload config: %w", err)
|
|
|
|
}
|
|
|
|
s.conf = c
|
|
|
|
|
|
|
|
if err := s.setRules(); err != nil {
|
|
|
|
return fmt.Errorf("failed to reload ruleset: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|