diff --git a/daemon/procmon/cache.go b/daemon/procmon/cache.go
index f4ff35d5..060fbec1 100644
--- a/daemon/procmon/cache.go
+++ b/daemon/procmon/cache.go
@@ -140,8 +140,8 @@ func (c *CacheProcs) sort(pid int) {
if item != nil && item.Pid == pid {
return
}
- c.RLock()
- defer c.RUnlock()
+ c.Lock()
+ defer c.Unlock()
sort.Slice(c.items, func(i, j int) bool {
t := c.items[i].LastSeen
diff --git a/daemon/tasks/base.go b/daemon/tasks/base.go
index 865fd4c6..caa408bb 100644
--- a/daemon/tasks/base.go
+++ b/daemon/tasks/base.go
@@ -48,7 +48,7 @@ type Task interface {
// and Data is the configuration of each task (a map[string]string, converted by the json package).
type TaskNotification struct {
// Data of the task.
- Data map[string]string
+ Data interface{}
// Name of the task.
Name string
diff --git a/daemon/tasks/socketsmonitor/dump.go b/daemon/tasks/socketsmonitor/dump.go
new file mode 100644
index 00000000..c47b0acc
--- /dev/null
+++ b/daemon/tasks/socketsmonitor/dump.go
@@ -0,0 +1,112 @@
+package socketsmonitor
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "sync"
+
+ "github.com/evilsocket/opensnitch/daemon/log"
+ daemonNetlink "github.com/evilsocket/opensnitch/daemon/netlink"
+ "github.com/evilsocket/opensnitch/daemon/procmon"
+)
+
+const (
+ // AnySocket constant indicates that we should return all sockets found.
+ // If the user selected a socket type, family or protocol, the value will be > 0
+ AnySocket = 0
+)
+
+// Socket represents every socket dumped from the kernel for the given filter.
+type Socket struct {
+ Socket *daemonNetlink.Socket
+ Iface string
+ PID int
+ Mark uint32
+ Proto uint8
+}
+
+// SocketsTable holds all the dumped sockets, after applying the filters, if any.
+type SocketsTable struct {
+ sync.RWMutex `json:"-"`
+ Table []*Socket
+ Processes map[int]*procmon.Process
+}
+
+func (pm *SocketsMonitor) dumpSockets() *SocketsTable {
+ socketList := &SocketsTable{}
+ socketList.Table = make([]*Socket, 0)
+ socketList.Processes = make(map[int]*procmon.Process, 0)
+ for n, opt := range options {
+ if exclude(pm.Config.Family, opt.Fam) {
+ continue
+ }
+ if exclude(pm.Config.Proto, opt.Proto) {
+ continue
+ }
+
+ sockList, err := daemonNetlink.SocketsDump(opt.Fam, opt.Proto)
+ if err != nil {
+ log.Debug("[sockmon][%d] fam: %d, proto: %d, error: %s", n, opt.Fam, opt.Proto, err)
+ continue
+ }
+ if len(sockList) == 0 {
+ log.Debug("[sockmon][%d] fam: %d, proto: %d, no sockets: %d", n, opt.Fam, opt.Proto, opt.Proto)
+ continue
+ }
+
+ var wg sync.WaitGroup
+ for _, sock := range sockList {
+ if sock == nil {
+ continue
+ }
+ if exclude(pm.Config.State, sock.State) {
+ continue
+ }
+ wg.Add(1)
+ // XXX: firing a goroutine per socket may be too much on some scenarios
+ go addSocketToTable(pm.Ctx, &wg, opt.Proto, socketList, *sock)
+ }
+ wg.Wait()
+ }
+
+ return socketList
+}
+
+func exclude(expected, what uint8) bool {
+ return expected > AnySocket && expected != what
+}
+
+func addSocketToTable(ctx context.Context, wg *sync.WaitGroup, proto uint8, st *SocketsTable, s daemonNetlink.Socket) {
+ inode := int(s.INode)
+ pid := procmon.GetPIDFromINode(inode, fmt.Sprint(inode,
+ s.ID.Source, s.ID.SourcePort, s.ID.Destination, s.ID.DestinationPort),
+ )
+ // pid can be -1 in some scenarios (tor socket in FIN_WAIT1 state).
+ // we could lookup the connection in the ebpfCache of connections.
+ st.Lock()
+ var p *procmon.Process
+ if pid == -1 {
+ p = &procmon.Process{}
+ } else {
+ if pp, found := st.Processes[pid]; !found {
+ p = procmon.FindProcess(pid, false)
+ } else {
+ p = pp
+ }
+ }
+ // XXX: should we assume that if the PID is in cache, it has already been sent to the GUI (server)?
+ ss := &Socket{}
+ ss.Socket = &s
+ ss.PID = pid
+ ss.Proto = proto
+ if iface, err := net.InterfaceByIndex(int(s.ID.Interface)); err == nil {
+ ss.Iface = iface.Name
+ }
+
+ st.Table = append(st.Table, ss)
+ st.Processes[pid] = p
+ st.Unlock()
+
+ wg.Done()
+}
diff --git a/daemon/tasks/socketsmonitor/main.go b/daemon/tasks/socketsmonitor/main.go
new file mode 100644
index 00000000..386ee9c8
--- /dev/null
+++ b/daemon/tasks/socketsmonitor/main.go
@@ -0,0 +1,166 @@
+package socketsmonitor
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "sync"
+ "time"
+ "unsafe"
+
+ "github.com/evilsocket/opensnitch/daemon/log"
+ "github.com/evilsocket/opensnitch/daemon/tasks"
+)
+
+// Name of this task
+var Name = "sockets-monitor"
+
+// Config of this task
+// {"interval": "5s", "states": "0,1,2,3", "family": 2, "proto": 17}
+type monConfig struct {
+ Interval string
+ State uint8
+ Proto uint8
+ Family uint8
+}
+
+// SocketsMonitor monitors a process ID.
+type SocketsMonitor struct {
+ tasks.TaskBase
+ mu *sync.RWMutex
+ Ticker *time.Ticker
+
+ Config *monConfig
+ states uint8
+
+ // stop the task if the daemon is disconnected from the GUI (server)
+ StopOnDisconnect bool
+
+ // flag to indicate that the task has been stopped, so any running task should
+ // exit on finish, to avoid sending data to closed channels.
+ isStopped bool
+}
+
+// initConfig parses the received configuration, and initializes it if
+// it's not complete.
+func initConfig(config interface{}) (*monConfig, error) {
+ // https://pkg.go.dev/encoding/json#Unmarshal
+ // JSON objects (are converted) to map[string]interface{}
+ cfg, ok := config.(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("[sockmon] invalid config received: %v", config)
+ }
+ var newCfg monConfig
+ newCfg.Interval = cfg["interval"].(string)
+ newCfg.State = uint8(cfg["state"].(float64))
+ newCfg.Proto = uint8(cfg["proto"].(float64))
+ newCfg.Family = uint8(cfg["family"].(float64))
+
+ if newCfg.Interval == "" {
+ newCfg.Interval = "5s"
+ }
+
+ return &newCfg, nil
+}
+
+// New returns a new SocketsMonitor
+func New(config interface{}, stopOnDisconnect bool) (*SocketsMonitor, error) {
+ cfg, err := initConfig(config)
+ if err != nil {
+ return nil, err
+ }
+ return &SocketsMonitor{
+ TaskBase: tasks.TaskBase{
+ Results: make(chan interface{}),
+ Errors: make(chan error),
+ StopChan: make(chan struct{}),
+ },
+ mu: &sync.RWMutex{},
+ StopOnDisconnect: stopOnDisconnect,
+ Config: cfg,
+ }, nil
+}
+
+// Start ...
+func (pm *SocketsMonitor) Start(ctx context.Context, cancel context.CancelFunc) error {
+ pm.mu.Lock()
+ defer pm.mu.Unlock()
+
+ pm.Ctx = ctx
+ pm.Cancel = cancel
+
+ interval, err := time.ParseDuration(pm.Config.Interval)
+ if err != nil {
+ return err
+ }
+
+ pm.Ticker = time.NewTicker(interval)
+ go func(ctx context.Context) {
+ for {
+ select {
+ case <-pm.TaskBase.StopChan:
+ goto Exit
+ case <-ctx.Done():
+ goto Exit
+ case <-pm.Ticker.C:
+ // FIXME: ensure that dumpSockets() are not overlapped
+ socketList := pm.dumpSockets()
+ sockJSON, err := json.Marshal(socketList)
+ if err != nil {
+ if !pm.isStopped {
+ pm.TaskBase.Errors <- err
+ }
+ goto Exit
+ }
+ if pm.isStopped {
+ goto Exit
+ }
+
+ pm.TaskBase.Results <- unsafe.String(unsafe.SliceData(sockJSON), len(sockJSON))
+ }
+ }
+ Exit:
+ log.Debug("[tasks.SocketsMonitor] stopped")
+ }(ctx)
+ return err
+}
+
+// Pause stops temporarily the task. For example it might be paused when the
+// connection with the GUI (server) is closed.
+func (pm *SocketsMonitor) Pause() error {
+ // TODO
+ return nil
+}
+
+// Resume stopped tasks.
+func (pm *SocketsMonitor) Resume() error {
+ // TODO
+ return nil
+}
+
+// Stop ...
+func (pm *SocketsMonitor) Stop() error {
+ pm.mu.RLock()
+ defer pm.mu.RUnlock()
+
+ if !pm.StopOnDisconnect {
+ return nil
+ }
+ log.Debug("[task.SocketsMonitor] Stop()")
+ pm.isStopped = true
+ pm.Ticker.Stop()
+ pm.Cancel()
+ close(pm.TaskBase.Results)
+ close(pm.TaskBase.Errors)
+ return nil
+}
+
+// Results ...
+func (pm *SocketsMonitor) Results() <-chan interface{} {
+ return pm.TaskBase.Results
+}
+
+// Errors ...
+func (pm *SocketsMonitor) Errors() <-chan error {
+ return pm.TaskBase.Errors
+}
diff --git a/daemon/tasks/socketsmonitor/options.go b/daemon/tasks/socketsmonitor/options.go
new file mode 100644
index 00000000..d683ba3b
--- /dev/null
+++ b/daemon/tasks/socketsmonitor/options.go
@@ -0,0 +1,31 @@
+package socketsmonitor
+
+import (
+ //"golang.org/x/sys/unix"
+ "syscall"
+)
+
+// Protos holds valid combinations of protocols, families and socket types that can be created.
+type Protos struct {
+ Proto uint8
+ Fam uint8
+}
+
+var options = []Protos{
+ {syscall.IPPROTO_DCCP, syscall.AF_INET},
+ {syscall.IPPROTO_DCCP, syscall.AF_INET6},
+ {syscall.IPPROTO_ICMPV6, syscall.AF_INET6},
+ {syscall.IPPROTO_ICMP, syscall.AF_INET},
+ {syscall.IPPROTO_IGMP, syscall.AF_INET},
+ {syscall.IPPROTO_IGMP, syscall.AF_INET6},
+ {syscall.IPPROTO_RAW, syscall.AF_INET},
+ {syscall.IPPROTO_RAW, syscall.AF_INET6},
+ {syscall.IPPROTO_SCTP, syscall.AF_INET},
+ {syscall.IPPROTO_SCTP, syscall.AF_INET6},
+ {syscall.IPPROTO_TCP, syscall.AF_INET},
+ {syscall.IPPROTO_TCP, syscall.AF_INET6},
+ {syscall.IPPROTO_UDP, syscall.AF_INET},
+ {syscall.IPPROTO_UDP, syscall.AF_INET6},
+ {syscall.IPPROTO_UDPLITE, syscall.AF_INET},
+ {syscall.IPPROTO_UDPLITE, syscall.AF_INET6},
+}
diff --git a/daemon/ui/notifications.go b/daemon/ui/notifications.go
index c3b9e3e7..13af0f8a 100644
--- a/daemon/ui/notifications.go
+++ b/daemon/ui/notifications.go
@@ -17,6 +17,7 @@ import (
"github.com/evilsocket/opensnitch/daemon/tasks"
"github.com/evilsocket/opensnitch/daemon/tasks/nodemonitor"
"github.com/evilsocket/opensnitch/daemon/tasks/pidmonitor"
+ "github.com/evilsocket/opensnitch/daemon/tasks/socketsmonitor"
"github.com/evilsocket/opensnitch/daemon/ui/config"
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
"golang.org/x/net/context"
@@ -59,6 +60,37 @@ func (c *Client) getClientConfig() *protocol.ClientConfig {
}
}
+func (c *Client) monitorSockets(config interface{}, stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
+ sockMonTask, err := socketsmonitor.New(config, true)
+ if err != nil {
+ c.sendNotificationReply(stream, notification.Id, "", err)
+ return
+ }
+ ctxSock, err := TaskMgr.AddTask(socketsmonitor.Name, sockMonTask)
+ if err != nil {
+ c.sendNotificationReply(stream, notification.Id, "", err)
+ return
+ }
+ go func(ctx context.Context) {
+ for {
+ select {
+ case <-ctx.Done():
+ goto Exit
+ case err := <-sockMonTask.Errors():
+ c.sendNotificationReply(stream, notification.Id, "", err)
+ case temp := <-sockMonTask.Results():
+ data, ok := temp.(string)
+ if !ok {
+ goto Exit
+ }
+ c.sendNotificationReply(stream, notification.Id, data, nil)
+ }
+ }
+ Exit:
+ // task should have already been removed via TASK_STOP
+ }(ctxSock)
+}
+
func (c *Client) monitorNode(node, interval string, stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
taskName, nodeMonTask := nodemonitor.New(node, interval, true)
ctxNode, err := TaskMgr.AddTask(taskName, nodeMonTask)
@@ -206,15 +238,28 @@ func (c *Client) handleActionTaskStart(stream protocol.UI_NotificationsClient, n
}
switch taskConf.Name {
case pidmonitor.Name:
- pid, err := strconv.Atoi(taskConf.Data["pid"])
+ conf, ok := taskConf.Data.(map[string]interface{})
+ if !ok {
+ log.Error("[pidmon] TaskStart.Data, PID err (string expected): %v", taskConf)
+ return
+ }
+ pid, err := strconv.Atoi(conf["pid"].(string))
if err != nil {
- log.Error("TaskStart.Data, PID err: %s, %v", err, taskConf)
+ log.Error("[pidmon] TaskStart.Data, PID err: %s, %v", err, taskConf)
c.sendNotificationReply(stream, notification.Id, "", err)
return
}
- c.monitorProcessDetails(pid, taskConf.Data["interval"], stream, notification)
+ interval, _ := conf["interval"].(string)
+ c.monitorProcessDetails(pid, interval, stream, notification)
case nodemonitor.Name:
- c.monitorNode(taskConf.Data["node"], taskConf.Data["interval"], stream, notification)
+ conf, ok := taskConf.Data.(map[string]interface{})
+ if !ok {
+ log.Error("[nodemon] TaskStart.Data, \"node\" err (string expected): %v", taskConf)
+ return
+ }
+ c.monitorNode(conf["node"].(string), conf["interval"].(string), stream, notification)
+ case socketsmonitor.Name:
+ c.monitorSockets(taskConf.Data, stream, notification)
default:
log.Debug("TaskStart, unknown task: %v", taskConf)
//c.sendNotificationReply(stream, notification.Id, "", err)
@@ -231,7 +276,12 @@ func (c *Client) handleActionTaskStop(stream protocol.UI_NotificationsClient, no
}
switch taskConf.Name {
case pidmonitor.Name:
- pid, err := strconv.Atoi(taskConf.Data["pid"])
+ conf, ok := taskConf.Data.(map[string]interface{})
+ if !ok {
+ log.Error("[pidmon] TaskStop.Data, PID err (string expected): %v", taskConf)
+ return
+ }
+ pid, err := strconv.Atoi(conf["pid"].(string))
if err != nil {
log.Error("TaskStop.Data, err: %s, %s, %v+, %q", err, notification.Data, taskConf.Data, taskConf.Data)
c.sendNotificationReply(stream, notification.Id, "", err)
@@ -239,7 +289,14 @@ func (c *Client) handleActionTaskStop(stream protocol.UI_NotificationsClient, no
}
TaskMgr.RemoveTask(fmt.Sprint(taskConf.Name, "-", pid))
case nodemonitor.Name:
- TaskMgr.RemoveTask(fmt.Sprint(nodemonitor.Name, "-", taskConf.Data["node"]))
+ conf, ok := taskConf.Data.(map[string]interface{})
+ if !ok {
+ log.Error("[pidmon] TaskStop.Data, PID err (string expected): %v", taskConf)
+ return
+ }
+ TaskMgr.RemoveTask(fmt.Sprint(nodemonitor.Name, "-", conf["node"].(string)))
+ case socketsmonitor.Name:
+ TaskMgr.RemoveTask(socketsmonitor.Name)
default:
log.Debug("TaskStop, unknown task: %v", taskConf)
//c.sendNotificationReply(stream, notification.Id, "", err)
diff --git a/ui/opensnitch/actions/__init__.py b/ui/opensnitch/actions/__init__.py
index 4d380376..e8827db4 100644
--- a/ui/opensnitch/actions/__init__.py
+++ b/ui/opensnitch/actions/__init__.py
@@ -9,7 +9,8 @@ from opensnitch.utils.xdg import xdg_config_home
from opensnitch.actions.default_configs import (
commonDelegateConfig,
rulesDelegateConfig,
- fwDelegateConfig
+ fwDelegateConfig,
+ netstatDelegateConfig
)
from opensnitch.plugins import PluginsList
@@ -129,6 +130,7 @@ class Actions(QObject):
self._actions_list[commonDelegateConfig[Actions.KEY_NAME]] = self.compile(commonDelegateConfig)
self._actions_list[rulesDelegateConfig[Actions.KEY_NAME]] = self.compile(rulesDelegateConfig)
self._actions_list[fwDelegateConfig[Actions.KEY_NAME]] = self.compile(fwDelegateConfig)
+ self._actions_list[netstatDelegateConfig[Actions.KEY_NAME]] = self.compile(netstatDelegateConfig)
def load(self, action_file):
"""read a json file from disk and create the action."""
diff --git a/ui/opensnitch/actions/default_configs.py b/ui/opensnitch/actions/default_configs.py
index 922a77c8..51b921c2 100644
--- a/ui/opensnitch/actions/default_configs.py
+++ b/ui/opensnitch/actions/default_configs.py
@@ -131,3 +131,51 @@ rulesDelegateConfig = {
}
}
}
+
+netstatDelegateConfig = {
+ "name": "netstatDelegateConfig",
+ "created": "",
+ "updated": "",
+ "actions": {
+ "highlight": {
+ "enabled": True,
+ "cells": [
+ {
+ "text": ["LISTEN"],
+ "cols": [1],
+ "color": "green",
+ "bgcolor": "",
+ "alignment": ["center"]
+ },
+ {
+ "text": ["CLOSE"],
+ "cols": [1],
+ "color": "red",
+ "bgcolor": "",
+ "alignment": ["center"]
+ },
+ {
+ "text": ["Established"],
+ "cols": [1],
+ "color": "blue",
+ "bgcolor": "",
+ "alignment": ["center"]
+ },
+ {
+ "text": [
+ "TCP_SYN_SENT", "TCP_SYN_RECV",
+ "TCP_FIN_WAIT1", "TCP_FIN_WAIT2",
+ "TCP_TIME_WAIT", "TCP_CLOSE_WAIT",
+ "TCP_LAST_ACK", "TCP_CLOSING",
+ "TCP_NEW_SYNC_RECV"
+ ],
+ "cols": [1],
+ "color": "",
+ "bgcolor": "",
+ "alignment": ["center"]
+ }
+ ],
+ "rows": []
+ }
+ }
+}
diff --git a/ui/opensnitch/config.py b/ui/opensnitch/config.py
index a1a09fdf..1ccb6826 100644
--- a/ui/opensnitch/config.py
+++ b/ui/opensnitch/config.py
@@ -137,12 +137,16 @@ class Config:
STATS_RULES_COL_STATE = "statsDialog/rules_columns_state"
STATS_FW_COL_STATE = "statsDialog/firewall_columns_state"
STATS_ALERTS_COL_STATE = "statsDialog/alerts_columns_state"
+ STATS_NETSTAT_COL_STATE = "statsDialog/netstat_columns_state"
STATS_RULES_TREE_EXPANDED_0 = "statsDialog/rules_tree_0_expanded"
STATS_RULES_TREE_EXPANDED_1 = "statsDialog/rules_tree_1_expanded"
STATS_RULES_SPLITTER_POS = "statsDialog/rules_splitter_pos"
STATS_NODES_SPLITTER_POS = "statsDialog/nodes_splitter_pos"
STATS_VIEW_COL_STATE = "statsDialog/view_columns_state"
STATS_VIEW_DETAILS_COL_STATE = "statsDialog/view_details_columns_state"
+ STATS_NETSTAT_FILTER_PROTO = "statsDialog/netstat_proto_filter"
+ STATS_NETSTAT_FILTER_FAMILY = "statsDialog/netstat_family_filter"
+ STATS_NETSTAT_FILTER_STATE = "statsDialog/netstat_state_filter"
QT_AUTO_SCREEN_SCALE_FACTOR = "global/screen_scale_factor_auto"
QT_SCREEN_SCALE_FACTOR = "global/screen_scale_factor"
diff --git a/ui/opensnitch/customwidgets/netstattablemodel.py b/ui/opensnitch/customwidgets/netstattablemodel.py
new file mode 100644
index 00000000..8691da3b
--- /dev/null
+++ b/ui/opensnitch/customwidgets/netstattablemodel.py
@@ -0,0 +1,31 @@
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QStandardItemModel
+from opensnitch.customwidgets.generictableview import GenericTableModel
+from opensnitch.utils import sockets
+
+class NetstatTableModel(GenericTableModel):
+
+ def __init__(self, tableName, headerLabels):
+ super().__init__(tableName, headerLabels)
+
+ self.COL_STATE = 1
+ self.COL_PROTO = 6
+ self.COL_FAMILY = 8
+
+ def data(self, index, role=Qt.DisplayRole):
+ """Paint rows with the data stored in self.items"""
+ if role == Qt.DisplayRole or role == Qt.EditRole:
+ items_count = len(self.items)
+ if index.isValid() and items_count > 0 and index.row() < items_count:
+ try:
+ # FIXME: protocol UDP + state CLOSE == state LISTEN
+ if index.column() == self.COL_STATE:
+ return sockets.State[self.items[index.row()][index.column()]]
+ elif index.column() == self.COL_PROTO:
+ return sockets.Proto[self.items[index.row()][index.column()]]
+ elif index.column() == self.COL_FAMILY:
+ return sockets.Family[self.items[index.row()][index.column()]]
+ return self.items[index.row()][index.column()]
+ except Exception as e:
+ print("[socketsmodel] exception:", e, index.row(), index.column())
+ return QStandardItemModel.data(self, index, role)
diff --git a/ui/opensnitch/database/__init__.py b/ui/opensnitch/database/__init__.py
index c9deee7a..d3b60a7b 100644
--- a/ui/opensnitch/database/__init__.py
+++ b/ui/opensnitch/database/__init__.py
@@ -199,7 +199,48 @@ class Database:
"status int " \
")", self.db)
q.exec_()
-
+ q = QSqlQuery("create table if not exists sockets (" \
+ "id int primary key, " \
+ "last_seen text, " \
+ "node text, " \
+ "src_port text, " \
+ "src_ip text, " \
+ "dst_ip text, " \
+ "dst_port text, " \
+ "proto text, " \
+ "uid text, " \
+ "inode text, " \
+ "iface text, " \
+ "family text, " \
+ "state text, " \
+ "cookies text, " \
+ "rqueue text, " \
+ "wqueue text, " \
+ "expires text, " \
+ "retrans text, " \
+ "timer text, " \
+ "mark text, " \
+ "proc_pid text, " \
+ "proc_comm text, " \
+ "proc_path text, " \
+ "UNIQUE(node, src_port, src_ip, dst_ip, dst_port, proto, family, inode)" \
+ ")", self.db)
+ q.exec_()
+ q = QSqlQuery("create index sck_srcport_index on sockets (src_port)", self.db)
+ q.exec_()
+ q = QSqlQuery("create index sck_dstip_index on sockets (dst_ip)", self.db)
+ q.exec_()
+ q = QSqlQuery("create index sck_srcip_index on sockets (src_ip)", self.db)
+ q.exec_()
+ q = QSqlQuery("create index sck_dsthost_index on sockets (dst_host)", self.db)
+ q.exec_()
+ q = QSqlQuery("create index sck_state_index on sockets (state)", self.db)
+ q.exec_()
+ q = QSqlQuery("create index sck_comm_index on sockets (proc_comm)", self.db)
+ q.exec_()
+ q = QSqlQuery("create index sck_path_index on sockets (proc_path)", self.db)
+ q.exec_()
+ q.exec_()
q = QSqlQuery("create index rules_index on rules (time)", self.db)
q.exec_()
diff --git a/ui/opensnitch/dialogs/firewall.py b/ui/opensnitch/dialogs/firewall.py
index 93a204e3..c8e3b913 100644
--- a/ui/opensnitch/dialogs/firewall.py
+++ b/ui/opensnitch/dialogs/firewall.py
@@ -26,7 +26,7 @@ class FirewallDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
POLICY_ACCEPT = 0
POLICY_DROP = 1
- _notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
+ _notification_callback = QtCore.pyqtSignal(str, ui_pb2.NotificationReply)
def __init__(self, parent=None, appicon=None, node=None):
QtWidgets.QDialog.__init__(self, parent)
@@ -87,8 +87,8 @@ class FirewallDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.cmdNewRule.setIcon(newIcon)
self.cmdHelp.setIcon(helpIcon)
- @QtCore.pyqtSlot(ui_pb2.NotificationReply)
- def _cb_notification_callback(self, reply):
+ @QtCore.pyqtSlot(str, ui_pb2.NotificationReply)
+ def _cb_notification_callback(self, addr, reply):
self.comboInput.setEnabled(True)
if reply.id in self._notifications_sent:
if reply.code == ui_pb2.OK:
diff --git a/ui/opensnitch/dialogs/firewall_rule.py b/ui/opensnitch/dialogs/firewall_rule.py
index 2567ad3d..60418435 100644
--- a/ui/opensnitch/dialogs/firewall_rule.py
+++ b/ui/opensnitch/dialogs/firewall_rule.py
@@ -53,7 +53,7 @@ class FwRuleDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
STATM_COUNTER = 15
STATM_LIMIT = 16
- _notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
+ _notification_callback = QtCore.pyqtSignal(str, ui_pb2.NotificationReply)
def __init__(self, parent=None, appicon=None):
QtWidgets.QDialog.__init__(self, parent)
@@ -389,8 +389,8 @@ The value must be in the format: VALUE/UNITS/TIME, for example:
self.comboDirection.currentIndexChanged.disconnect(self._cb_direction_changed)
self.hide()
- @QtCore.pyqtSlot(ui_pb2.NotificationReply)
- def _cb_notification_callback(self, reply):
+ @QtCore.pyqtSlot(str, ui_pb2.NotificationReply)
+ def _cb_notification_callback(self, addr, reply):
self._enable_buttons()
try:
diff --git a/ui/opensnitch/dialogs/preferences.py b/ui/opensnitch/dialogs/preferences.py
index 7bd1ab94..198fa538 100644
--- a/ui/opensnitch/dialogs/preferences.py
+++ b/ui/opensnitch/dialogs/preferences.py
@@ -21,7 +21,7 @@ DIALOG_UI_PATH = "%s/../res/preferences.ui" % os.path.dirname(sys.modules[__name
class PreferencesDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
LOG_TAG = "[Preferences] "
- _notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
+ _notification_callback = QtCore.pyqtSignal(str, ui_pb2.NotificationReply)
saved = QtCore.pyqtSignal()
TAB_POPUPS = 0
@@ -962,8 +962,8 @@ class PreferencesDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
}
self._themes.change_theme(self, self.comboUITheme.currentText(), extra_opts)
- @QtCore.pyqtSlot(ui_pb2.NotificationReply)
- def _cb_notification_callback(self, reply):
+ @QtCore.pyqtSlot(str, ui_pb2.NotificationReply)
+ def _cb_notification_callback(self, addr, reply):
#print(self.LOG_TAG, "Config notification received: ", reply.id, reply.code)
if reply.id in self._notifications_sent:
if reply.code == ui_pb2.OK:
diff --git a/ui/opensnitch/dialogs/processdetails.py b/ui/opensnitch/dialogs/processdetails.py
index 51e2b502..1167023a 100644
--- a/ui/opensnitch/dialogs/processdetails.py
+++ b/ui/opensnitch/dialogs/processdetails.py
@@ -18,7 +18,7 @@ class ProcessDetailsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0])
LOG_TAG = "[ProcessDetails]: "
- _notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
+ _notification_callback = QtCore.pyqtSignal(str, ui_pb2.NotificationReply)
TAB_STATUS = 0
TAB_DESCRIPTORS = 1
@@ -111,8 +111,8 @@ class ProcessDetailsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0])
except Exception as e:
print("procdialog._configure_plugins() exception:", name, " you may want to enable this plugin -", e)
- @QtCore.pyqtSlot(ui_pb2.NotificationReply)
- def _cb_notification_callback(self, reply):
+ @QtCore.pyqtSlot(str, ui_pb2.NotificationReply)
+ def _cb_notification_callback(self, addr, reply):
if reply.id not in self._notifications_sent:
print("[stats] unknown notification received: ", reply.id)
else:
diff --git a/ui/opensnitch/dialogs/ruleseditor.py b/ui/opensnitch/dialogs/ruleseditor.py
index b437de29..7ccfe64a 100644
--- a/ui/opensnitch/dialogs/ruleseditor.py
+++ b/ui/opensnitch/dialogs/ruleseditor.py
@@ -48,7 +48,7 @@ class RulesEditorDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
PW_USER = 0
PW_UID = 2
- _notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
+ _notification_callback = QtCore.pyqtSignal(str, ui_pb2.NotificationReply)
def __init__(self, parent=None, _rule=None, appicon=None):
super(RulesEditorDialog, self).__init__(parent)
@@ -302,8 +302,8 @@ class RulesEditorDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._rules.updated.emit(0)
- @QtCore.pyqtSlot(ui_pb2.NotificationReply)
- def _cb_notification_callback(self, reply):
+ @QtCore.pyqtSlot(str, ui_pb2.NotificationReply)
+ def _cb_notification_callback(self, addr, reply):
#print(self.LOG_TAG, "Rule notification received: ", reply.id, reply.code)
if reply.id in self._notifications_sent:
if reply.code == ui_pb2.OK:
diff --git a/ui/opensnitch/dialogs/stats.py b/ui/opensnitch/dialogs/stats.py
index 35062f3e..382f19e5 100644
--- a/ui/opensnitch/dialogs/stats.py
+++ b/ui/opensnitch/dialogs/stats.py
@@ -24,6 +24,7 @@ from opensnitch.customwidgets.colorizeddelegate import ColorizedDelegate
from opensnitch.customwidgets.firewalltableview import FirewallTableModel
from opensnitch.customwidgets.generictableview import GenericTableModel
from opensnitch.customwidgets.addresstablemodel import AddressTableModel
+from opensnitch.customwidgets.netstattablemodel import NetstatTableModel
from opensnitch.utils import Message, QuickHelp, AsnDB, Icons
from opensnitch.utils.infowindow import InfoWindow
from opensnitch.utils.xdg import xdg_current_desktop
@@ -40,7 +41,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
_status_changed_trigger = QtCore.pyqtSignal(bool)
_shown_trigger = QtCore.pyqtSignal()
_notification_trigger = QtCore.pyqtSignal(ui_pb2.Notification)
- _notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
+ _notification_callback = QtCore.pyqtSignal(str, ui_pb2.NotificationReply)
SORT_ORDER = ["ASC", "DESC"]
LIMITS = ["LIMIT 50", "LIMIT 100", "LIMIT 200", "LIMIT 300", ""]
@@ -97,8 +98,10 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
TAB_ADDRS = 5
TAB_PORTS = 6
TAB_USERS = 7
- TAB_FIREWALL = 8 # in rules tab
- TAB_ALERTS = 9 # in rules tab
+ TAB_NETSTAT = 8
+ # these "specials" tables must be placed after the "real" tabs
+ TAB_FIREWALL = 9 # in rules tab
+ TAB_ALERTS = 10 # in rules tab
# tree's top level items
RULES_TREE_APPS = 0
@@ -138,6 +141,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
TAB_ADDRS: False,
TAB_PORTS: False,
TAB_USERS: False,
+ TAB_NETSTAT: False,
TAB_FIREWALL: False,
TAB_ALERTS: False
}
@@ -214,40 +218,6 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
"last_order_to": 0,
"tracking_column:": COL_R_NAME
},
- TAB_FIREWALL: {
- "name": "firewall",
- "label": None,
- "cmd": None,
- "cmdCleanStats": None,
- "view": None,
- "filterLine": None,
- "model": None,
- "delegate": "defaultFWDelegateConfig",
- "display_fields": "*",
- "header_labels": [],
- "last_order_by": "2",
- "last_order_to": 0,
- "tracking_column:": COL_TIME
- },
- TAB_ALERTS: {
- "name": "alerts",
- "label": None,
- "cmd": None,
- "cmdCleanStats": None,
- "view": None,
- "filterLine": None,
- "model": None,
- "delegate": "defaultRulesDelegateConfig",
- "display_fields": "time as Time, " \
- "node as Node, " \
- "type as Type, " \
- "substr(what, 0, 128) as What, " \
- "substr(body, 0, 128) as Description ",
- "header_labels": [],
- "last_order_by": "1",
- "last_order_to": 0,
- "tracking_column:": COL_TIME
- },
TAB_HOSTS: {
"name": "hosts",
"label": None,
@@ -322,6 +292,65 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
"last_order_by": "2",
"last_order_to": 1,
"tracking_column:": COL_TIME
+ },
+ TAB_NETSTAT: {
+ "name": "sockets",
+ "label": None,
+ "cmd": None,
+ "cmdCleanStats": None,
+ "view": None,
+ "filterLine": None,
+ "model": None,
+ "delegate": "netstatDelegateConfig",
+ "display_fields": "proc_comm as Comm," \
+ "state as State, " \
+ "src_port as SrcPort, " \
+ "src_ip as SrcIP, " \
+ "dst_ip as DstIP, " \
+ "dst_port as DstPort, " \
+ "proto as Protocol, " \
+ "uid as UID, " \
+ "family as Family, " \
+ "iface as IFace, " \
+ "'pid:' || proc_pid || ', inode: ' || inode || ', cookies: '|| cookies || ', rqueue: ' || rqueue || ', wqueue: ' || wqueue || ', expires: ' || expires || ', retrans: ' || retrans || ', timer: ' || timer as Metadata ",
+ "header_labels": [],
+ "last_order_by": "2",
+ "last_order_to": 1,
+ "tracking_column:": COL_TIME
+ },
+ TAB_FIREWALL: {
+ "name": "firewall",
+ "label": None,
+ "cmd": None,
+ "cmdCleanStats": None,
+ "view": None,
+ "filterLine": None,
+ "model": None,
+ "delegate": "defaultFWDelegateConfig",
+ "display_fields": "*",
+ "header_labels": [],
+ "last_order_by": "2",
+ "last_order_to": 0,
+ "tracking_column:": COL_TIME
+ },
+ TAB_ALERTS: {
+ "name": "alerts",
+ "label": None,
+ "cmd": None,
+ "cmdCleanStats": None,
+ "view": None,
+ "filterLine": None,
+ "model": None,
+ "delegate": "defaultRulesDelegateConfig",
+ "display_fields": "time as Time, " \
+ "node as Node, " \
+ "type as Type, " \
+ "substr(what, 0, 128) as What, " \
+ "substr(body, 0, 128) as Description ",
+ "header_labels": [],
+ "last_order_by": "1",
+ "last_order_to": 0,
+ "tracking_column:": COL_TIME
}
}
@@ -381,6 +410,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._fw = Firewall().instance()
self._rules = Rules.instance()
self._fw.rules.rulesUpdated.connect(self._cb_fw_rules_updated)
+ self._nodes.nodesUpdated.connect(self._cb_nodes_updated)
self._rules.updated.connect(self._cb_app_rules_updated)
self._actions = Actions().instance()
self._action_list = self._actions.getByType(PluginBase.TYPE_MAIN_DIALOG)
@@ -452,6 +482,44 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.nextButton.clicked.connect(self._cb_next_button_clicked)
self.prevButton.clicked.connect(self._cb_prev_button_clicked)
+
+ # TODO: move to utils/
+ self.comboNetstatProto.clear()
+ self.comboNetstatProto.addItem(QC.translate("stats", "ALL"), 0)
+ self.comboNetstatProto.addItem("TCP", 6)
+ self.comboNetstatProto.addItem("UDP", 17)
+ self.comboNetstatProto.addItem("SCTP", 132)
+ self.comboNetstatProto.addItem("DCCP", 33)
+ self.comboNetstatProto.addItem("ICMP", 1)
+ self.comboNetstatProto.addItem("ICMPv6", 58)
+ self.comboNetstatProto.addItem("IGMP", 2)
+
+ # These are sockets states. Conntrack uses a different enum.
+ self.comboNetstatStates.clear()
+ self.comboNetstatStates.addItem(QC.translate("stats", "ALL"), 0)
+ self.comboNetstatStates.addItem("Established", 1)
+ self.comboNetstatStates.addItem("TCP_SYN_SENT", 2)
+ self.comboNetstatStates.addItem("TCP_SYN_RECV", 3)
+ self.comboNetstatStates.addItem("TCP_FIN_WAIT1", 4)
+ self.comboNetstatStates.addItem("TCP_FIN_WAIT2", 5)
+ self.comboNetstatStates.addItem("TCP_TIME_WAIT", 6)
+ self.comboNetstatStates.addItem("CLOSE", 7)
+ self.comboNetstatStates.addItem("TCP_CLOSE_WAIT", 8)
+ self.comboNetstatStates.addItem("TCP_LAST_ACK", 9)
+ self.comboNetstatStates.addItem("LISTEN", 10)
+ self.comboNetstatStates.addItem("TCP_CLOSING", 11)
+ self.comboNetstatStates.addItem("TCP_NEW_SYN_RECV", 12)
+
+ self.comboNetstatFamily.clear()
+ self.comboNetstatFamily.addItem(QC.translate("stats", "ALL"), 0)
+ self.comboNetstatFamily.addItem("AF_INET", 2)
+ self.comboNetstatFamily.addItem("AF_INET6", 10)
+ self.comboNetstatInterval.currentIndexChanged.connect(lambda index: self._cb_combo_netstat_changed(0, index))
+ self.comboNetstatNodes.activated.connect(lambda index: self._cb_combo_netstat_changed(1, index))
+ self.comboNetstatProto.currentIndexChanged.connect(lambda index: self._cb_combo_netstat_changed(2, index))
+ self.comboNetstatFamily.currentIndexChanged.connect(lambda index: self._cb_combo_netstat_changed(3, index))
+ self.comboNetstatStates.currentIndexChanged.connect(lambda index: self._cb_combo_netstat_changed(4, index))
+
self.enableRuleCheck.setVisible(False)
self.delRuleButton.setVisible(False)
self.editRuleButton.setVisible(False)
@@ -515,6 +583,20 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.TABLES[self.TAB_PORTS]['header_labels'] = stats_headers
self.TABLES[self.TAB_USERS]['header_labels'] = stats_headers
+ self.LAST_NETSTAT_NODE = None
+ self.TABLES[self.TAB_NETSTAT]['header_labels'] = [
+ "Comm",
+ QC.translate("stats", "State", "This is a word, without spaces and symbols.").replace(" ", ""),
+ QC.translate("stats", "SrcPort", "This is a word, without spaces and symbols.").replace(" ", ""),
+ QC.translate("stats", "SrcIP", "This is a word, without spaces and symbols.").replace(" ", ""),
+ QC.translate("stats", "DstIP", "This is a word, without spaces and symbols.").replace(" ", ""),
+ QC.translate("stats", "DstPort", "This is a word, without spaces and symbols.").replace(" ", ""),
+ QC.translate("stats", "UID", "This is a word, without spaces and symbols.").replace(" ", ""),
+ QC.translate("stats", "Family", "This is a word, without spaces and symbols.").replace(" ", ""),
+ QC.translate("stats", "Iface", "This is a word, without spaces and symbols.").replace(" ", ""),
+ QC.translate("stats", "Metadata", "This is a word, without spaces and symbols.").replace(" ", "")
+ ]
+
self.TABLES[self.TAB_MAIN]['view'] = self._setup_table(QtWidgets.QTableView, self.eventsTable, "connections",
self.TABLES[self.TAB_MAIN]['display_fields'],
order_by="1",
@@ -607,6 +689,16 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
order_by="2",
limit=self._get_limit()
)
+ self.TABLES[self.TAB_NETSTAT]['view'] = self._setup_table(QtWidgets.QTableView,
+ self.netstatTable, "sockets",
+ self.TABLES[self.TAB_NETSTAT]['display_fields'],
+ model=NetstatTableModel("sockets", self.TABLES[self.TAB_NETSTAT]['header_labels']),
+ verticalScrollBar=self.netstatScrollBar,
+ #resize_cols=(),
+ delegate=self.TABLES[self.TAB_NETSTAT]['delegate'],
+ order_by="2",
+ limit=self._get_limit()
+ )
self.TABLES[self.TAB_NODES]['label'] = self.nodesLabel
self.TABLES[self.TAB_RULES]['label'] = self.ruleLabel
@@ -615,6 +707,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.TABLES[self.TAB_ADDRS]['label'] = self.addrsLabel
self.TABLES[self.TAB_PORTS]['label'] = self.portsLabel
self.TABLES[self.TAB_USERS]['label'] = self.usersLabel
+ self.TABLES[self.TAB_NETSTAT]['label'] = self.netstatLabel
self.TABLES[self.TAB_NODES]['cmd'] = self.cmdNodesBack
self.TABLES[self.TAB_RULES]['cmd'] = self.cmdRulesBack
@@ -623,6 +716,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.TABLES[self.TAB_ADDRS]['cmd'] = self.cmdAddrsBack
self.TABLES[self.TAB_PORTS]['cmd'] = self.cmdPortsBack
self.TABLES[self.TAB_USERS]['cmd'] = self.cmdUsersBack
+ self.TABLES[self.TAB_NETSTAT]['cmd'] = self.cmdNetstatBack
self.TABLES[self.TAB_MAIN]['cmdCleanStats'] = self.cmdCleanSql
self.TABLES[self.TAB_NODES]['cmdCleanStats'] = self.cmdCleanSql
@@ -632,6 +726,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.TABLES[self.TAB_ADDRS]['cmdCleanStats'] = self.cmdCleanSql
self.TABLES[self.TAB_PORTS]['cmdCleanStats'] = self.cmdCleanSql
self.TABLES[self.TAB_USERS]['cmdCleanStats'] = self.cmdCleanSql
+ self.TABLES[self.TAB_NETSTAT]['cmdCleanStats'] = self.cmdCleanSql
# the rules clean button is only for a particular rule, not all.
self.TABLES[self.TAB_MAIN]['cmdCleanStats'].clicked.connect(lambda: self._cb_clean_sql_clicked(self.TAB_MAIN))
@@ -675,7 +770,8 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.TABLES[self.TAB_PROCS]['view'],
self.TABLES[self.TAB_ADDRS]['view'],
self.TABLES[self.TAB_PORTS]['view'],
- self.TABLES[self.TAB_USERS]['view']
+ self.TABLES[self.TAB_USERS]['view'],
+ self.TABLES[self.TAB_NETSTAT]['view']
)
self._file_names = ( \
'events.csv',
@@ -685,7 +781,8 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
'procs.csv',
'addrs.csv',
'ports.csv',
- 'users.csv'
+ 'users.csv',
+ 'netstat.csv'
)
self.iconStart = Icons.new(self, "media-playback-start")
@@ -841,11 +938,14 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
w = self.nodesSplitter.width()
self.nodesSplitter.setSizes([int(w/2), int(w/3)])
+ self._configure_netstat_combos()
+
self._restore_details_view_columns(self.eventsTable.horizontalHeader(), Config.STATS_GENERAL_COL_STATE)
self._restore_details_view_columns(self.nodesTable.horizontalHeader(), Config.STATS_NODES_COL_STATE)
self._restore_details_view_columns(self.rulesTable.horizontalHeader(), Config.STATS_RULES_COL_STATE)
self._restore_details_view_columns(self.fwTable.horizontalHeader(), Config.STATS_FW_COL_STATE)
self._restore_details_view_columns(self.alertsTable.horizontalHeader(), Config.STATS_ALERTS_COL_STATE)
+ self._restore_details_view_columns(self.netstatTable.horizontalHeader(), Config.STATS_NETSTAT_COL_STATE)
rulesTreeNodes_expanded = self._cfg.getBool(Config.STATS_RULES_TREE_EXPANDED_1)
if rulesTreeNodes_expanded != None:
@@ -877,6 +977,8 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._cfg.setSettings(Config.STATS_FW_COL_STATE, fwHeader.saveState())
alertsHeader = self.alertsTable.horizontalHeader()
self._cfg.setSettings(Config.STATS_ALERTS_COL_STATE, alertsHeader.saveState())
+ netstatHeader = self.netstatTable.horizontalHeader()
+ self._cfg.setSettings(Config.STATS_NETSTAT_COL_STATE, netstatHeader.saveState())
rules_tree_apps = self._get_rulesTree_item(self.RULES_TREE_APPS)
if rules_tree_apps != None:
@@ -932,6 +1034,23 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
csv.writer(stream, delimiter=',').writerows(selection)
QtWidgets.qApp.clipboard().setText(stream.getvalue())
+ def _configure_netstat_combos(self):
+ self.comboNetstatStates.blockSignals(True);
+ self.comboNetstatStates.setCurrentIndex(
+ self._cfg.getInt(Config.STATS_NETSTAT_FILTER_STATE, 0)
+ )
+ self.comboNetstatStates.blockSignals(False);
+ self.comboNetstatFamily.blockSignals(True);
+ self.comboNetstatFamily.setCurrentIndex(
+ self._cfg.getInt(Config.STATS_NETSTAT_FILTER_FAMILY, 0)
+ )
+ self.comboNetstatFamily.blockSignals(False);
+ self.comboNetstatProto.blockSignals(True);
+ self.comboNetstatProto.setCurrentIndex(
+ self._cfg.getInt(Config.STATS_NETSTAT_FILTER_PROTO, 0)
+ )
+ self.comboNetstatProto.blockSignals(False);
+
def _configure_events_contextual_menu(self, pos):
try:
cur_idx = self.tabWidget.currentIndex()
@@ -1469,6 +1588,21 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
def _cb_app_rules_updated(self, what):
self._refresh_active_table()
+ def _cb_nodes_updated(self, count):
+ prevNode = self.comboNetstatNodes.currentIndex()
+ self.comboNetstatNodes.blockSignals(True);
+ self.comboNetstatNodes.clear()
+ for node in self._nodes.get_nodes():
+ self.comboNetstatNodes.addItem(node)
+
+ if prevNode == -1:
+ prevNode = 0
+ self.comboNetstatNodes.setCurrentIndex(prevNode)
+ if count == 0:
+ self.netstatLabel.setText("")
+ self.comboNetstatInterval.setCurrentIndex(0)
+ self.comboNetstatNodes.blockSignals(False);
+
@QtCore.pyqtSlot(str)
def _cb_fw_table_rows_reordered(self, node_addr):
node = self._nodes.get_node(node_addr)
@@ -1497,23 +1631,33 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._proc_details_dialog.monitor(pids)
- @QtCore.pyqtSlot(ui_pb2.NotificationReply)
- def _cb_notification_callback(self, reply):
+ @QtCore.pyqtSlot(str, ui_pb2.NotificationReply)
+ def _cb_notification_callback(self, node_addr, reply):
if reply.id in self._notifications_sent:
noti = self._notifications_sent[reply.id]
if noti.type == ui_pb2.TASK_START and reply.code != ui_pb2.ERROR:
- self._update_node_info(reply.data)
+ noti_data = json.loads(noti.data)
+ if noti_data['name'] == "node-monitor":
+ self._update_node_info(reply.data)
+ elif noti_data['name'] == "sockets-monitor":
+ self._update_netstat_table(node_addr, reply.data)
+ else:
+ print("_cb_notification_callback, unknown task reply?", noti_data)
return
-
- if reply.code == ui_pb2.ERROR:
+ elif noti.type == ui_pb2.TASK_START and reply.code == ui_pb2.ERROR:
+ self.netstatLabel.setText("error starting netstat table: {0}".format(reply.data))
+ elif reply.code == ui_pb2.ERROR:
Message.ok(
QC.translate("stats", "Error:"),
"{0}".format(reply.data),
QtWidgets.QMessageBox.Warning)
+ else:
+ print("_cb_notification_callback, unknown reply:", reply)
del self._notifications_sent[reply.id]
else:
+ print("_cb_notification_callback, reply not in the list:", reply)
Message.ok(
QC.translate("stats", "Warning:"),
"{0}".format(reply.data),
@@ -1522,12 +1666,19 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
def _cb_tab_changed(self, index):
self.comboAction.setVisible(index == self.TAB_MAIN)
+ if index != self.TAB_NETSTAT and self.LAST_TAB == self.TAB_NETSTAT:
+ self._unmonitor_node_netstat(self.LAST_SELECTED_ITEM)
+ self.comboNetstatNodes.setCurrentIndex(0)
+
if self.LAST_TAB == self.TAB_NODES and self.LAST_SELECTED_ITEM != "":
self._unmonitor_deselected_node(self.LAST_SELECTED_ITEM)
self.TABLES[index]['cmdCleanStats'].setVisible(True)
if index == self.TAB_MAIN:
self._set_events_query()
+ elif index == self.TAB_NETSTAT:
+ self.IN_DETAIL_VIEW[index] = True
+ self._monitor_node_netstat()
else:
if index == self.TAB_RULES:
# display the clean buton only if not in detail view
@@ -1617,6 +1768,21 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
if qstr != None:
self.setQuery(model, qstr)
+ def _cb_combo_netstat_changed(self, combo, idx):
+ refreshIndex = self.comboNetstatInterval.currentIndex()
+ self._unmonitor_node_netstat(self.LAST_NETSTAT_NODE)
+ if refreshIndex > 0:
+ self._monitor_node_netstat()
+
+ if combo == 2:
+ self._cfg.setSettings(Config.STATS_NETSTAT_FILTER_PROTO, self.comboNetstatProto.currentIndex())
+ elif combo == 3:
+ self._cfg.setSettings(Config.STATS_NETSTAT_FILTER_FAMILY, self.comboNetstatFamily.currentIndex())
+ elif combo == 4:
+ self._cfg.setSettings(Config.STATS_NETSTAT_FILTER_STATE, self.comboNetstatStates.currentIndex())
+
+ self.LAST_NETSTAT_NODE = self.comboNetstatNodes.currentText()
+
def _cb_limit_combo_changed(self, idx):
if self.tabWidget.currentIndex() == self.TAB_MAIN:
self._set_events_query()
@@ -2484,6 +2650,142 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
if nid != None:
self._notifications_sent[nid] = noti
self.labelNodeDetails.setText("")
+ print("taskStop, prev node:", last_addr, "nid:", nid)
+
+ # XXX: would be useful to leave latest data?
+ #self._reset_node_info()
+
+ def _monitor_node_netstat(self):
+ # TODO:
+ # - create a tasks package, to centralize/normalize tasks' names and
+ # config
+ self.netstatLabel.show()
+
+ node_addr = self.comboNetstatNodes.currentText()
+ if node_addr == "":
+ print("monitor_netstat_node: no nodes")
+ self.netstatLabel.setText("")
+ return
+ if not self._nodes.is_connected(node_addr):
+ print("monitor_node_netstat, node not connected:", node_addr)
+ self.netstatLabel.setText("{0} node is not connected".format(node_addr))
+ return
+
+ refreshIndex = self.comboNetstatInterval.currentIndex()
+ if refreshIndex == 0:
+ self._unmonitor_node_netstat(node_addr)
+ return
+
+ refreshInterval = self.comboNetstatInterval.currentText()
+ proto = self.comboNetstatProto.currentIndex()
+ family = self.comboNetstatFamily.currentIndex()
+ state = self.comboNetstatStates.currentIndex()
+ config = '{"name": "sockets-monitor", "data": {"interval": "%s", "state": %d, "proto": %d, "family": %d}}' % (
+ refreshInterval,
+ int(self.comboNetstatStates.itemData(state)),
+ int(self.comboNetstatProto.itemData(proto)),
+ int(self.comboNetstatFamily.itemData(family))
+ )
+
+ self.netstatLabel.setText(QC.translate("stats", "loading in {0}...".format(refreshInterval)))
+
+ noti = ui_pb2.Notification(
+ clientName="",
+ serverName="",
+ type=ui_pb2.TASK_START,
+ data=config,
+ rules=[])
+ nid = self._nodes.send_notification(
+ node_addr, noti, self._notification_callback
+ )
+ if nid != None:
+ self._notifications_sent[nid] = noti
+
+ self.LAST_SELECTED_ITEM = node_addr
+
+ def _unmonitor_node_netstat(self, node_addr):
+ self.netstatLabel.hide()
+ self.netstatLabel.setText("")
+ if node_addr == "":
+ print("unmonitor_netstat_node: no nodes")
+ return
+
+ if not self._nodes.is_connected(node_addr):
+ print("unmonitor_node_netstat, node not connected:", node_addr)
+ else:
+ noti = ui_pb2.Notification(
+ clientName="",
+ serverName="",
+ type=ui_pb2.TASK_STOP,
+ data='{"name": "sockets-monitor", "data": {}}',
+ rules=[])
+ nid = self._nodes.send_notification(
+ node_addr, noti, self._notification_callback
+ )
+ if nid != None:
+ self._notifications_sent[nid] = noti
+
+ def _update_netstat_table(self, node_addr, data):
+ netstat = json.loads(data)
+ fields = []
+ values = []
+ cols = "(last_seen, node, src_port, src_ip, dst_ip, dst_port, proto, uid, inode, iface, family, state, cookies, rqueue, wqueue, expires, retrans, timer, proc_path, proc_comm, proc_pid)"
+ try:
+ now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ # TODO: make this optional
+ self._db.clean(self.TABLES[self.TAB_NETSTAT]['name'])
+ self._db.transaction()
+ for k in netstat['Table']:
+ if k == None:
+ continue
+ sck = k['Socket']
+ iface = k['Socket']['ID']['Interface']
+ if k['Iface'] != "":
+ iface = k['Iface']
+ proc_comm = ""
+ proc_path = ""
+ proc_pid = ""
+ if k['PID'] != -1 and str(k['PID']) in netstat['Processes'].keys():
+ proc_pid = str(k['PID'])
+ proc_path = netstat['Processes'][proc_pid]['Path']
+ proc_comm = netstat['Processes'][proc_pid]['Comm']
+ self._db.insert(
+ self.TABLES[self.TAB_NETSTAT]['name'],
+ cols,
+ (
+ now,
+ node_addr,
+ k['Socket']['ID']['SourcePort'],
+ k['Socket']['ID']['Source'],
+ k['Socket']['ID']['Destination'],
+ k['Socket']['ID']['DestinationPort'],
+ k['Proto'],
+ k['Socket']['UID'],
+ k['Socket']['INode'],
+ iface,
+ k['Socket']['Family'],
+ k['Socket']['State'],
+ str(k['Socket']['ID']['Cookie']),
+ k['Socket']['RQueue'],
+ k['Socket']['WQueue'],
+ k['Socket']['Expires'],
+ k['Socket']['Retrans'],
+ k['Socket']['Timer'],
+ proc_path,
+ proc_comm,
+ proc_pid
+ )
+ )
+ self._db.commit()
+ self.netstatLabel.setText(QC.translate("stats", "refreshing..."))
+ self._refresh_active_table()
+ except Exception as e:
+ print("_update_netstat_table exception:", e)
+ print(data)
+ self.netstatLabel.setText("error loading netstat table")
+ self.netstatLabel.setText(QC.translate("stats", "error loading: {0}".format(repr(e))))
+
+# create plugins and actions before dialogs
def _update_node_info(self, data):
try:
@@ -3068,7 +3370,20 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
values.append(table.model().index(row, col).data())
w.writerow(values)
- def _setup_table(self, widget, tableWidget, table_name, fields="*", group_by="", order_by="2", sort_direction=SORT_ORDER[1], limit="", resize_cols=(), model=None, delegate=None, verticalScrollBar=None, tracking_column=COL_TIME):
+ def _setup_table(self,
+ widget,
+ tableWidget,
+ table_name,
+ fields="*",
+ group_by="",
+ order_by="2",
+ sort_direction=SORT_ORDER[1],
+ limit="",
+ resize_cols=(),
+ model=None,
+ delegate=None,
+ verticalScrollBar=None,
+ tracking_column=COL_TIME):
tableWidget.setSortingEnabled(True)
if model == None:
model = self._db.get_new_qsql_model()
diff --git a/ui/opensnitch/nodes.py b/ui/opensnitch/nodes.py
index b7ee7dcd..94007261 100644
--- a/ui/opensnitch/nodes.py
+++ b/ui/opensnitch/nodes.py
@@ -276,6 +276,7 @@ class Nodes(QObject):
# FIXME: the reply is sent before we return the notification id
if callback_signal != None:
callback_signal.emit(
+ addr,
ui_pb2.NotificationReply(
id=notification.id,
code=ui_pb2.ERROR,
@@ -294,6 +295,7 @@ class Nodes(QObject):
print(self.LOG_TAG + " exception sending notification: ", e, addr, notification)
if callback_signal != None:
callback_signal.emit(
+ addr,
ui_pb2.NotificationReply(
id=notification.id,
code=ui_pb2.ERROR,
@@ -336,7 +338,7 @@ class Nodes(QObject):
return
if self._notifications_sent[reply.id]['callback'] != None:
- self._notifications_sent[reply.id]['callback'].emit(reply)
+ self._notifications_sent[reply.id]['callback'].emit(addr, reply)
# delete only one-time notifications
# we need the ID of streaming notifications from the server
diff --git a/ui/opensnitch/res/stats.ui b/ui/opensnitch/res/stats.ui
index 53c5747b..d1d917a9 100644
--- a/ui/opensnitch/res/stats.ui
+++ b/ui/opensnitch/res/stats.ui
@@ -560,7 +560,7 @@
0
0
- 336
+ 314
404
@@ -1518,6 +1518,313 @@
+
+
+
+
+
+ Netstat
+
+
+ -
+
+
+ 0
+
+
-
+
+
+ QAbstractItemView::SelectRows
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+
+
+ -
+
+
-
+
+
+
+
+
+
+ ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup
+
+
+
+ -
+
+
-
+
+ Stop
+
+
+
+
+
+ -
+
+ 5s
+
+
+ -
+
+ 10s
+
+
+ -
+
+ 15s
+
+
+ -
+
+ 20s
+
+
+ -
+
+ 30s
+
+
+ -
+
+ 45s
+
+
+ -
+
+ 1m
+
+
+ -
+
+ 5m
+
+
+ -
+
+ 10m
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
-
+
+ All nodes
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Protocol
+
+
+
+ -
+
+
-
+
+ ALL
+
+
+ -
+
+ TCP
+
+
+ -
+
+ UDP
+
+
+ -
+
+ SCTP
+
+
+ -
+
+ DCCP
+
+
+ -
+
+ ICMP
+
+
+ -
+
+ RAW
+
+
+
+
+ -
+
+
+ Family
+
+
+
+ -
+
+
-
+
+ ALL
+
+
+ -
+
+ AF_INET
+
+
+ -
+
+ AF_INET6
+
+
+ -
+
+ AF_PACKET
+
+
+
+
+ -
+
+
+ State
+
+
+
+ -
+
+
-
+
+ ALL
+
+
+ -
+
+ Established
+
+
+ -
+
+ TCP_SYN_SENT
+
+
+ -
+
+ TCP_SYN_RECV
+
+
+ -
+
+ TCP_FIN_WAIT1
+
+
+ -
+
+ TCP_FIN_WAIT2
+
+
+ -
+
+ TCP_TIME_WAIT
+
+
+ -
+
+ TCP_CLOSE
+
+
+ -
+
+ TCP_CLOSE_WAIT
+
+
+ -
+
+ TCP_LAST_ACK
+
+
+ -
+
+ TCP_LISTEN
+
+
+ -
+
+ TCP_CLOSING
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
-
diff --git a/ui/opensnitch/service.py b/ui/opensnitch/service.py
index d6ab486e..0aee9116 100644
--- a/ui/opensnitch/service.py
+++ b/ui/opensnitch/service.py
@@ -37,7 +37,7 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
_add_alert_trigger = QtCore.pyqtSignal(str, str, ui_pb2.Alert)
_version_warning_trigger = QtCore.pyqtSignal(str, str)
_status_change_trigger = QtCore.pyqtSignal(bool)
- _notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
+ _notification_callback = QtCore.pyqtSignal(str, ui_pb2.NotificationReply)
_show_message_trigger = QtCore.pyqtSignal(str, str, int, int)
# .desktop filename located under /usr/share/applications/
@@ -401,8 +401,8 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
else:
self._tray.setIcon(self.off_icon)
- @QtCore.pyqtSlot(ui_pb2.NotificationReply)
- def _on_notification_reply(self, reply):
+ @QtCore.pyqtSlot(str, ui_pb2.NotificationReply)
+ def _on_notification_reply(self, addr, reply):
if reply.code == ui_pb2.ERROR:
self._tray.showMessage("Error",
reply.data,
diff --git a/ui/opensnitch/utils/sockets.py b/ui/opensnitch/utils/sockets.py
new file mode 100644
index 00000000..73d1fa89
--- /dev/null
+++ b/ui/opensnitch/utils/sockets.py
@@ -0,0 +1,43 @@
+
+
+# https://pkg.go.dev/syscall#pkg-constants
+Family = {
+ '0': 'AF_UNSPEC',
+ '2': 'AF_INET',
+ '10': 'AF_INET6',
+ '11': 'AF_PACKET',
+ '40': 'AF_VSOCK',
+ '44': 'AF_XDP',
+ '45': 'AF_MCTP',
+}
+
+Proto = {
+ '0': 'IP',
+ '1': 'ICMP',
+ '2': 'IGMP',
+ '3': 'ETH_P_ALL',
+ '6': 'TCP',
+ '17': 'UDP',
+ '33': 'DCCP',
+ '41': 'IPv6',
+ '58': 'ICMPv6',
+ '132': 'SCTP',
+ '136': 'UDPLITE',
+ '255': 'RAW'
+}
+
+State = {
+ '1': 'Established',
+ '2': 'TCP_SYN_SENT',
+ '3': 'TCP_SYN_RECV',
+ '4': 'TCP_FIN_WAIT1',
+ '5': 'TCP_FIN_WAIT2',
+ '6': 'TCP_TIME_WAIT',
+ '7': 'CLOSE',
+ '8': 'TCP_CLOSE_WAIT',
+ '9': 'TCP_LAST_ACK',
+ '10': 'LISTEN',
+ '11': 'TCP_CLOSING',
+ '12': 'TCP_NEW_SYNC_RECV',
+ '13': 'TCP_MAX_STATES'
+}