Skip to content

Commit 66ee50b

Browse files
tasks: allow to load tasks from disk
Now it's possible to load tasks from disk. There's a central file which contains the defined tasks: /etc/opensnitchd/tasks/tasks.json with this format: { "tasks": [ { "enabled": false, "name": "downloader", "configfile": "/etc/opensnitchd/tasks/downloader/downloader.json", } ] } The field "name" defines the type of task to load, defined in each task. It's static. Whenever the tasks.json is modified, if the tasks are enabled, the task configuration file defined in the "configfile" field will be loaded. Configuration format of each task: { "name": "downloader", "parent": "downloader", "data": {} } The field "name" is dynamic, unique, and identifies the instance of the task. If there're two tasks configured with the same name, only one will be loaded. The "parent" field is used to know what's the base task. If it's not specified, we'll try to use the "name" as the base task. This allows you to run multiple instances of the same task with different configurations: a downloader to update malware blocklists, another to update IP blocklists, etc. The "data" field depends of each task. It's generic, and each task is responsible for defining it. For example for the Downloader task: "data": { "interval": "1h", "timeout": "5s", "urls": [ { "name": "adaway", "enabled": true, "remote": "https://adaway.o/hosts.txt", "localfile": "/tmp/blocklist/ads-adaway-hosts.txt" } ] } The tasks configuration file are also monitored for changes, and whenever a change is detected the task is reloaded. ---- Notifications (WIP) It's not yet fully defined and it'll change, but this is the state as of today. For real-time tasks, like pid-monitor, sockets monitor and node monitor, the GUI initiates the task with a unique ID (a timestamp), to know who is sending what, and display the data properly on the GUI. For permanent tasks we use a unique ID defined for each task. Right now, if we don't have the notifications channel opened and the notification ID corresponds with one of the tasks, we'll post an alert to the GUI, to display a desktop notification. Each task can define if send notifications or not. For example: { "name": "downloader", "data": { (...) "notify": { "enabled": true } } } then in each task, act accordingly to what is defined in the configuration.
1 parent 4b38ca1 commit 66ee50b

File tree

20 files changed

+669
-94
lines changed

20 files changed

+669
-94
lines changed

daemon/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ all: opensnitchd
66

