tasks: added sockets monitor task (netstat)

Added new task to monitor local sockets of nodes, similar to ss or
netstat.

More info: #1112
This commit is contained in:
Gustavo Iñiguez Goia 2024-10-07 23:40:40 +02:00
parent 2fc1977d32
commit 83fad69316
Failed to generate hash of commit
21 changed files with 1235 additions and 76 deletions

View file

@ -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

View file

@ -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

View file

@ -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()
}

View file

@ -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
}

View file

@ -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},
}

View file

@ -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)

View file

@ -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."""

View file

@ -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": []
}
}
}

View file

@ -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"

View file

@ -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)

View file

@ -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_()

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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()

View file

@ -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

View file

@ -560,7 +560,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>336</width>
<width>314</width>
<height>404</height>
</rect>
</property>
@ -1518,6 +1518,313 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_9">
<attribute name="icon">
<iconset theme="network"/>
</attribute>
<attribute name="title">
<string>Netstat</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_7">
<item row="2" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_18">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="GenericTableView" name="netstatTable">
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>false</bool>
</property>
<property name="cornerButtonEnabled">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderStretchLastSection">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="QScrollBar" name="netstatScrollBar">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_20">
<item>
<widget class="QPushButton" name="cmdNetstatBack">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="go-previous">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboNetstatInterval">
<item>
<property name="text">
<string>Stop</string>
</property>
<property name="icon">
<iconset theme="stop"/>
</property>
</item>
<item>
<property name="text">
<string>5s</string>
</property>
</item>
<item>
<property name="text">
<string>10s</string>
</property>
</item>
<item>
<property name="text">
<string>15s</string>
</property>
</item>
<item>
<property name="text">
<string>20s</string>
</property>
</item>
<item>
<property name="text">
<string>30s</string>
</property>
</item>
<item>
<property name="text">
<string>45s</string>
</property>
</item>
<item>
<property name="text">
<string>1m</string>
</property>
</item>
<item>
<property name="text">
<string>5m</string>
</property>
</item>
<item>
<property name="text">
<string>10m</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboNetstatNodes">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>All nodes</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>Protocol</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboNetstatProto">
<item>
<property name="text">
<string>ALL</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TCP</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">UDP</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">SCTP</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">DCCP</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">ICMP</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">RAW</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>Family</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboNetstatFamily">
<item>
<property name="text">
<string>ALL</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">AF_INET</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">AF_INET6</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">AF_PACKET</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>State</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboNetstatStates">
<item>
<property name="text">
<string>ALL</string>
</property>
</item>
<item>
<property name="text">
<string>Established</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TCP_SYN_SENT</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TCP_SYN_RECV</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TCP_FIN_WAIT1</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TCP_FIN_WAIT2</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TCP_TIME_WAIT</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TCP_CLOSE</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TCP_CLOSE_WAIT</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TCP_LAST_ACK</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TCP_LISTEN</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TCP_CLOSING</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="netstatLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="3" column="0">

View file

@ -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,

View file

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