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' +}