77
install:
88
@mkdir -p $(DESTDIR)/etc/opensnitchd/rules
9+
@mkdir -p $(DESTDIR)/etc/opensnitchd/tasks
910
@install -Dm755 opensnitchd \
1011
-t $(DESTDIR)$(PREFIX)/bin/
1112
@install -Dm644 opensnitchd.service \
@@ -17,6 +18,7 @@ install:
1718
@install -Dm644 network_aliases.json \
1819
-t $(DESTDIR)/etc/opensnitchd/
1920
@install -Dm600 data/rules/* $(DESTDIR)/etc/opensnitchd/rules/
21+
@install -Dm600 data/tasks/tasks.json $(DESTDIR)/etc/opensnitchd/tasks/
2022
@systemctl daemon-reload
2123

2224
opensnitchd: $(SRC)

daemon/data/tasks/tasks.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"tasks": []
3+
}
Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
1-
package tasks
1+
package base
22

33
import (
44
"context"
55
)
66

7+
const (
8+
PID_MON = 9000
9+
NODE_MON = 9001
10+
SOCKETS_MON = 9002
11+
DOWNLOADER = 9003
12+
NETSNIFFER = 9004
13+
IOCS_SCANNER = 9005
14+
REDFLAGS = 9006
15+
)
16+
717
// TaskBase holds the common fields of every task.
818
// Warning: don't define fields in tasks with these names.
919
type TaskBase struct {
1020
Ctx context.Context
1121
Cancel context.CancelFunc
22+
Name string
1223
Results chan interface{}
1324
Errors chan error
1425

26+
// ID that identifies this task
27+
// Temporary tasks like PIDMonitor have a NotificationID which is used
28+
// to receive and display the data from the task on the GUI.
29+
// Permanent tasks like a background downloader won't have this ID,
30+
// so this ID will serve as initial identification to know who is sending what,
31+
// and treat data apropiately, if needed (sometimes it'll just be a desktop notification).
32+
ID uint64
33+
1534
// Stop the task if the daemon is disconnected from the GUI (server).
1635
// Some tasks don't need to run if the daemon is not connected to the GUI,
1736
// like PIDMonitor, SocketsMonitor,etc.
@@ -20,6 +39,23 @@ type TaskBase struct {
2039
StopOnDisconnect bool
2140
}
2241

42+
func (t *TaskBase) SetID(id uint64) {
43+
t.ID = id
44+
}
45+
46+
func (t *TaskBase) GetID() uint64 {
47+
return t.ID
48+
}
49+
50+
func (t *TaskBase) IsTemporary() bool {
51+
return t.StopOnDisconnect
52+
}
53+
54+
type TaskResults struct {
55+
Type int
56+
Data interface{}
57+
}
58+
2359
// Task defines the interface for tasks that the task manager will execute.
2460
type Task interface {
2561
// Start starts the task, potentially running it asynchronously.
@@ -28,6 +64,11 @@ type Task interface {
2864
// Stop stops the task.
2965
Stop() error
3066

67+
//GetName() string
68+
SetID(uint64)
69+
GetID() uint64
70+
IsTemporary() bool
71+
3172
Pause() error
3273
Resume() error
3374

daemon/tasks/config/main.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package config
2+
3+
// Copyright 2025 The OpenSnitch Authors. All rights reserved.
4+
// Use of this source code is governed by the GPLv3
5+
// license that can be found in the LICENSE file.
6+
7+
import (
8+
"encoding/json"
9+
"fmt"
10+
"io/ioutil"
11+
"sync"
12+
13+
"github.com/evilsocket/opensnitch/daemon/log"
14+
"github.com/fsnotify/fsnotify"
15+
)
16+
17+
var (
18+
DefaultCfgFile = "/etc/opensnitchd/tasks/tasks.json"
19+
)
20+
21+
// TaskData represents the configuration of a task.
22+
// For example:
23+
// {
24+
// "name": "sockets-monitor",
25+
// "data": {"protocol": 1, "state": "all", array: [...], ...}
26+
// }
27+
// The data field must be a JSON object.
28+
// Each task can unmarshal the object to its own JSON object.
29+
type TaskData struct {
30+
// Parent holds the name of the parent task
31+
Parent string
32+
33+
// Name holds the name of this particular task.
34+
// It must be unique if you want to run multiple instances of the same task.
35+
Name string
36+
Data map[string]interface{}
37+
}
38+
39+
// TaskConfig holds the information of each task. The name, the configuration
40+
// file and if its enabled or not.
41+
// The name of the task must be the one defined in each task: task.Name
42+
type TaskConfig struct {
43+
Name string
44+
ConfigFile string
45+
Enabled bool
46+
}
47+
48+
// TaskList holds the list of existing tasks.
49+
//
50+
// {
51+
// "list": [
52+
// {
53+
// "name": "node-monitor",
54+
// "enabled": true,
55+
// "file": "/etc/opensnitchd/tasks/node-monitor/node-monitor.json",
56+
// },
57+
// ...
58+
// ]
59+
// }
60+
//
61+
type TasksList struct {
62+
Tasks []TaskConfig
63+
}
64+
65+
type Loader struct {
66+
watcher *fsnotify.Watcher
67+
Tasks []TaskConfig
68+
CfgFile string
69+
stopLiveReload chan struct{}
70+
TaskChanged chan string
71+
liveReloadRunning bool
72+
73+
sync.RWMutex
74+
}
75+
76+
// NewTasksLoader returns a new configuration loader object.
77+
// It'll monitor the configuration files for changes.
78+
func NewTasksLoader() (*Loader, error) {
79+
watcher, err := fsnotify.NewWatcher()
80+
if err != nil {
81+
return nil, err
82+
}
83+
return &Loader{
84+
liveReloadRunning: false,
85+
watcher: watcher,
86+
stopLiveReload: make(chan struct{}),
87+
TaskChanged: make(chan string),
88+
}, nil
89+
}
90+
91+
func (l *Loader) Load(path string) ([]TaskConfig, error) {
92+
if path == "" {
93+
path = DefaultCfgFile
94+
}
95+
log.Debug("[tasks] Loader.Load() config file: %s", path)
96+
97+
raw, err := ioutil.ReadFile(path)
98+
if err != nil || len(raw) == 0 {
99+
return nil, fmt.Errorf("error reading tasks list file %s: %s", path, err)
100+
}
101+
var tasks TasksList
102+
err = json.Unmarshal(raw, &tasks)
103+
if err != nil {
104+
return nil, fmt.Errorf("Error unmarshalling config file %s: %s", path, err)
105+
}
106+
l.Tasks = tasks.Tasks
107+
l.CfgFile = path
108+
109+
if !l.isLiveReloadRunning() {
110+
go l.liveReloadWorker()
111+
}
112+
113+
return l.Tasks, nil
114+
}

daemon/tasks/config/monitor.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package config
2+
3+
import (
4+
//"path"
5+
"strings"
6+
"time"
7+
8+
"github.com/evilsocket/opensnitch/daemon/log"
9+
"github.com/fsnotify/fsnotify"
10+
)
11+
12+
func (l *Loader) AddWatch(path string) error {
13+
l.RLock()
14+
defer l.RUnlock()
15+
return l.watcher.Add(path)
16+
}
17+
18+
func (l *Loader) RemoveWatch(path string) error {
19+
l.RLock()
20+
defer l.RUnlock()
21+
return l.watcher.Remove(path)
22+
}
23+
24+
func (l *Loader) AddWatches() {
25+
if err := l.watcher.Add(l.CfgFile); err != nil {
26+
log.Error("[tasks] Could not watch path %s: %s", l.CfgFile, err)
27+
}
28+
29+
for _, task := range l.Tasks {
30+
if task.ConfigFile == "" {
31+
log.Warning("[tasks] Loader watch, \"configfile\" field missing, skipping task %s: enabled: %v, %s", task.Name, task.Enabled, task.ConfigFile)
32+
continue
33+
}
34+
if !task.Enabled {
35+
continue
36+
}
37+
log.Debug("[tasks] Loader watching %s: %v, %s", task.Name, task.Enabled, task.ConfigFile)
38+
39+
if err := l.AddWatch(task.ConfigFile); err != nil {
40+
log.Error("[tasks] Loader, could not watch path %s: %s", task.ConfigFile, err)
41+
}
42+
}
43+
}
44+
45+
func (l *Loader) setLiveReloadRunning(running bool) {
46+
l.Lock()
47+
l.liveReloadRunning = running
48+
l.Unlock()
49+
}
50+
51+
func (l *Loader) isLiveReloadRunning() bool {
52+
l.RLock()
53+
defer l.RUnlock()
54+
return l.liveReloadRunning
55+
}
56+
57+
func (l *Loader) liveReloadWorker() {
58+
l.setLiveReloadRunning(true)
59+
defer l.setLiveReloadRunning(false)
60+
61+
//log.Info("Tasks watcher started on path %v ...", l.Tasks)
62+
63+
for {
64+
l.AddWatches()
65+
66+
select {
67+
case <-l.stopLiveReload:
68+
goto Exit
69+
case event, ok := <-l.watcher.Events:
70+
if !ok {
71+
log.Error("[tasks] Loader.watcher events not ready, closed?")
72+
}
73+
if !strings.HasSuffix(event.Name, ".json") {
74+
continue
75+
}
76+
log.Trace("[tasks] watcher event. Write: %v, Create: %v, Removed: %v Renamed: %v, %+v, %s",
77+
event.Op&fsnotify.Write == fsnotify.Write,
78+
event.Op&fsnotify.Create == fsnotify.Create,
79+
event.Op&fsnotify.Remove == fsnotify.Remove,
80+
event.Op&fsnotify.Rename == fsnotify.Rename,
81+
event, event.Name)
82+
83+
// a new rule json file has been created or updated
84+
if event.Op&fsnotify.Create == fsnotify.Create {
85+
log.Info("New task: %s", event.Name)
86+
87+
} else if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Rename == fsnotify.Rename {
88+
log.Important("Tasks file changed %s, reloading ...", event.Name)
89+
// the events may occur too rapidly, and sometimes the file does not exist yet.
90+
time.Sleep(1 * time.Second)
91+
l.TaskChanged <- event.Name
92+
}
93+
94+
case err := <-l.watcher.Errors:
95+
log.Warning("[tasks] watcher error: %s", err)
96+
}
97+
}
98+
Exit:
99+
log.Info("[tasks] liveReloadWorker() exited")
100+
}

daemon/tasks/config/utils.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package config
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
8+
"github.com/evilsocket/opensnitch/daemon/log"
9+
)
10+
11+
func LoadTaskData(path string) (TaskData, error) {
12+
raw, err := ioutil.ReadFile(path)
13+
if err != nil {
14+
return TaskData{}, fmt.Errorf("error opening task file %s: %s", path, err)
15+
}
16+
log.Trace("LoadTaskData: %s -> %s", path, string(raw))
17+
18+
var taskConf TaskData
19+
err = json.Unmarshal(raw, &taskConf)
20+
if err != nil {
21+
return TaskData{}, fmt.Errorf("error unmarshalling task file %s: %s", path, err)
22+
}
23+
24+
return taskConf, nil
25+
}

daemon/tasks/doc.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package tasks
22

3-
// Copyright 2024 The OpenSnitch Authors. All rights reserved.
3+
// Copyright 2025 The OpenSnitch Authors. All rights reserved.
44
// Use of this source code is governed by the GPLv3
55
// license that can be found in the LICENSE file.
66

77
/*
88
Package tasks manages actions launched by/to the daemon.
99
1010
These tasks are handled by the TaskManager, which is in charge of start new
11-
task, update and stop them.
11+
tasks, update and stop them.
1212
1313
The name of each task serves as the unique key inside the task manager.
1414
Some tasks will be unique, like SocketsMonitor, and others might have more than one instance, like "pid-monitor-123", "pid-monitor-987".
@@ -17,4 +17,8 @@ Tasks run in background.
1717
1818
Some tasks may run periodically (every 5s, every 2 days, ...), others will run until stop and others until a timeout.
1919
20+
Tere're also permanent tasks and temporary tasks:
21+
- temporary tasks only last while the UI is running, for example the node-monitor, netstat monitor or pid-monitor.
22+
- peramnent tasks run periodically regardless if the UI is running or not, like a cron job (url downloader, background scanner, etc).
23+
2024
*/

0 commit comments

Comments
 (0)