improved rules reloading, cli parameters

- When reloading rules from a path:
   stop existing (domains,ips,regexp) lists monitors, stop rules
   watcher and start watching the new dir for changes, delete existing
   rules from memory, etc.
 - Previously, cli parameters (queue number, log file, etc) were taking
   into account before loading the configuration.
   Now the configuration file is loaded first (default-config.json), and
   if any of the cli parameter has been specified, it'll overwrite the
   loaded configuration from file.

   This means for example that if you use "-process-monitor-method proc",
   and "ebpf" is configured in default-config.json, firstly "ebpf" will
   be configured, and later "proc".

   (-queue-num option for now requires to match config option
   cfg.FwOptions.QueueNumber)
This commit is contained in:
Gustavo Iñiguez Goia 2024-05-22 00:47:54 +02:00
parent 661e3da48f
commit c0d1da20d2
Failed to generate hash of commit
5 changed files with 109 additions and 60 deletions

View file

@ -62,9 +62,9 @@ var (
logFile = ""
logUTC = true
logMicro = false
rulesPath = "/etc/opensnitchd/rules/"
rulesPath = ""
configFile = "/etc/opensnitchd/default-config.json"
fwConfigFile = "/etc/opensnitchd/system-fw.json"
fwConfigFile = ""
ebpfModPath = "" // /usr/lib/opensnitchd/ebpf
noLiveReload = false
queueNum = 0
@ -103,7 +103,7 @@ func init() {
flag.BoolVar(&showVersion, "version", debug, "Show daemon version of this executable and exit.")
flag.BoolVar(&checkRequirements, "check-requirements", debug, "Check system requirements for incompatibilities.")
flag.StringVar(&procmonMethod, "process-monitor-method", procmonMethod, "How to search for processes path. Options: ftrace, audit (experimental), ebpf (experimental), proc (default)")
flag.StringVar(&procmonMethod, "process-monitor-method", procmonMethod, "Options: audit, ebpf, proc (default)")
flag.StringVar(&uiSocket, "ui-socket", uiSocket, "Path the UI gRPC service listener (https://github.com/grpc/grpc/blob/master/doc/naming.md).")
flag.IntVar(&queueNum, "queue-num", queueNum, "Netfilter queue number.")
flag.IntVar(&workers, "workers", workers, "Number of concurrent workers.")
@ -151,6 +151,20 @@ func overwriteLogging() bool {
return debug || warning || important || errorlog || logFile != "" || logMicro
}
// overwriteFw reloads the fw with the configuration file specified via cli.
func overwriteFw(cfg *config.Config, qNum uint16, fwCfg string) {
firewall.Reload(
cfg.Firewall,
fwCfg,
cfg.FwOptions.MonitorInterval,
qNum,
)
// TODO: Close() closes the daemon if closing the queue timeouts
//queue.Close()
//repeatQueue.Close()
//setupQueues(qNum)
}
func setupQueues(qNum uint16) {
// prepare the queue
var err error
@ -557,6 +571,7 @@ func main() {
setupLogging()
setupProfiling()
setupSignals()
log.Important("Starting %s v%s", core.Name, core.Version)
@ -565,55 +580,55 @@ func main() {
log.Fatal("%s", err)
}
if err == nil && cfg.Rules.Path != "" {
rulesPath = cfg.Rules.Path
}
if rulesPath == "" {
log.Fatal("rules path cannot be empty")
}
rulesPath, err := core.ExpandPath(rulesPath)
if err != nil {
log.Fatal("Error accessing rules path (does it exist?): %s", err)
}
if cfg.FwOptions.ConfigPath == "" {
cfg.FwOptions.ConfigPath = fwConfigFile
}
log.Info("Using system fw configuration %s ...", fwConfigFile)
if uint16(queueNum) != cfg.FwOptions.QueueNum && queueNum > 0 {
cfg.FwOptions.QueueNum = uint16(queueNum)
}
setupSignals()
log.Info("Loading rules from %s ...", rulesPath)
log.Info("Loading rules from %s ...", cfg.Rules.Path)
rules, err = rule.NewLoader(!noLiveReload)
if err != nil {
log.Fatal("%s", err)
} else if err = rules.Load(rulesPath); err != nil {
log.Fatal("%s", err)
}
stats = statistics.New(rules)
loggerMgr = loggers.NewLoggerManager()
stats.SetLoggers(loggerMgr)
uiClient = ui.NewClient(uiSocket, configFile, stats, rules, loggerMgr)
// default expected queue from the cli is 0. If it's greater than 0
// overwrite config value (which by default is also 0)
qNum := cfg.FwOptions.QueueNum
if uint16(queueNum) != cfg.FwOptions.QueueNum && queueNum > 0 {
qNum = uint16(queueNum)
}
log.Info("Using queue number %d ...", qNum)
setupWorkers()
setupQueues(cfg.FwOptions.QueueNum)
setupQueues(qNum)
// queue and firewall rules should be ready by now
uiClient.Connect()
listenToEvents()
// overwrite configuration options with the ones specified from the cli
if overwriteLogging() {
setupLogging()
}
if fwConfigFile != "" {
log.Info("Reloading fw rules from %s, queue %d ...", fwConfigFile, qNum)
overwriteFw(cfg, qNum, fwConfigFile)
}
log.Info("Using system fw configuration %s ...", fwConfigFile)
if rulesPath != "" {
log.Info("Reloading rules from %s ...", rulesPath)
if err := rules.Reload(rulesPath); err != nil {
log.Fatal("Error loading rules path %s", rulesPath)
}
}
// overwrite monitor method from configuration if the user has passed
// the option via command line.
if procmonMethod != "" {
if procmonMethod != "" || (ebpfModPath != "" && ebpfModPath != cfg.Ebpf.ModulesPath) {
log.Info("Reloading proc monitor (%s) (ebpf mods path: %s)...", procmonMethod, cfg.Ebpf.ModulesPath)
if err := monitor.ReconfigureMonitorMethod(procmonMethod, cfg.Ebpf); err != nil {
msg := fmt.Sprintf("Unable to set process monitor method via parameter: %v", err)
uiClient.SendWarningAlert(msg)

View file

@ -25,11 +25,12 @@ import (
type Loader struct {
watcher *fsnotify.Watcher
rules map[string]*Rule
path string
rulesKeys []string
Path string
liveReload bool
liveReloadRunning bool
checkSums bool
stopLiveReload chan struct{}
sync.RWMutex
}
@ -42,11 +43,12 @@ func NewLoader(liveReload bool) (*Loader, error) {
return nil, err
}
return &Loader{
path: "",
Path: "",
rules: make(map[string]*Rule),
liveReload: liveReload,
watcher: watcher,
liveReloadRunning: false,
stopLiveReload: make(chan struct{}),
}, nil
}
@ -86,13 +88,35 @@ func (l *Loader) HasChecksums(op Operand) {
// Reload loads rules from the specified path, deleting existing loaded
// rules from memory.
func (l *Loader) Reload(path string) error {
log.Info("rules.Loader.Reload(): %s", path)
// check that the new path exists before reloading
if core.Exists(path) == false {
return fmt.Errorf("The new path '%s' does not exist", path)
}
// stop monitors
if l.liveReloadRunning {
l.stopLiveReload <- struct{}{}
}
if l.watcher != nil {
l.watcher.Remove(l.Path)
}
for _, r := range l.rules {
l.cleanListsRule(r)
}
// then delete the rules, and reload everything
l.Lock()
l.rulesKeys = make([]string, 0)
l.rules = make(map[string]*Rule)
l.Unlock()
return l.Load(path)
}
// Load loads rules files from disk.
func (l *Loader) Load(path string) error {
log.Debug("rules.Loader.Load(): %s", path)
if core.Exists(path) == false {
return fmt.Errorf("Path '%s' does not exist\nCreate it if you want to save rules to disk", path)
}
@ -107,7 +131,7 @@ func (l *Loader) Load(path string) error {
return fmt.Errorf("Error globbing '%s': %s", expr, err)
}
l.path = path
l.Path = path
if len(l.rules) == 0 {
l.rules = make(map[string]*Rule)
}
@ -132,7 +156,7 @@ func (l *Loader) Load(path string) error {
func (l *Loader) Add(rule *Rule, saveToDisk bool) error {
l.addUserRule(rule)
if saveToDisk {
fileName := filepath.Join(l.path, fmt.Sprintf("%s.json", rule.Name))
fileName := filepath.Join(l.Path, fmt.Sprintf("%s.json", rule.Name))
return l.Save(rule, fileName)
}
return nil
@ -147,7 +171,7 @@ func (l *Loader) Replace(rule *Rule, saveToDisk bool) error {
l.Lock()
defer l.Unlock()
fileName := filepath.Join(l.path, fmt.Sprintf("%s.json", rule.Name))
fileName := filepath.Join(l.Path, fmt.Sprintf("%s.json", rule.Name))
return l.Save(rule, fileName)
}
return nil
@ -263,7 +287,7 @@ func (l *Loader) deleteRule(filePath string) {
}
func (l *Loader) deleteRuleFromDisk(ruleName string) error {
path := fmt.Sprint(l.path, "/", ruleName, ".json")
path := fmt.Sprint(l.Path, "/", ruleName, ".json")
return os.Remove(path)
}
@ -277,7 +301,8 @@ func (l *Loader) deleteOldRuleFromDisk(oldRule, newRule *Rule) {
}
}
// cleanListsRule erases the list of domains of an Operator of type Lists
// cleanListsRule erases the lists loaded of an Operator of type Lists,
// and stops the workers monitoring the lists.
func (l *Loader) cleanListsRule(oldRule *Rule) {
if oldRule.Operator.Type == Lists {
oldRule.Operator.StopMonitoringLists()
@ -412,8 +437,8 @@ func (l *Loader) scheduleTemporaryRule(rule Rule) error {
func (l *Loader) liveReloadWorker() {
l.liveReloadRunning = true
log.Debug("Rules watcher started on path %s ...", l.path)
if err := l.watcher.Add(l.path); err != nil {
log.Debug("Rules watcher started on path %s ...", l.Path)
if err := l.watcher.Add(l.Path); err != nil {
log.Error("Could not watch path: %s", err)
l.liveReloadRunning = false
return
@ -421,6 +446,8 @@ func (l *Loader) liveReloadWorker() {
for {
select {
case <-l.stopLiveReload:
goto Exit
case event := <-l.watcher.Events:
// a new rule json file has been created or updated
if event.Op&fsnotify.Write == fsnotify.Write {
@ -442,6 +469,9 @@ func (l *Loader) liveReloadWorker() {
log.Error("File system watcher error: %s", err)
}
}
Exit:
log.Debug("[rules] liveReloadWorker() exited")
l.liveReloadRunning = false
}
// FindFirstMatch will try match the connection against the existing rule set.

View file

@ -80,7 +80,7 @@ func (o *Operator) monitorLists() {
Exit:
modTimes = nil
o.ClearLists()
log.Info("lists monitor stopped")
log.Info("lists monitor stopped: %s", o.Data)
}
// ClearLists deletes all the entries of a list

View file

@ -76,6 +76,7 @@ func NewClient(socketPath, localConfigFile string, stats *statistics.Statistics,
isConnected: make(chan bool),
alertsChan: make(chan protocol.Alert, maxQueuedAlerts),
}
c.config.Rules.Path = rules.Path
//for i := 0; i < 4; i++ {
go c.alertsDispatcher()

View file

@ -157,32 +157,16 @@ func (c *Client) reloadConfiguration(reload bool, newConfig config.Config) *moni
log.Debug("[config] config.internal.gcpercent not changed")
}
// 1. load rules
c.rules.EnableChecksums(newConfig.Rules.EnableChecksums)
if c.config.Rules.Path != newConfig.Rules.Path {
c.rules.Reload(newConfig.Rules.Path)
log.Debug("[config] reloading config.rules.path: %s", newConfig.Rules.Path)
log.Debug("[config] reloading config.rules.path, old: <%s> new: <%s>", c.config.Rules.Path, newConfig.Rules.Path)
} else {
log.Debug("[config] config.rules.path not changed")
}
reloadFw := false
if c.GetFirewallType() != newConfig.Firewall ||
newConfig.FwOptions.ConfigPath != c.config.FwOptions.ConfigPath ||
newConfig.FwOptions.QueueNum != c.config.FwOptions.QueueNum ||
newConfig.FwOptions.MonitorInterval != c.config.FwOptions.MonitorInterval {
log.Debug("[config] reloading config.firewall")
reloadFw = true
firewall.Reload(
newConfig.Firewall,
newConfig.FwOptions.ConfigPath,
newConfig.FwOptions.MonitorInterval,
newConfig.FwOptions.QueueNum,
)
} else {
log.Debug("[config] config.firewall not changed")
}
// 2. load proc mon method
reloadProc := false
if c.config.ProcMonitorMethod == "" ||
newConfig.ProcMonitorMethod != c.config.ProcMonitorMethod {
@ -208,6 +192,25 @@ func (c *Client) reloadConfiguration(reload bool, newConfig config.Config) *moni
log.Debug("[config] config.procmon not changed")
}
// 3. load fw
reloadFw := false
if c.GetFirewallType() != newConfig.Firewall ||
newConfig.FwOptions.ConfigPath != c.config.FwOptions.ConfigPath ||
newConfig.FwOptions.QueueNum != c.config.FwOptions.QueueNum ||
newConfig.FwOptions.MonitorInterval != c.config.FwOptions.MonitorInterval {
log.Debug("[config] reloading config.firewall")
reloadFw = true
firewall.Reload(
newConfig.Firewall,
newConfig.FwOptions.ConfigPath,
newConfig.FwOptions.MonitorInterval,
newConfig.FwOptions.QueueNum,
)
} else {
log.Debug("[config] config.firewall not changed")
}
if (reloadProc || reloadFw) && newConfig.Internal.FlushConnsOnStart {
log.Debug("[config] flushing established connections")
netlink.FlushConnections()