collect and display bytes sent/recv per process

New feature to collect and display bytes sent/received per process.

 - it only works with 'ebpf' monitor method.
 - the information is collected on kernel space and sent to the daemon:
   - when the connection socket is closed.
   - every 2s on large transfers. On this case the bytes are
     accumulated.

The daemon sends the events to the server (GUI), where the information
is added to the DB.

The information is displayed on the GUI:
 - on the statusbar in real-time (based on the refresh interval defined).
 - on the Applications tab.

By right clicking on the Applications tab headers, the user can reset
Tx/Rx stats, and grouping bytes per unit (default) or not.

Finally, the rx/tx stats are deleted based on the preferences options.
This commit is contained in:
Gustavo Iñiguez Goia 2024-04-23 11:59:11 +02:00
parent 2ec37ed593
commit 4cdf709606
Failed to generate hash of commit
22 changed files with 1324 additions and 238 deletions

View file

@ -121,7 +121,7 @@ func (p *Process) GetExtraInfo() error {
// ReadPPID obtains the pid of the parent process
func (p *Process) ReadPPID() {
// ReadFile + parse = ~40us
data, err := ioutil.ReadFile(p.pathStat)
data, err := ioutil.ReadFile(p.procPath[Stat])
if err != nil {
p.PPID = 0
return
@ -143,7 +143,7 @@ func (p *Process) ReadComm() error {
if p.Comm != "" {
return nil
}
data, err := ioutil.ReadFile(p.pathComm)
data, err := ioutil.ReadFile(p.procPath[Comm])
if err != nil {
return err
}
@ -156,7 +156,7 @@ func (p *Process) ReadCwd() error {
if p.CWD != "" {
return nil
}
link, err := os.Readlink(p.pathCwd)
link, err := os.Readlink(p.procPath[Cwd])
if err != nil {
return err
}
@ -166,7 +166,7 @@ func (p *Process) ReadCwd() error {
// ReadEnv reads and parses the environment variables of a process.
func (p *Process) ReadEnv() {
data, err := ioutil.ReadFile(p.pathEnviron)
data, err := ioutil.ReadFile(p.procPath[Environ])
if err != nil {
return
}
@ -200,7 +200,7 @@ func (p *Process) ReadPath() error {
defer func() {
if p.Path == "" {
// determine if this process might be of a kernel task.
if data, err := ioutil.ReadFile(p.pathMaps); err == nil && len(data) == 0 {
if data, err := ioutil.ReadFile(p.procPath[Maps]); err == nil && len(data) == 0 {
p.Path = KernelConnection
p.Args = append(p.Args, p.Comm)
return
@ -209,12 +209,12 @@ func (p *Process) ReadPath() error {
}
}()
if _, err := os.Lstat(p.pathExe); err != nil {
if _, err := os.Lstat(p.procPath[Exe]); err != nil {
return err
}
// FIXME: this reading can give error: file name too long
link, err := os.Readlink(p.pathExe)
link, err := os.Readlink(p.procPath[Exe])
if err != nil {
return err
}
@ -226,7 +226,7 @@ func (p *Process) ReadPath() error {
func (p *Process) SetPath(path string) {
p.Path = path
p.CleanPath()
p.RealPath = core.ConcatStrings(p.pathRoot, "/", p.Path)
p.RealPath = core.ConcatStrings(p.procPath[Root], "/", p.Path)
if core.Exists(p.RealPath) == false {
p.RealPath = p.Path
// p.CleanPath() ?
@ -236,13 +236,13 @@ func (p *Process) SetPath(path string) {
// ReadCmdline reads the cmdline of the process from ProcFS /proc/<pid>/cmdline
// This file may be empty if the process is of a kernel task.
// It can also be empty for short-lived processes.
func (p *Process) ReadCmdline() {
func (p *Process) ReadCmdline() error {
if len(p.Args) > 0 {
return
return nil
}
data, err := ioutil.ReadFile(p.pathCmdline)
data, err := ioutil.ReadFile(p.procPath[Cmdline])
if err != nil || len(data) == 0 {
return
return fmt.Errorf("%s empty", p.procPath[Cmdline])
}
// XXX: remove this loop, and split by "\x00"
for i, b := range data {
@ -259,6 +259,8 @@ func (p *Process) ReadCmdline() {
}
}
p.CleanArgs()
return nil
}
// CleanArgs applies fixes on the cmdline arguments.
@ -271,7 +273,7 @@ func (p *Process) CleanArgs() {
}
func (p *Process) readDescriptors() {
f, err := os.Open(p.pathFd)
f, err := os.Open(p.procPath[Fd])
if err != nil {
return
}
@ -283,7 +285,7 @@ func (p *Process) readDescriptors() {
tempFd := &procDescriptors{
Name: fd.Name(),
}
link, err := os.Readlink(core.ConcatStrings(p.pathFd, fd.Name()))
link, err := os.Readlink(core.ConcatStrings(p.procPath[Fd], fd.Name()))
if err != nil {
continue
}
@ -311,7 +313,7 @@ func (p *Process) readDescriptors() {
}
func (p *Process) readIOStats() (err error) {
f, err := os.Open(p.pathIO)
f, err := os.Open(p.procPath[IO])
if err != nil {
return err
}
@ -342,19 +344,19 @@ func (p *Process) readIOStats() (err error) {
}
func (p *Process) readStatus() {
if data, err := ioutil.ReadFile(p.pathStatus); err == nil {
if data, err := ioutil.ReadFile(p.procPath[Status]); err == nil {
p.Status = string(data)
}
if data, err := ioutil.ReadFile(p.pathStat); err == nil {
if data, err := ioutil.ReadFile(p.procPath[Stat]); err == nil {
p.Stat = string(data)
}
if data, err := ioutil.ReadFile(core.ConcatStrings("/proc/", strconv.Itoa(p.ID), "/stack")); err == nil {
p.Stack = string(data)
}
if data, err := ioutil.ReadFile(p.pathMaps); err == nil {
if data, err := ioutil.ReadFile(p.procPath[Maps]); err == nil {
p.Maps = string(data)
}
if data, err := ioutil.ReadFile(p.pathStatm); err == nil {
if data, err := ioutil.ReadFile(p.procPath[Statm]); err == nil {
p.Statm = &procStatm{}
fmt.Sscanf(string(data), "%d %d %d %d %d %d %d", &p.Statm.Size, &p.Statm.Resident, &p.Statm.Shared, &p.Statm.Text, &p.Statm.Lib, &p.Statm.Data, &p.Statm.Dt)
}
@ -372,7 +374,7 @@ func (p *Process) CleanPath() {
// to any process.
// Therefore we cannot use /proc/self/exe directly, because it resolves to our own process.
if strings.HasPrefix(p.Path, ProcSelf) {
if link, err := os.Readlink(p.pathExe); err == nil {
if link, err := os.Readlink(p.procPath[Exe]); err == nil {
p.Path = link
return
}
@ -390,7 +392,7 @@ func (p *Process) CleanPath() {
}
// We may receive relative paths from kernel, but the path of a process must be absolute
if core.IsAbsPath(p.Path) == false {
if pathLen > 0 && core.IsAbsPath(p.Path) == false {
if err := p.ReadPath(); err != nil {
log.Debug("ClenPath() error reading process path%s", err)
return
@ -401,7 +403,7 @@ func (p *Process) CleanPath() {
// IsAlive checks if the process is still running
func (p *Process) IsAlive() bool {
return core.Exists(p.pathProc)
return core.Exists(p.procPath[ProcID])
}
// IsChild determines if this process is child of its parent
@ -459,7 +461,7 @@ func (p *Process) ComputeChecksum(algo string) {
// Path cannot be trusted, because multiple processes with the same path
// can coexist in different namespaces.
// The real path is /proc/<pid>/root/<path-to-the-binary>
paths := []string{p.pathExe, p.RealPath, p.Path}
paths := []string{p.procPath[Exe], p.RealPath, p.Path}
var h hash.Hash
if algo == HashMD5 {
@ -530,7 +532,7 @@ func (p *Process) dumpFileImage(filePath string) ([]byte, error) {
var mappings []MemoryMapping
// read memory mappings
mapsFile, err := os.Open(p.pathMaps)
mapsFile, err := os.Open(p.procPath[Maps])
if err != nil {
return nil, err
}
@ -593,7 +595,7 @@ func (p *Process) dumpFileImage(filePath string) ([]byte, error) {
// given a range of addrs, read it from mem and return the content
func (p *Process) readMem(mappings []MemoryMapping) ([]byte, error) {
var elfCode []byte
memFile, err := os.Open(p.pathMem)
memFile, err := os.Open(p.procPath[Mem])
if err != nil {
return nil, err
}

View file

@ -6,25 +6,28 @@ import (
"fmt"
"sync"
"syscall"
"time"
"unsafe"
"github.com/evilsocket/opensnitch/daemon/core"
"github.com/evilsocket/opensnitch/daemon/log"
daemonNetlink "github.com/evilsocket/opensnitch/daemon/netlink"
"github.com/evilsocket/opensnitch/daemon/procmon"
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
elf "github.com/iovisor/gobpf/elf"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
)
//contains pointers to ebpf maps for a given protocol (tcp/udp/v6)
// contains pointers to ebpf maps for a given protocol (tcp/udp/v6)
type ebpfMapsForProto struct {
bpfmap *elf.Map
}
//Not in use, ~4usec faster lookup compared to m.LookupElement()
//mimics union bpf_attr's anonymous struct used by BPF_MAP_*_ELEM commands
//from <linux_headers>/include/uapi/linux/bpf.h
// mimics union bpf_attr's anonymous struct used by BPF_MAP_*_ELEM commands
// from <linux_headers>/include/uapi/linux/bpf.h
type bpf_lookup_elem_t struct {
map_fd uint64 //even though in bpf.h its type is __u32, we must make it 8 bytes long
//because "key" is of type __aligned_u64, i.e. "key" must be aligned on an 8-byte boundary
@ -47,8 +50,8 @@ const (
// Error returns the error type and a message with the explanation
type Error struct {
What int // 1 global error, 2 events error, 3 ...
Msg error
What int // 1 global error, 2 events error, 3 ...
}
var (
@ -76,22 +79,22 @@ var (
hostByteOrder binary.ByteOrder
)
//Start installs ebpf kprobes
// Start installs ebpf kprobes
func Start(modPath string) *Error {
modulesPath = modPath
setRunning(false)
if err := mountDebugFS(); err != nil {
return &Error{
NotAvailable,
fmt.Errorf("ebpf.Start: mount debugfs error. Report on github please: %s", err),
NotAvailable,
}
}
var err error
m, err = core.LoadEbpfModule("opensnitch.o", modulesPath)
if err != nil {
dispatchErrorEvent(fmt.Sprint("[eBPF]: ", err.Error()))
return &Error{NotAvailable, fmt.Errorf("[eBPF] Error loading opensnitch.o: %s", err.Error())}
return &Error{fmt.Errorf("[eBPF] Error loading opensnitch.o: %s", err.Error()), NotAvailable}
}
m.EnableOptionCompatProbe()
@ -101,10 +104,10 @@ func Start(modPath string) *Error {
if err := m.EnableKprobes(0); err != nil {
m.Close()
if err := m.Load(nil); err != nil {
return &Error{NotAvailable, fmt.Errorf("eBPF failed to load /etc/opensnitchd/opensnitch.o (2): %v", err)}
return &Error{fmt.Errorf("eBPF failed to load /etc/opensnitchd/opensnitch.o (2): %v", err), NotAvailable}
}
if err := m.EnableKprobes(0); err != nil {
return &Error{NotAvailable, fmt.Errorf("eBPF error when enabling kprobes: %v", err)}
return &Error{fmt.Errorf("eBPF error when enabling kprobes: %v", err), NotAvailable}
}
}
determineHostByteOrder()
@ -121,7 +124,7 @@ func Start(modPath string) *Error {
}
for prot, mfp := range ebpfMaps {
if mfp.bpfmap == nil {
return &Error{NotAvailable, fmt.Errorf("eBPF module opensnitch.o malformed, bpfmap[%s] nil", prot)}
return &Error{fmt.Errorf("eBPF module opensnitch.o malformed, bpfmap[%s] nil", prot), NotAvailable}
}
}
@ -200,7 +203,7 @@ func Stop() {
}
}
//make bpf() syscall with bpf_lookup prepared by the caller
// make bpf() syscall with bpf_lookup prepared by the caller
func makeBpfSyscall(bpf_lookup *bpf_lookup_elem_t) uintptr {
BPF_MAP_LOOKUP_ELEM := 1 //cmd number
syscall_BPF := 321 //syscall number
@ -211,9 +214,64 @@ func makeBpfSyscall(bpf_lookup *bpf_lookup_elem_t) uintptr {
return r1
}
// dispatch a rx/tx event to the server
func dispatchRxTxEvent(proc *procmon.Process, proto uint32, fam uint8, bsent, brecv uint64) {
protoProc := proc.Serialize()
protoStr := "tcp"
if proto == unix.IPPROTO_UDP {
protoStr = "udp"
}
family := ""
if fam == unix.AF_INET6 {
family = "6"
}
// send only the bytes received of the packet(s), not the totals of the process.
protoProc.BytesSent = map[string]uint64{protoStr + family: bsent}
protoProc.BytesRecv = map[string]uint64{protoStr + family: brecv}
log.Debug("dispatchProcExit, proto: %s, sent: %d, recv: %d", protoStr, bsent, brecv)
dispatchEvent(
&protocol.Alert{
Id: uint64(time.Now().UnixNano()),
Type: protocol.Alert_INFO,
Action: protocol.Alert_SAVE_TO_DB,
What: protocol.Alert_KERNEL_NET_RXTX,
// TODO: send a KernelEvent{ KernelNetEvent }
Data: &protocol.Alert_Proc{
protoProc,
},
},
)
}
func dispatchProcExitEvent(proc *procmon.Process, proto uint32, fam uint8, bsent, brecv uint64) {
protoProc := proc.Serialize()
log.Debug("dispatchProcExit: %s", proc.Path)
dispatchEvent(
&protocol.Alert{
Id: uint64(time.Now().UnixNano()),
Type: protocol.Alert_INFO,
Action: protocol.Alert_SAVE_TO_DB,
What: protocol.Alert_KERNEL_PROC_EXIT,
Data: &protocol.Alert_Proc{
protoProc,
},
},
)
}
func dispatchErrorEvent(what string) {
log.Error(what)
dispatchEvent(what)
dispatchEvent(
&protocol.Alert{
Id: uint64(time.Now().UnixNano()),
Type: protocol.Alert_ERROR,
Action: protocol.Alert_SHOW_ALERT,
What: protocol.Alert_GENERIC,
Data: &protocol.Alert_Text{what},
},
)
}
func dispatchEvent(data interface{}) {

View file

@ -6,6 +6,7 @@ import (
"fmt"
"os"
"os/signal"
"unsafe"
"github.com/evilsocket/opensnitch/daemon/core"
"github.com/evilsocket/opensnitch/daemon/log"
@ -43,10 +44,31 @@ type execEvent struct {
Pad2 uint32
}
type netEventT struct {
Type uint64
SaddrV6 uint64
DaddrV6 uint64
Cookie uint64
BytesSent uint64
BytesRecv uint64
LastSeen uint64
PID uint32
UID uint32
PPID uint32
Proto uint32
Saddr uint32
Daddr uint32
Sport uint16
Dport uint16
Fam uint8
}
// Struct that holds the metadata of a connection.
// When we receive a new connection, we look for it on the eBPF maps,
// and if it's found, this information is returned.
type networkEventT struct {
// When we receive a new connection via nfqueue, we look for it on the
// eBPF maps by the key srcport+srcip+dstip+dstport.
// If it's found, the following struct/info is returned (defined in opensnitch.c).
type connEventT struct {
Pid uint64
UID uint64
Comm [TaskCommLen]byte
@ -60,6 +82,10 @@ const (
EV_TYPE_EXECVEAT
EV_TYPE_FORK
EV_TYPE_SCHED_EXIT
EV_TYPE_TCP_CONN_DESTROYED
EV_TYPE_UDP_CONN_DESTROYED
EV_TYPE_RECV_BYTES
EV_TYPE_SENT_BYTES
)
var (
@ -69,7 +95,7 @@ var (
perfMapName = "proc-events"
// default value is 8.
// Not enough to handle high loads such http downloads, torent traffic, etc.
// Not enough to handle "high loads" such http downloads, torrent traffic, etc.
// (regular desktop usage)
ringBuffSize = 64 // * PAGE_SIZE (4k usually)
)
@ -81,13 +107,13 @@ func initEventsStreamer() *Error {
perfMod, err = core.LoadEbpfModule("opensnitch-procs.o", modulesPath)
if err != nil {
dispatchErrorEvent(fmt.Sprint("[eBPF events]: ", err))
return &Error{EventsNotAvailable, err}
return &Error{err, EventsNotAvailable}
}
perfMod.EnableOptionCompatProbe()
if err = perfMod.Load(elfOpts); err != nil {
dispatchErrorEvent(fmt.Sprint("[eBPF events]: ", err))
return &Error{EventsNotAvailable, err}
return &Error{err, EventsNotAvailable}
}
tracepoints := []string{
@ -115,7 +141,7 @@ Verify that your kernel has support for tracepoints (opensnitchd -check-requirem
perfMod.Close()
if err = perfMod.Load(elfOpts); err != nil {
dispatchErrorEvent(fmt.Sprintf("[eBPF events] failed to load /etc/opensnitchd/opensnitch-procs.o (2): %v", err))
return &Error{EventsNotAvailable, err}
return &Error{err, EventsNotAvailable}
}
if err = perfMod.EnableKprobes(0); err != nil {
dispatchErrorEvent(fmt.Sprintf("[eBPF events] error enabling kprobes: %v", err))
@ -130,7 +156,7 @@ Verify that your kernel has support for tracepoints (opensnitchd -check-requirem
eventWorkers = 0
if err := initPerfMap(perfMod); err != nil {
return &Error{EventsNotAvailable, err}
return &Error{err, EventsNotAvailable}
}
return nil
@ -158,19 +184,64 @@ func initPerfMap(mod *elf.Module) error {
func streamEventsWorker(id int, chn chan []byte, lost chan uint64, kernelEvents chan interface{}) {
var event execEvent
var netEvent netEventT
var buf bytes.Buffer
for {
event = execEvent{}
netEvent = netEventT{}
buf.Reset()
select {
case <-ctxTasks.Done():
goto Exit
case l := <-lost:
log.Debug("Lost ebpf events: %d", l)
case d := <-chn:
if err := binary.Read(bytes.NewBuffer(d), hostByteOrder, &event); err != nil {
log.Debug("[eBPF events #%d] error: %s", id, err)
case incomingEvent := <-chn:
switch incomingEvent[0] {
case EV_TYPE_SENT_BYTES,
EV_TYPE_RECV_BYTES,
EV_TYPE_TCP_CONN_DESTROYED,
EV_TYPE_UDP_CONN_DESTROYED:
buf.Write(incomingEvent)
if err := binary.Read(&buf, hostByteOrder, &netEvent); err != nil {
log.Debug("[eBPF NET events #%d] netbytes error: %s", id, err)
continue
}
default:
buf.Write(incomingEvent)
if err := binary.Read(&buf, hostByteOrder, &event); err != nil {
log.Debug("[eBPF events #%d] error: %s, event: %d", id, err, incomingEvent[0])
continue
}
}
switch incomingEvent[0] {
case EV_TYPE_SENT_BYTES, EV_TYPE_RECV_BYTES:
//dstIP := make(net.IP, 4)
//srcIP := make(net.IP, 4)
//binary.BigEndian.PutUint32(srcIP, netEvent.Saddr)
log.Debug("[eBPF events recv/sent]: %d, pid: %d, proto: %d sport: %d -> dport: %d, bytes_sent: %d, bytes_recv: %d", netEvent.Type, netEvent.PID, netEvent.Proto, netEvent.Sport, netEvent.Dport, netEvent.BytesSent, netEvent.BytesRecv)
item, found := procmon.EventsCache.IsInStoreByPID(int(netEvent.PID))
if found {
dispatchRxTxEvent(&item.Proc, netEvent.Proto, netEvent.Fam, netEvent.BytesSent, netEvent.BytesRecv)
// TODO: Proc.AddBytes? to apply quotas more rapidly?
procmon.EventsCache.UpdateItem(&item.Proc)
continue
}
case EV_TYPE_TCP_CONN_DESTROYED, EV_TYPE_UDP_CONN_DESTROYED:
log.Debug("[eBPF events conn destroyed]: %d, pid: %d, proto: %d sport: %d -> dport: %d, bytes_sent: %d, bytes_recv: %d", netEvent.Type, netEvent.PID, netEvent.Proto, netEvent.Sport, netEvent.Dport, netEvent.BytesSent, netEvent.BytesRecv)
item, found := procmon.EventsCache.IsInStoreByPID(int(netEvent.PID))
if found {
dispatchRxTxEvent(&item.Proc, netEvent.Proto, netEvent.Fam, netEvent.BytesSent, netEvent.BytesRecv)
item.Proc.AddBytes(netEvent.Fam, netEvent.Proto, netEvent.BytesSent, netEvent.BytesRecv)
procmon.EventsCache.UpdateItem(&item.Proc)
continue
}
switch event.Type {
case EV_TYPE_EXEC, EV_TYPE_EXECVEAT:
processExecEvent(&event)
@ -259,4 +330,7 @@ func getProcDetails(event *execEvent, proc *procmon.Process) {
func processExitEvent(event *execEvent) {
log.Debug("[eBPF exit event] pid: %d, ppid: %d", event.PID, event.PPID)
procmon.EventsCache.Delete(int(event.PID))
m.DeleteElement(perfMod.Map("tcpBytesMap"), unsafe.Pointer(&event.PID))
m.DeleteElement(perfMod.Map("udpBytesMap"), unsafe.Pointer(&event.PID))
}

View file

@ -83,7 +83,7 @@ func getPidFromEbpf(proto string, srcPort uint, srcIP net.IP, dstIP net.IP, dstP
return
}
var value networkEventT
var value connEventT
var key []byte
var isIP4 bool = (proto == "tcp") || (proto == "udp") || (proto == "udplite")
@ -163,7 +163,7 @@ func getPidFromEbpf(proto string, srcPort uint, srcIP net.IP, dstIP net.IP, dstP
// By default we only receive the PID of the process, so we need to get
// the rest of the details.
// TODO: get the details from kernel, with mm_struct (exe_file, fd_path, etc).
func findConnProcess(value *networkEventT, connKey string) (proc *procmon.Process) {
func findConnProcess(value *connEventT, connKey string) (proc *procmon.Process) {
// Use socket's UID. A process may have dropped privileges.
// This is the UID that we've always used.

View file

@ -77,7 +77,7 @@ func getItems(proto string, isIPv6 bool) (items uint) {
lookupKey = make([]byte, 36)
nextKey = make([]byte, 36)
}
var value networkEventT
var value connEventT
firstrun := true
for {
@ -122,7 +122,7 @@ func deleteOldItems(proto string, isIPv6 bool, maxToDelete uint) (deleted uint)
lookupKey = make([]byte, 36)
nextKey = make([]byte, 36)
}
var value networkEventT
var value connEventT
firstrun := true
i := uint(0)

View file

@ -8,6 +8,7 @@ import (
"github.com/evilsocket/opensnitch/daemon/core"
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
"golang.org/x/sys/unix"
)
var (
@ -29,6 +30,24 @@ const (
HashSHA1 = "process.hash.sha1"
)
// ..
const (
ProcID = iota
Comm
Cmdline
Exe
Cwd
Environ
Root
Status
Statm
Stat
Mem
Maps
Fd
IO
)
// man 5 proc; man procfs
type procIOstats struct {
RChar int64
@ -61,6 +80,13 @@ type procStatm struct {
Dt int
}
type procBytes struct {
sent uint64
recv uint64
proto uint8
fam uint8
}
// Process holds the details of a process.
type Process struct {
mu *sync.RWMutex
@ -69,26 +95,15 @@ type Process struct {
IOStats *procIOstats
NetStats *procNetStats
Env map[string]string
BytesSent map[string]uint64
BytesRecv map[string]uint64
Checksums map[string]string
CWD string
Status string
Stat string
Stack string
Maps string
Comm string
pathProc string
pathComm string
pathExe string
pathCmdline string
pathCwd string
pathEnviron string
pathRoot string
pathFd string
pathStatus string
pathStatm string
pathStat string
pathMaps string
pathMem string
pathIO string
// Path is the absolute path to the binary
Path string
@ -97,7 +112,7 @@ type Process struct {
// The simplest form of accessing the RealPath is by prepending /proc/<pid>/root/ to the path:
// /usr/bin/curl -> /proc/<pid>/root/usr/bin/curl
RealPath string
CWD string
Tree []*protocol.StringInt
Descriptors []*procDescriptors
// Args is the command that the user typed. It MAY contain the absolute path
@ -109,6 +124,7 @@ type Process struct {
// -> Path: /usr/bin/curl
// -> Args: /usr/bin/curl https://....
Args []string
procPath []string
Starttime int64
ID int
PPID int
@ -124,27 +140,31 @@ func NewProcessEmpty(pid int, comm string) *Process {
PPID: 0,
Comm: comm,
Args: make([]string, 0),
procPath: make([]string, 14),
Env: make(map[string]string),
BytesSent: make(map[string]uint64, 2),
BytesRecv: make(map[string]uint64, 2),
Tree: make([]*protocol.StringInt, 0),
IOStats: &procIOstats{},
NetStats: &procNetStats{},
Statm: &procStatm{},
Checksums: make(map[string]string),
}
p.pathProc = core.ConcatStrings("/proc/", strconv.Itoa(p.ID))
p.pathExe = core.ConcatStrings(p.pathProc, "/exe")
p.pathCwd = core.ConcatStrings(p.pathProc, "/cwd")
p.pathComm = core.ConcatStrings(p.pathProc, "/comm")
p.pathCmdline = core.ConcatStrings(p.pathProc, "/cmdline")
p.pathEnviron = core.ConcatStrings(p.pathProc, "/environ")
p.pathStatus = core.ConcatStrings(p.pathProc, "/status")
p.pathStatm = core.ConcatStrings(p.pathProc, "/statm")
p.pathRoot = core.ConcatStrings(p.pathProc, "/root")
p.pathMaps = core.ConcatStrings(p.pathProc, "/maps")
p.pathStat = core.ConcatStrings(p.pathProc, "/stat")
p.pathMem = core.ConcatStrings(p.pathProc, "/mem")
p.pathFd = core.ConcatStrings(p.pathProc, "/fd/")
p.pathIO = core.ConcatStrings(p.pathProc, "/io")
p.procPath[ProcID] = core.ConcatStrings("/proc/", strconv.Itoa(p.ID))
p.procPath[Exe] = core.ConcatStrings(p.procPath[ProcID], "/exe")
p.procPath[Cwd] = core.ConcatStrings(p.procPath[ProcID], "/cwd")
p.procPath[Comm] = core.ConcatStrings(p.procPath[ProcID], "/comm")
p.procPath[Cmdline] = core.ConcatStrings(p.procPath[ProcID], "/cmdline")
p.procPath[Environ] = core.ConcatStrings(p.procPath[ProcID], "/environ")
p.procPath[Status] = core.ConcatStrings(p.procPath[ProcID], "/status")
p.procPath[Statm] = core.ConcatStrings(p.procPath[ProcID], "/statm")
p.procPath[Root] = core.ConcatStrings(p.procPath[ProcID], "/root")
p.procPath[Maps] = core.ConcatStrings(p.procPath[ProcID], "/maps")
p.procPath[Stat] = core.ConcatStrings(p.procPath[ProcID], "/stat")
p.procPath[Mem] = core.ConcatStrings(p.procPath[ProcID], "/mem")
p.procPath[Fd] = core.ConcatStrings(p.procPath[ProcID], "/fd/")
p.procPath[IO] = core.ConcatStrings(p.procPath[ProcID], "/io")
return p
}
@ -195,8 +215,29 @@ func (p *Process) RUnlock() {
p.mu.RUnlock()
}
//Serialize transforms a Process object to gRPC protocol object
// AddBytes accumulates the bytes sent by this process
func (p *Process) AddBytes(fam uint8, proto uint32, sent, recv uint64) {
p.mu.Lock()
defer p.mu.Unlock()
protoStr := "tcp"
if proto == unix.IPPROTO_UDP {
protoStr = "udp"
}
family := ""
if fam == unix.AF_INET6 {
family = "6"
}
p.BytesSent[protoStr+family] += recv
p.BytesRecv[protoStr+family] += recv
}
// Serialize transforms a Process object to gRPC protocol object
func (p *Process) Serialize() *protocol.Process {
p.mu.RLock()
defer p.mu.RUnlock()
ioStats := p.IOStats
netStats := p.NetStats
if ioStats == nil {
@ -206,6 +247,17 @@ func (p *Process) Serialize() *protocol.Process {
netStats = &procNetStats{}
}
// maps are referenced data types, we cannot assign a map to another
// an expect to be a copy.
bsent := make(map[string]uint64, len(p.BytesSent))
brecv := make(map[string]uint64, len(p.BytesRecv))
for k, v := range p.BytesSent {
bsent[k] = v
}
for k, v := range p.BytesRecv {
brecv[k] = v
}
return &protocol.Process{
Pid: uint64(p.ID),
Ppid: uint64(p.PPID),
@ -220,7 +272,9 @@ func (p *Process) Serialize() *protocol.Process {
IoWrites: uint64(ioStats.WChar),
NetReads: netStats.ReadBytes,
NetWrites: netStats.WriteBytes,
ProcessTree: p.Tree,
BytesSent: bsent,
BytesRecv: brecv,
Tree: p.Tree,
}
}

View file

@ -32,6 +32,7 @@ const (
List = Type("list")
Network = Type("network")
Lists = Type("lists")
Quota = Type("Quota")
)
// Available operands
@ -62,9 +63,9 @@ const (
OpNetLists = Operand("lists.nets")
// TODO
//OpHashMD5Lists = Operand("lists.hash.md5")
//OpQuota = Operand("quota")
//OpQuotaTxOver = Operand("quota.sent.over") // 1000b, 1kb, 1mb, 1gb, ...
//OpQuotaRxOver = Operand("quota.recv.over") // 1000b, 1kb, 1mb, 1gb, ...
OpQuota = Operand("quota")
OpQuotaTxOver = Operand("quota.sent.over") // 1000b, 1kb, 1mb, 1gb, ...
OpQuotaRxOver = Operand("quota.recv.over") // 1000b, 1kb, 1mb, 1gb, ...
)
type opCallback func(value interface{}) bool
@ -154,6 +155,33 @@ func (o *Operator) Compile() error {
o.cb = o.ipNetCmp
} else if o.Operand == OpProcessHashMD5 || o.Operand == OpProcessHashSHA1 {
o.cb = o.hashCmp
} else if o.Operand == OpQuotaTxOver {
if o.Data == "" {
return fmt.Errorf("Operand quota cannot be empty: %s", o)
}
o.cb = o.quotaCmp
if strings.HasSuffix(o.Data, "kb") {
if val, err := strconv.Atoi(o.Data[:len(o.Data)-2]); err == nil {
o.Data = fmt.Sprint(val * 1024)
}
}
if strings.HasSuffix(o.Data, "mb") {
if val, err := strconv.Atoi(o.Data[:len(o.Data)-2]); err == nil {
o.Data = fmt.Sprint((val * 1024) * 1024)
}
}
if strings.HasSuffix(o.Data, "gb") {
if val, err := strconv.Atoi(o.Data[:len(o.Data)-2]); err == nil {
o.Data = fmt.Sprint(((val * 1024) * 1024) * 1024)
}
}
if strings.HasSuffix(o.Data, "tb") {
if val, err := strconv.Atoi(o.Data[:len(o.Data)-2]); err == nil {
o.Data = fmt.Sprint((((val * 1024) * 1024) * 1024) * 1024)
}
} else {
o.Data = o.Data[:len(o.Data)-1]
}
}
log.Debug("Operator compiled: %s", o)
o.isCompiled = true
@ -176,6 +204,38 @@ func (o *Operator) simpleCmp(v interface{}) bool {
return v == o.Data
}
// quotaCmp
// 1. compare sent/recv bytes
// 2. on bytes over quota:
// - reset proc bytes
// - send alert
// - change action to deny ¿?
// 3. if comparison matches, apply defined action: over quota? -> reject, until quota? -> allow
func (o *Operator) quotaCmp(v interface{}) bool {
con, ok := v.(*conman.Connection)
if !ok {
return false
}
// XXX: get rid of this conversion on Compile()?
b, err := strconv.ParseUint(o.Data, 10, 64)
if err != nil {
return false
}
bsent, _ := con.Process.BytesSent[con.Protocol]
brecv, _ := con.Process.BytesRecv[con.Protocol]
result := false
// quota.over
result = bsent > b || brecv > b
// quota.sent.over
//result = bsent > b
// quota.until.over
//result = bsent < b
return result
}
func (o *Operator) reCmp(v interface{}) bool {
if vt := reflect.ValueOf(v).Kind(); vt != reflect.String {
log.Warning("Operator.reCmp() bad interface type: %T", v)
@ -341,6 +401,8 @@ func (o *Operator) Match(con *conman.Connection, hasChecksums bool) bool {
return o.cb(strconv.FormatUint(uint64(con.SrcPort), 10))
} else if o.Operand == OpProcessID {
return o.cb(strconv.Itoa(con.Process.ID))
} else if o.Operand == OpQuotaTxOver {
return o.cb(con)
} else if strings.HasPrefix(string(o.Operand), string(OpProcessEnvPrefix)) {
envVarName := core.Trim(string(o.Operand[OpProcessEnvPrefixLen:]))
envVarValue, _ := con.Process.Env[envVarName]

View file

@ -33,6 +33,14 @@ func NewAlert(atype protocol.Alert_Type, what protocol.Alert_What, action protoc
}
switch what {
case protocol.Alert_KERNEL_PROC_EXIT:
switch data.(type) {
case procmon.Process:
a.Data = &protocol.Alert_Proc{
data.(*procmon.Process).Serialize(),
}
}
case protocol.Alert_KERNEL_EVENT:
switch data.(type) {

View file

@ -360,7 +360,12 @@ func (c *Client) PostAlert(atype protocol.Alert_Type, awhat protocol.Alert_What,
if c.Connected() == false {
log.Debug("UI not connected, queueing alert: %d", len(c.alertsChan))
}
switch data.(type) {
case *protocol.Alert:
c.alertsChan <- *(data.(*protocol.Alert))
default:
c.alertsChan <- *NewAlert(atype, awhat, action, prio, data)
}
}
func (c *Client) monitorConfigWorker() {

View file

@ -106,16 +106,19 @@ func (c *Client) loadConfiguration(rawConfig []byte) bool {
}
c.setSocketPath(tempSocketPath)
}
if clientConfig.DefaultAction != "" {
clientDisconnectedRule.Action = rule.Action(clientConfig.DefaultAction)
clientErrorRule.Action = rule.Action(clientConfig.DefaultAction)
// TODO: reconfigure connected rule if changed, but not save it to disk.
//clientConnectedRule.Action = rule.Action(clientConfig.DefaultAction)
}
if clientConfig.DefaultDuration != "" {
clientDisconnectedRule.Duration = rule.Duration(clientConfig.DefaultDuration)
clientErrorRule.Duration = rule.Duration(clientConfig.DefaultDuration)
}
if clientConfig.ProcMonitorMethod != "" {
err := monitor.ReconfigureMonitorMethod(clientConfig.ProcMonitorMethod, clientConfig.Ebpf.ModulesPath)
if err != nil {

View file

@ -3,6 +3,7 @@
#include "common_defs.h"
//https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/limits.h#L13
#ifndef MAX_PATH_LEN
#define MAX_PATH_LEN 4096
@ -32,6 +33,10 @@ enum events_type {
EVENT_EXECVEAT,
EVENT_FORK,
EVENT_SCHED_EXIT,
EVENT_TCP_CONN_DESTROYED,
EVENT_UDP_CONN_DESTROYED,
EVENT_RECV_BYTES,
EVENT_SEND_BYTES
};
struct trace_ev_common {
@ -41,6 +46,20 @@ struct trace_ev_common {
int common_pid;
};
struct trace_tcp_destroy_sock {
struct trace_ev_common ext;
const void * skaddr;
u16 sport;
u16 dport;
u16 family;
u8 saddr[4];
u8 daddr[4];
u8 saddr_v6[16];
u8 daddr_v6[16];
u64 cookie;
};
struct trace_sys_enter_execve {
struct trace_ev_common ext;
@ -84,6 +103,26 @@ struct data_t {
u32 pad2;
};
struct network_event_t {
u64 type;
u64 saddr_v6;
u64 daddr_v6;
u64 cookie;
u64 bytes_sent;
u64 bytes_recv;
u64 last_sent;
u32 pid;
u32 uid;
u32 ppid;
u32 proto;
u32 saddr;
u32 daddr;
u16 sport;
u16 dport;
u8 family;
};
//-----------------------------------------------------------------------------
// maps
@ -94,4 +133,20 @@ struct bpf_map_def SEC("maps/heapstore") heapstore = {
.max_entries = 1
};
struct bpf_map_def SEC("maps/tcpBytesMap") tcpBytesMap = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(u64),
.value_size = sizeof(struct network_event_t),
.max_entries = 13000,
};
struct bpf_map_def SEC("maps/udpBytesMap") udpBytesMap = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(u64),
.value_size = sizeof(struct network_event_t),
.max_entries = 13001,
};
#endif

View file

@ -1,6 +1,7 @@
#define KBUILD_MODNAME "opensnitch-procs"
#include "common.h"
#include <net/sock.h>
struct bpf_map_def SEC("maps/proc-events") events = {
// Since kernel 4.4
@ -12,9 +13,9 @@ struct bpf_map_def SEC("maps/proc-events") events = {
struct bpf_map_def SEC("maps/execMap") execMap = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(u32),
.key_size = sizeof(u64),
.value_size = sizeof(struct data_t),
.max_entries = 256,
.max_entries = 257,
};
@ -56,6 +57,149 @@ out:
bpf_map_delete_elem(&execMap, &pid_tgid);
}
static int __always_inline __handle_destroy_sock(struct pt_regs *ctx, short proto, short fam)
{
#if defined(__i386__)
// On x86_32 platforms accessing arguments using PT_REGS_PARM1 seems to cause probles.
// That's why we are accessing registers directly.
struct sock *sk = (struct sock *)((ctx)->ax);
#else
struct sock *sk = (struct sock *)PT_REGS_PARM1(ctx);
#endif
bpf_probe_read(&fam, sizeof(fam), &sk->__sk_common.skc_family);
if (fam != AF_INET && fam != AF_INET6){
return 0;
}
struct network_event_t *net_event={0};
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
// invalid pid / unable to obtain it
if (pid == 0){
return 0;
}
if (proto == IPPROTO_UDP){
net_event = (struct network_event_t *)bpf_map_lookup_elem(&udpBytesMap, &pid_tgid);
} else {
net_event = (struct network_event_t *)bpf_map_lookup_elem(&tcpBytesMap, &pid_tgid);
}
if (!net_event){
return 0;
}
net_event->proto = proto;
net_event->pid = pid;
net_event->family = fam;
bpf_probe_read(&net_event->proto, sizeof(u8), &sk->sk_protocol);
bpf_probe_read(&net_event->dport, sizeof(net_event->dport), &sk->__sk_common.skc_dport);
bpf_probe_read(&net_event->sport, sizeof(net_event->sport), &sk->__sk_common.skc_num);
bpf_probe_read(&net_event->daddr, sizeof(net_event->daddr), &sk->__sk_common.skc_daddr);
bpf_probe_read(&net_event->saddr, sizeof(net_event->saddr), &sk->__sk_common.skc_rcv_saddr);
bpf_probe_read(&net_event->cookie, sizeof(net_event->cookie), &sk->__sk_common.skc_cookie);
net_event->type = EVENT_TCP_CONN_DESTROYED;
if (proto == IPPROTO_UDP){
net_event->type = EVENT_UDP_CONN_DESTROYED;
}
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, net_event, sizeof(*net_event));
if (proto == IPPROTO_UDP){
bpf_map_delete_elem(&udpBytesMap, &pid_tgid);
} else {
bpf_map_delete_elem(&tcpBytesMap, &pid_tgid);
}
return 0;
};
/**
* A common function to count bytes per protocol and type (recv/sent).
* Bytes are only sent to userspace every +-3 seconds, or on the first packet
* seen, otherwise they're accumulated.
*/
static int __always_inline __handle_transfer_bytes(struct pt_regs *ctx, short proto, short fam, short type)
{
int slen = PT_REGS_RC(ctx);
if (slen < 0){
return 0;
}
u64 now = bpf_ktime_get_ns();
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
// TODO: check pid == 0?
struct network_event_t *net_event=NULL;
if (proto == IPPROTO_TCP){
net_event = (struct network_event_t *)bpf_map_lookup_elem(&tcpBytesMap, &pid_tgid);
}
else if (proto == IPPROTO_UDP){
net_event = (struct network_event_t *)bpf_map_lookup_elem(&udpBytesMap, &pid_tgid);
}
if (!net_event){
struct network_event_t new_net_event;
__builtin_memset(&new_net_event, 0, sizeof(new_net_event));
new_net_event.pid = pid;
new_net_event.last_sent = now;
new_net_event.proto = proto;
new_net_event.type = type;
new_net_event.family = fam;
if (type == EVENT_SEND_BYTES){
new_net_event.bytes_sent = slen;
} else {
new_net_event.bytes_recv = slen;
}
int ret = 0;
if (proto == IPPROTO_TCP){
ret = bpf_map_update_elem(&tcpBytesMap, &pid_tgid, &new_net_event, BPF_ANY);
} else if (proto == IPPROTO_UDP){
ret = bpf_map_update_elem(&udpBytesMap, &pid_tgid, &new_net_event, BPF_ANY);
}
if (ret != 0){
char x[] = "transfer bytes, unable to update map, proto: %d, error: %d\n";
bpf_trace_printk(x, sizeof(x), proto, ret);
}
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &new_net_event, sizeof(new_net_event));
return 0;
}
u64 diff = now - net_event->last_sent;
net_event->pid = pid;
net_event->family = fam;
if (type == EVENT_SEND_BYTES){
__sync_fetch_and_add(&net_event->bytes_sent, slen);
} else {
__sync_fetch_and_add(&net_event->bytes_recv, slen);
}
if (diff > 1e9 * 2) {
net_event->last_sent = now;
net_event->pid = pid;
net_event->proto = proto;
net_event->type = type;
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, net_event, sizeof(*net_event));
// once sent to userspace, reset counters
if (type == EVENT_SEND_BYTES){
net_event->bytes_sent = 0;
} else {
net_event->bytes_recv = 0;
}
}
if (proto == IPPROTO_TCP){
bpf_map_update_elem(&tcpBytesMap, &pid_tgid, net_event, BPF_ANY);
} else if (proto == IPPROTO_UDP){
bpf_map_update_elem(&udpBytesMap, &pid_tgid, net_event, BPF_ANY);
}
return 0;
};
// https://0xax.gitbooks.io/linux-insides/content/SysCall/linux-syscall-4.html
// bprm_execve REGS_PARM3
// https://elixir.bootlin.com/linux/latest/source/fs/exec.c#L1796
@ -72,7 +216,21 @@ int tracepoint__sched_sched_process_exit(struct pt_regs *ctx)
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, data, sizeof(*data));
u64 pid_tgid = bpf_get_current_pid_tgid();
u64 pid = pid_tgid >> 32;
struct network_event_t *tcp_net_event = (struct network_event_t *)bpf_map_lookup_elem(&tcpBytesMap, &pid_tgid);
struct network_event_t *udp_net_event = (struct network_event_t *)bpf_map_lookup_elem(&udpBytesMap, &pid_tgid);
if (tcp_net_event != NULL){
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, tcp_net_event, sizeof(*tcp_net_event));
}
if (udp_net_event != NULL){
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, udp_net_event, sizeof(*udp_net_event));
}
bpf_map_delete_elem(&tcpBytesMap, &pid);
bpf_map_delete_elem(&udpBytesMap, &pid);
bpf_map_delete_elem(&execMap, &pid_tgid);
return 0;
};
@ -193,6 +351,108 @@ int tracepoint__syscalls_sys_enter_execveat(struct trace_sys_enter_execveat* ctx
};
SEC("kretprobe/tcp_sendmsg")
int kretprobe__tcp_sendmsg(struct pt_regs *ctx)
{
__handle_transfer_bytes(ctx, IPPROTO_TCP, 0x0, EVENT_SEND_BYTES);
return 0;
};
SEC("kretprobe/tcp_recvmsg")
int kretprobe__tcp_recvmsg(struct pt_regs *ctx)
{
__handle_transfer_bytes(ctx, IPPROTO_TCP, 0x0, EVENT_RECV_BYTES);
return 0;
};
SEC("kretprobe/udp_sendmsg")
int kretprobe__udp_sendmsg(struct pt_regs *ctx)
{
__handle_transfer_bytes(ctx, IPPROTO_UDP, AF_INET, EVENT_SEND_BYTES);
return 0;
};
SEC("kretprobe/udp_recvmsg")
int kretprobe__udp_recvmsg(struct pt_regs *ctx)
{
__handle_transfer_bytes(ctx, IPPROTO_UDP, AF_INET, EVENT_RECV_BYTES);
return 0;
};
SEC("kretprobe/udpv6_sendmsg")
int kretprobe__udpv6_sendmsg(struct pt_regs *ctx)
{
__handle_transfer_bytes(ctx, IPPROTO_UDP, 0xa, EVENT_SEND_BYTES);
return 0;
};
SEC("kretprobe/udpv6_recvmsg")
int kretprobe__udpv6_recvmsg(struct pt_regs *ctx)
{
__handle_transfer_bytes(ctx, IPPROTO_UDP, 0xa, EVENT_RECV_BYTES);
return 0;
};
SEC("kprobe/tcp_close")
int kprobe__tcp_close(struct pt_regs *ctx)
{
__handle_destroy_sock(ctx, IPPROTO_TCP, 0x0);
return 0;
}
// SEC("kprobe/release_sock")
// SEC("kprobe/inet_sock_destruct")
/**
* tcp_v4_destroy_sock also used for tcpv6?
* https://elixir.bootlin.com/linux/latest/source/net/ipv6/tcp_ipv6.c#L2150
*/
SEC("kprobe/tcp_v4_destroy_sock")
int kprobe__tcp_v4_destroy_sock(struct pt_regs *ctx)
{
__handle_destroy_sock(ctx, 0x0, 0x0);
return 0;
}
SEC("kprobe/udp_abort")
int kprobe__udp_abort(struct pt_regs *ctx)
{
__handle_destroy_sock(ctx, IPPROTO_UDP, AF_INET);
return 0;
}
/**
* udp_disconnect common for ipv4 and ipv6
* https://elixir.bootlin.com/linux/latest/source/net/ipv6/udp.c#L1761
*/
SEC("kprobe/udp_disconnect")
int kprobe__udp_disconnect(struct pt_regs *ctx)
{
__handle_destroy_sock(ctx, IPPROTO_UDP, 0x0);
return 0;
}
SEC("kprobe/udp_destruct_sock")
int kprobe__udp_destruct_sock(struct pt_regs *ctx)
{
__handle_destroy_sock(ctx, IPPROTO_UDP, AF_INET);
return 0;
}
SEC("kprobe/udpv6_destruct_sock")
int kprobe__udpv6_destruct_sock(struct pt_regs *ctx)
{
__handle_destroy_sock(ctx, IPPROTO_UDP, AF_INET6);
return 0;
}
char _license[] SEC("license") = "GPL";
// this number will be interpreted by the elf loader

View file

@ -25,9 +25,10 @@ message Alert {
HIGH = 2;
}
enum Type {
ERROR = 0;
WARNING = 1;
INFO = 2;
NOT_DEFINED = 0;
ERROR = 1;
WARNING = 2;
INFO = 3;
}
enum Action {
NONE = 0;
@ -44,6 +45,11 @@ message Alert {
NETLINK = 5;
// bind, exec, etc
KERNEL_EVENT = 6;
KERNEL_PROC_EXEC = 7;
KERNEL_PROC_EXIT = 8;
KERNEL_NET_BIND = 9;
KERNEL_NET_RXTX = 10;
UNKNOWN = 999;
}
uint64 id = 1;
@ -62,6 +68,8 @@ message Alert {
Connection conn = 9;
Rule rule = 10;
FwRule fwrule = 11;
KernelEvent kEvent = 12;
Message msg = 13;
}
}
@ -69,6 +77,42 @@ message MsgResponse {
uint64 id = 1;
}
message KernelProcEvent {
uint64 id = 1;
uint32 what = 2;
Process proc = 3;
}
message KernelNetEvent {
uint64 id = 1;
// bind, connection, rxtx, others?
uint32 what = 2;
Connection conn = 3;
}
message KernelEvent {
enum Generic {
GENERIC = 0;
PROC_EXEC = 1;
PROC_EXIT = 2;
// net events
NET_BIND = 100;
NET_RXTX = 101;
}
uint64 id = 1;
// execve, proc (bytes/recv), bind
uint32 type = 2;
oneof data {
string text = 3;
KernelProcEvent procEvent = 4;
KernelNetEvent netEvent = 5;
}
}
// --------------------------------------------------------------------------
message Event {
string time = 1;
Connection connection = 2;
@ -110,6 +154,11 @@ message StringInt {
uint32 value = 2;
}
message Message {
string key = 1;
string value = 2;
}
message Process {
uint64 pid = 1;
uint64 ppid = 2;
@ -124,7 +173,9 @@ message Process {
uint64 io_writes = 11;
uint64 net_reads = 12;
uint64 net_writes = 13;
repeated StringInt process_tree = 14;
map<string, uint64> bytes_sent = 15;
map<string, uint64> bytes_recv = 16;
repeated StringInt tree = 17;
}
message Connection {
@ -138,10 +189,12 @@ message Connection {
uint32 process_id = 8;
string process_path = 9;
string process_cwd = 10;
repeated string process_args = 11;
map<string, string> process_env = 12;
map<string, string> process_checksums = 13;
repeated StringInt process_tree = 14;
uint64 process_bytessent = 11;
uint64 process_bytesrecv = 12;
repeated string process_args = 13;
map<string, string> process_env = 14;
map<string, string> process_checksums = 15;
repeated StringInt process_tree = 16;
}
message Operator {

View file

View file

@ -0,0 +1,29 @@
from PyQt5 import QtWidgets, QtGui, QtCore
from opensnitch import ui_pb2
from opensnitch.notifications import DesktopNotifications
def get_urgency(alert):
urgency = DesktopNotifications.URGENCY_NORMAL
if alert.priority == ui_pb2.Alert.LOW:
urgency = DesktopNotifications.URGENCY_LOW
elif alert.priority == ui_pb2.Alert.HIGH:
urgency = DesktopNotifications.URGENCY_CRITICAL
return urgency
def get_icon(alert):
icon = QtWidgets.QSystemTrayIcon.Information
_title = QtCore.QCoreApplication.translate("messages", "Info")
atype = "INFO"
if alert.type == ui_pb2.Alert.ERROR:
atype = "ERROR"
_title = QtCore.QCoreApplication.translate("messages", "Error")
icon = QtWidgets.QSystemTrayIcon.Critical
if alert.type == ui_pb2.Alert.WARNING:
atype = "WARNING"
_title = QtCore.QCoreApplication.translate("messages", "Warning")
icon = QtWidgets.QSystemTrayIcon.Warning
return icon

View file

@ -0,0 +1,69 @@
from PyQt5 import QtWidgets, QtCore
from datetime import datetime
from opensnitch import ui_pb2
from opensnitch.database import Database
from opensnitch.notifications import DesktopNotifications
from opensnitch.alerts import _utils
class Alert:
type = "INFO"
what = "GENERIC"
body = "generic alert"
title = "Info"
icon = QtWidgets.QSystemTrayIcon.Information
urgency = DesktopNotifications.URGENCY_NORMAL
pb_alert = None
# flag to indicate if the alert was generated locally (same host)
is_local = True
def __init__(self, proto, addr, is_local, pb_alert):
self._db = Database.instance()
self.proto = proto
self.addr = addr
self.is_local = is_local
self.pb_alert = pb_alert
self.alert_type = pb_alert.type
self.body = pb_alert.text
def build(self):
self.title = QtCore.QCoreApplication.translate("messages", "Info")
if self.what == ui_pb2.Alert.KERNEL_EVENT:
self.body = "%s\n%s" % (self.text, self.proc.path)
self.what = "KERNEL EVENT"
if self.what == ui_pb2.Alert.NET_EVENT:
self.body = "%s\n%s" % (self.text, self.proc.path)
self.what = "NETWORK EVENT"
if self.is_local is False:
self.body = "node: {0}:{1}\n\n{2}\n{3}".format(self.proto, self.addr, self.text, self.proc.path)
self.icon = _utils.get_icon(self.pb_alert)
self.urgency = _utils.get_urgency(self.pb_alert)
if self.type == ui_pb2.Alert.ERROR:
self.type = "ERROR"
self.title = QtCore.QCoreApplication.translate("messages", "Error")
self.icon = QtWidgets.QSystemTrayIcon.Critical
elif self.type == ui_pb2.Alert.WARNING:
self.type = "WARNING"
self.title = QtCore.QCoreApplication.translate("messages", "Warning")
self.icon = QtWidgets.QSystemTrayIcon.Warning
if self.priority == ui_pb2.Alert.LOW:
urgency = DesktopNotifications.URGENCY_LOW
elif self.priority == ui_pb2.Alert.HIGH:
urgency = DesktopNotifications.URGENCY_CRITICAL
return self.title, self.body, self.icon, urgency
def save(self):
if self.type == ui_pb2.Alert.GENERIC:
self._db.insert(
"alerts",
"(time, node, type, action, priority, what, body, status)",
(
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
self.proto+":"+self.addr, self.type, "", "", self.what, self.body, 0
))

View file

@ -0,0 +1,67 @@
import json
from datetime import datetime
from google.protobuf.json_format import MessageToJson
from opensnitch.database import Database
class RxTx:
def __init__(self, proto, addr, pb_alert):
self._db = Database.instance()
self.proto = proto
self.addr = addr
self.proc = json.loads(MessageToJson(pb_alert.proc))
self.env = ""
if self.proc.get('env') != None:
self.env = json.dumps(self.proc['env'])
self.tree = ""
if self.proc.get('tree') != None:
self.tree = json.dumps(self.proc['tree'])
self.checksums=""
if self.proc.get('checksums') != None:
self.checksums = json.dumps(self.proc['checksums'])
# totals
self.bytesSent = 0
self.bytesRecv = 0
self.proto = ""
if self.proc.get('bytesSent') != None:
for k in self.proc['bytesSent']:
self.bytesSent += int(self.proc['bytesSent'][k])
self.proto = k
if self.proc.get('bytesRecv') != None:
for k in self.proc['bytesRecv']:
self.bytesRecv += int(self.proc['bytesRecv'][k])
self.proto = k
self.cwd = ""
if self.proc.get('cwd') != None:
self.cwd = self.proc['cwd']
def save(self):
ret, lastId = self._db.insert("procs", "(what, hits)", (self.proc['path'], 0), action_on_conflict="IGNORE")
# TODO: path is not valid as primary key. We should use
# node+path as minimum.
ret, lastId = self._db.insert("rxtx",
"(time, what, proto, bytes_sent, bytes_recv, proc_path_fk)",
(
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
0, # process, conn, etc
self.proto,
self.bytesSent,
self.bytesRecv,
self.proc['path']
))
ret, lastId = self._db.insert("proc_details",
"(time, node, comm, path, cmdline, cwd, md5, tree, env)",
(
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"{0}:{1}".format(self.proto, self.addr),
self.proc['comm'],
self.proc['path'],
" ".join(self.proc['args']),
self.cwd,
self.checksums,
self.tree,
self.env
), action_on_conflict="IGNORE")

View file

@ -141,6 +141,7 @@ class Config:
STATS_RULES_SPLITTER_POS = "statsDialog/rules_splitter_pos"
STATS_VIEW_COL_STATE = "statsDialog/view_columns_state"
STATS_VIEW_DETAILS_COL_STATE = "statsDialog/view_details_columns_state"
STATS_RXTX_UNITS_FORMAT = "statsDialog/rxtx_units_format"
INFOWIN_GEOMETRY = "infoWindow/geometry"

View file

@ -200,6 +200,47 @@ class Database:
")", self.db)
q.exec_()
q = QSqlQuery("create table if not exists rxtx (" \
"id int primary key, " \
"time time, " \
"what int, " \
"proto text, " \
"bytes_sent int, " \
"bytes_recv int, " \
"proc_path_fk text, " \
"FOREIGN KEY(proc_path_fk) REFERENCES procs(path)" \
")", self.db)
q.exec_()
q = QSqlQuery("CREATE INDEX idx_rxtx_what_path ON rxtx (what, proc_path_fk)", self.db)
q.exec_()
q = QSqlQuery("CREATE INDEX idx_rxtx_bytes_sent ON rxtx (bytes_sent)", self.db)
q.exec_()
q = QSqlQuery("CREATE INDEX idx_rxtx_bytes_recv ON rxtx (bytes_recv)", self.db)
q.exec_()
q = QSqlQuery("create table if not exists proc_details (" \
"time time, " \
"node text, " \
"path text, " \
#"hostid text, " \ # container host/id ??
"comm text, " \
"cmdline text, " \
"cwd text, " \
"md5 text, " \
"tree text, " \
"env text, " \
"proc_path_fk text, " \
"FOREIGN KEY(proc_path_fk) REFERENCES procs(what)" \
# what should we consider unique?
# node+path prevents from registering different executions of
# the same binary. A scenario where this could be useful is
# when a process is launched with different environment
# variables.
"UNIQUE(node, path)" \
")", self.db)
q.exec_()
q = QSqlQuery("create index rules_index on rules (time)", self.db)
q.exec_()
@ -300,9 +341,12 @@ class Database:
q = QSqlQuery("PRAGMA optimize;", self.db)
q.exec_()
def clean(self, table):
def clean(self, table, where=None):
qstr = "DELETE FROM " + table
if where is not None:
qstr += " WHERE " + where
with self._lock:
q = QSqlQuery("delete from " + table, self.db)
q = QSqlQuery(qstr, self.db)
q.exec_()
def vacuum(self):
@ -358,6 +402,7 @@ class Database:
if oldt == None or newt == None or oldt == 0 or newt == 0:
return -1
rows_deleted = 0
oldest = datetime.strptime(oldt, "%Y-%m-%d %H:%M:%S.%f")
newest = datetime.strptime(newt, "%Y-%m-%d %H:%M:%S.%f")
diff = newest - oldest
@ -368,8 +413,30 @@ class Database:
q.prepare("DELETE FROM connections WHERE time < ?")
q.bindValue(0, str(date_to_purge))
if q.exec_():
print("purge_oldest() {0} records deleted".format(q.numRowsAffected()))
return q.numRowsAffected()
print("purge_oldest() connections: {0} records deleted".format(q.numRowsAffected()))
rows_deleted += q.numRowsAffected()
else:
print(q.lastError().driverText())
# XXX: delete really the alerts? There shouldn't be that many
q = QSqlQuery(self.db)
q.prepare("DELETE FROM alerts WHERE time < ?")
q.bindValue(0, str(date_to_purge))
if q.exec_():
print("purge_oldest() alerts: {0} records deleted".format(q.numRowsAffected()))
rows_deleted += q.numRowsAffected()
q = QSqlQuery(self.db)
q.prepare("DELETE FROM rxtx WHERE time < ?")
q.bindValue(0, str(date_to_purge))
if q.exec_():
print("purge_oldest() rxtx: {0} records deleted".format(q.numRowsAffected()))
rows_deleted += q.numRowsAffected()
return rows_deleted
except Exception as e:
print("db, purge_oldest() error:", e)
@ -405,7 +472,7 @@ class Database:
for idx, v in enumerate(columns):
q.bindValue(idx, v)
if q.exec_():
return True
return True, q.lastInsertId()
else:
print("_insert() ERROR", query_str)
print(q.lastError().driverText())
@ -415,7 +482,7 @@ class Database:
finally:
q.finish()
return False
return False, -1
def insert(self, table, fields, columns, update_field=None, update_values=None, action_on_conflict="REPLACE"):
if update_field != None:
@ -684,7 +751,7 @@ class Database:
def get_alert(self, alert_time, node_addr=None):
"""
get alert, given the time of the alert and the node
get an alert, given the time of the alert and the node
"""
qstr = "SELECT * FROM alerts WHERE time=?"
if node_addr != None:
@ -698,3 +765,44 @@ class Database:
q.exec_()
return q
def get_process(self, path, node_addr=None):
qstr = "SELECT * FROM process WHERE path=?"
if node_addr != None:
qstr = qstr + " AND node=?"
q = QSqlQuery(qstr, self.db)
q.prepare(qstr)
q.addBindValue(path)
if node_addr != None:
q.addBindValue(node_addr)
q.exec_()
return q
def get_process_bytes(self, path, get_totals=True, node_addr=None):
qstr = "SELECT sum(bytes_sent) as bytes_sent, sum(bytes_recv) as bytes_recv FROM process WHERE path=?"
if not get_totals:
qstr = "SELECT bytes_sent, bytes_recv FROM process WHERE path=?"
if node_addr != None:
qstr = qstr + " AND node=?"
q = QSqlQuery(qstr, self.db)
q.prepare(qstr)
q.addBindValue(path)
if node_addr != None:
q.addBindValue(node_addr)
q.exec_()
return q
def reset_rxtx_stats(self, field, object):
"""Reset rxtx stats to 0 (defined by the 'field' parameter).
The object will be used to specify what to reset (process, conns...)
"""
self.update(
table="rxtx",
fields="{0}=?".format(field),
values=[0],
condition="what == {0}".format(object),
action_on_conflict="")

View file

@ -44,6 +44,23 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
LIMITS = ["LIMIT 50", "LIMIT 100", "LIMIT 200", "LIMIT 300", ""]
LAST_GROUP_BY = ""
RXTX_BYTES = "bytes"
RXTX_UNITS = "units"
RXTX_FILTER_BY_BYTES = "rt.total_sent as Tx, rt.total_recv as Rx"
RXTX_FILTER_BY_UNITS = """CASE
WHEN rt.total_sent >= 1024 AND rt.total_sent <= 1048576 THEN (ROUND(rt.total_sent / 1024, 2)) || ' KBytes'
WHEN rt.total_sent > 1048576 AND rt.total_sent < 1073741824 THEN (ROUND(rt.total_sent / 1048576, 2)) || ' MBytes'
WHEN rt.total_sent >= 1073741824 THEN (ROUND(rt.total_sent / 1073741824, 2)) || ' GBytes'
ELSE rt.total_sent
END AS Tx,
CASE
WHEN rt.total_recv >= 1024 AND rt.total_recv <= 1048576 THEN (ROUND(rt.total_recv / 1024, 2)) || ' KBytes'
WHEN rt.total_recv > 1048576 AND rt.total_recv < 1073741824 THEN (ROUND(rt.total_recv / 1048576, 2)) || ' MBytes'
WHEN rt.total_recv >= 1073741824 THEN (ROUND(rt.total_recv / 1073741824, 2)) || ' GBytes'
ELSE rt.total_recv
END AS Rx"""
# general
COL_TIME = 0
COL_NODE = 1
@ -264,7 +281,26 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
"filterLine": None,
"model": None,
"delegate": "commonDelegateConfig",
"display_fields": "*",
"display_fields": "",
"bytes_units": RXTX_FILTER_BY_UNITS,
"custom_query": """
WITH rxtx_totals AS (
SELECT
proc_path_fk,
what,
total(bytes_sent) AS total_sent,
total(bytes_recv) AS total_recv
FROM rxtx
WHERE what = 0
GROUP BY proc_path_fk, what
)
SELECT
procs.what,
procs.hits,
#UNITS#
FROM procs
JOIN rxtx_totals rt ON procs.what = rt.proc_path_fk""",
"header_labels": [],
"last_order_by": "2",
"last_order_to": 1,
@ -371,6 +407,13 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._actions = Actions().instance()
self._actions.loadAll()
self._last_update = datetime.datetime.now()
self._last_rxtx_update = datetime.datetime.now()
self._last_rxtx = {'sent': 0, 'recv': 0}
# rxtx timer to reset statusbar stats after 3s if we haven't received
# new bytes stats.
self._rxtx_timer = QtCore.QTimer()
self._rxtx_timer.setInterval(2000)
self._rxtx_timer.timeout.connect(self._rxtx_timer_callback)
# TODO: allow to display multiples dialogs
self._proc_details_dialog = ProcessDetailsDialog(appicon=appicon)
@ -493,10 +536,15 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
]
self.TABLES[self.TAB_HOSTS]['header_labels'] = stats_headers
self.TABLES[self.TAB_PROCS]['header_labels'] = stats_headers
self.TABLES[self.TAB_ADDRS]['header_labels'] = stats_headers
self.TABLES[self.TAB_PORTS]['header_labels'] = stats_headers
self.TABLES[self.TAB_USERS]['header_labels'] = stats_headers
self.TABLES[self.TAB_PROCS]['header_labels'] = [
QC.translate("stats", "What", "This is a word, without spaces and symbols.").replace(" ", ""),
QC.translate("stats", "Hits", "This is a word, without spaces and symbols.").replace(" ", ""),
'Tx',
'Rx'
]
self.TABLES[self.TAB_MAIN]['view'] = self._setup_table(QtWidgets.QTableView, self.eventsTable, "connections",
self.TABLES[self.TAB_MAIN]['display_fields'],
@ -557,11 +605,13 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.TABLES[self.TAB_PROCS]['view'] = self._setup_table(QtWidgets.QTableView,
self.procsTable, "procs",
model=GenericTableModel("procs", self.TABLES[self.TAB_PROCS]['header_labels']),
fields="",
verticalScrollBar=self.procsScrollBar,
resize_cols=(self.COL_WHAT,),
delegate=self.TABLES[self.TAB_PROCS]['delegate'],
order_by="2",
limit=self._get_limit()
limit=self._get_limit(),
custom_query=self.TABLES[self.TAB_PROCS]['custom_query'].replace("#UNITS#", self.TABLES[self.TAB_PROCS]['bytes_units'])
)
self.TABLES[self.TAB_ADDRS]['view'] = self._setup_table(QtWidgets.QTableView,
self.addrTable, "addrs",
@ -631,6 +681,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.TABLES[self.TAB_FIREWALL]['view'].customContextMenuRequested.connect(self._cb_table_context_menu)
self.TABLES[self.TAB_ALERTS]['view'].setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.TABLES[self.TAB_ALERTS]['view'].customContextMenuRequested.connect(self._cb_table_context_menu)
self.TABLES[self.TAB_ALERTS]['view'].resizeRowsToContents()
for idx in range(1,10):
if self.TABLES[idx]['cmd'] != None:
@ -683,6 +734,18 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.fwTreeEdit.clicked.connect(self._cb_tree_edit_firewall_clicked)
self._configure_buttons_icons()
def _rxtx_timer_callback(self):
"""Reset rxtx stats if we haven't received new bytes stats in the latest
2 seconds.
Whenever we receive a new bytes event, the timer is resetted.
"""
self._rxtx_timer.start()
self._last_rxtx = {
'sent': 0,
'recv': 0
}
self.rxtxLabel.setText(" 🡅 0 🡇 0")
#Sometimes a maximized window which had been minimized earlier won't unminimize
#To workaround, we explicitely maximize such windows when unminimizing happens
def changeEvent(self, event):
@ -821,6 +884,12 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
if dialog_general_filter_text != None:
self.filterLine.setText(dialog_general_filter_text)
rxtx_units = self._cfg.getSettings(Config.STATS_RXTX_UNITS_FORMAT)
if rxtx_units == self.RXTX_BYTES:
self.TABLES[self.TAB_PROCS]['bytes_units'] = self.RXTX_FILTER_BY_BYTES
else:
self.TABLES[self.TAB_PROCS]['bytes_units'] = self.RXTX_FILTER_BY_UNITS
def _save_settings(self):
self._cfg.setSettings(Config.STATS_GEOMETRY, self.saveGeometry())
self._cfg.setSettings(Config.STATS_LAST_TAB, self.tabWidget.currentIndex())
@ -927,6 +996,41 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._clear_rows_selection()
return True
def _configure_headers_procs_contextual_menu(self, pos, cur_idx):
try:
table = self._get_active_table()
point = QtCore.QPoint(pos.x()+10, pos.y()+5)
menu = QtWidgets.QMenu(self)
_menu_units_reset_rx = menu.addAction(QC.translate("stats", "Reset Rx stats"))
_menu_units_reset_tx = menu.addAction(QC.translate("stats", "Reset Tx stats"))
unitsMenu = QtWidgets.QMenu(QC.translate("stats", "Format"))
_menu_units_bytes = unitsMenu.addAction(QC.translate("stats", "Bytes"))
_menu_units_group = unitsMenu.addAction(QC.translate("stats", "Group by units"))
menu.addMenu(unitsMenu)
action = menu.exec_(table.mapToGlobal(point))
if action == _menu_units_reset_rx:
self._db.reset_rxtx_stats("bytes_sent", 0)
elif action == _menu_units_reset_tx:
self._db.reset_rxtx_stats("bytes_recv", 0)
elif action == _menu_units_bytes:
self._cfg.setSettings(Config.STATS_RXTX_UNITS_FORMAT, self.RXTX_BYTES)
self.TABLES[cur_idx]['bytes_units'] = self.RXTX_FILTER_BY_BYTES
elif action == _menu_units_group:
self._cfg.setSettings(Config.STATS_RXTX_UNITS_FORMAT, self.RXTX_UNITS)
self.TABLES[cur_idx]['bytes_units'] = self.RXTX_FILTER_BY_UNITS
view = self.TABLES[self.TAB_PROCS]['view']
model = view.model()
qstr = self.TABLES[self.TAB_PROCS]['custom_query'].replace("#UNITS#", self.TABLES[cur_idx]['bytes_units']) \
+ self._get_order() + self._get_limit()
self.setQuery(model, qstr)
except Exception as e:
print("config procs headers exception:", e)
def _configure_fwrules_contextual_menu(self, pos):
try:
cur_idx = self.tabWidget.currentIndex()
@ -1495,6 +1599,14 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._refresh_active_table()
def _cb_headers_context_menu(self, pos):
cur_idx = self.tabWidget.currentIndex()
#if cur_idx == self.TAB_MAIN:
# self._configure_headers_main_contextual_menu(pos, cur_idx)
if cur_idx == self.TAB_PROCS:
self._configure_headers_procs_contextual_menu(pos, cur_idx)
def _cb_table_context_menu(self, pos):
cur_idx = self.tabWidget.currentIndex()
if cur_idx != self.TAB_RULES and cur_idx != self.TAB_MAIN:
@ -1560,8 +1672,14 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
else:
where_clause = self._get_filter_line_clause(cur_idx, text)
qstr = self._db.get_query( self.TABLES[cur_idx]['name'], self.TABLES[cur_idx]['display_fields'] ) + \
where_clause + self._get_order()
if self.TABLES[cur_idx].get('custom_query') != None:
qstr = self.TABLES[cur_idx]['custom_query']
if cur_idx == StatsDialog.TAB_PROCS:
qstr = self.TABLES[self.TAB_PROCS]['custom_query'].replace("#UNITS#", self.TABLES[cur_idx]['bytes_units'])
else:
qstr = self._db.get_query( self.TABLES[cur_idx]['name'], self.TABLES[cur_idx]['display_fields'] )
qstr += where_clause + self._get_order()
if text == "":
qstr = qstr + self._get_limit()
@ -1594,6 +1712,10 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
cur_idx = self.tabWidget.currentIndex()
if cur_idx == StatsDialog.TAB_RULES:
self._db.empty_rule(self.TABLES[cur_idx]['label'].text())
elif cur_idx == StatsDialog.TAB_PROCS:
self._db.clean(self.TABLES[cur_idx]['name'])
self._db.clean("proc_details")
self._db.clean("rxtx", "what == 0")
elif self.IN_DETAIL_VIEW[cur_idx]:
self._del_by_field(cur_idx, self.TABLES[cur_idx]['name'], self.TABLES[cur_idx]['label'].text())
else:
@ -1618,11 +1740,18 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
filter_text = self.TABLES[cur_idx]['filterLine'].text()
where_clause = self._get_filter_line_clause(cur_idx, filter_text)
self.setQuery(model,
self._db.get_query(
if self.TABLES[cur_idx].get('custom_query') != None:
qstr = self.TABLES[cur_idx]['custom_query']
if cur_idx == StatsDialog.TAB_PROCS:
qstr = self.TABLES[self.TAB_PROCS]['custom_query'].replace("#UNITS#", self.TABLES[cur_idx]['bytes_units'])
else:
qstr = self._db.get_query(
self.TABLES[cur_idx]['name'],
self.TABLES[cur_idx]['display_fields']) + where_clause + " " + self._get_order() + self._get_limit()
)
self.TABLES[cur_idx]['display_fields'])
qstr += where_clause + self._get_order() + self._get_limit()
self.setQuery(model, qstr)
finally:
self._restore_details_view_columns(
self.TABLES[cur_idx]['view'].horizontalHeader(),
@ -2547,6 +2676,10 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
nrows = self._get_active_table().model().rowCount()
self.cmdProcDetails.setVisible(nrows != 0)
records = self._db.get_process_bytes(data)
if records != None and records.next():
labelText = "{0} - sent: {1}, recv: {2}".format(data, records.value(0), records.value(1))
self.procsLabel.setText(labelText)
def _set_addrs_query(self, data):
model = self._get_active_table().model()
@ -2846,7 +2979,21 @@ 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,
custom_query=None):
tableWidget.setSortingEnabled(True)
if model == None:
model = self._db.get_new_qsql_model()
@ -2856,7 +3003,11 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
tableWidget.verticalScrollBar().sliderReleased.connect(self._cb_scrollbar_released)
tableWidget.setTrackingColumn(tracking_column)
self.setQuery(model, "SELECT " + fields + " FROM " + table_name + group_by + " ORDER BY " + order_by + " " + sort_direction + limit)
query_tail = group_by + " ORDER BY " + order_by + " " + sort_direction + limit
if custom_query != None:
self.setQuery(model, custom_query + query_tail)
else:
self.setQuery(model, "SELECT " + fields + " FROM " + table_name + query_tail)
tableWidget.setModel(model)
if delegate != None:
@ -2866,6 +3017,8 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
header = tableWidget.horizontalHeader()
if header != None:
header.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
header.customContextMenuRequested.connect(self._cb_headers_context_menu)
header.sortIndicatorChanged.connect(self._cb_table_header_clicked)
for _, col in enumerate(resize_cols):
@ -2936,6 +3089,37 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
if need_query_update and not self._are_rows_selected():
self._refresh_active_table()
@QtCore.pyqtSlot(int, int)
def on_bytes_updated(self, sent, recv):
"""Display rxtx stats on the statusbar
"""
# start/reset the timer
self._rxtx_timer.start()
self._last_rxtx = {
'sent': self._last_rxtx['sent'] + sent,
'recv': self._last_rxtx['recv'] + recv
}
tx_units = ""
rx_units = ""
diff = datetime.datetime.now() - self._last_rxtx_update
if diff.seconds < self._ui_refresh_interval:
return
if self._last_rxtx['sent'] > 1024:
tx_units = "KB"
sent = round(float(self._last_rxtx['sent'] / 1024), 2)
if self._last_rxtx['recv'] > 1024:
rx_units = "KB"
recv = round(float(self._last_rxtx['recv'] / 1024), 2)
self.rxtxLabel.setText(" 🡅 {0} {1} 🡇 {2} {3}".format(sent, tx_units, recv, rx_units))
self._last_rxtx = {
'sent': 0,
'recv': 0
}
self._last_rxtx_update = datetime.datetime.now()
# prevent a click on the window's x
# from quitting the whole application
def closeEvent(self, e):

View file

@ -286,6 +286,7 @@
</layout>
</item>
<item>
<widget class="QFrame" name="frame_3">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
@ -329,6 +330,7 @@
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
@ -504,9 +506,6 @@
</item>
<item>
<widget class="GenericTableView" name="alertsTable">
<property name="editTriggers">
<set>QAbstractItemView::AnyKeyPressed|QAbstractItemView::EditKeyPressed</set>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
@ -1206,7 +1205,8 @@
</property>
<property name="font">
<font>
<pointsize>8</pointsize>
<family>DejaVu Sans</family>
<pointsize>9</pointsize>
<kerning>true</kerning>
</font>
</property>
@ -1258,7 +1258,8 @@
</property>
<property name="font">
<font>
<pointsize>8</pointsize>
<family>DejaVu Sans</family>
<pointsize>9</pointsize>
<kerning>true</kerning>
</font>
</property>
@ -1310,7 +1311,8 @@
</property>
<property name="font">
<font>
<pointsize>8</pointsize>
<family>DejaVu Sans</family>
<pointsize>9</pointsize>
<kerning>true</kerning>
</font>
</property>
@ -1362,7 +1364,8 @@
</property>
<property name="font">
<font>
<pointsize>8</pointsize>
<family>DejaVu Sans</family>
<pointsize>9</pointsize>
<kerning>true</kerning>
</font>
</property>
@ -1377,6 +1380,13 @@
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="rxtxLabel">
<property name="text">
<string>-</string>
</property>
</widget>
</item>
</layout>
</item>
<item>

View file

@ -18,6 +18,7 @@ from opensnitch import ui_pb2_grpc
from opensnitch.dialogs.prompt import PromptDialog
from opensnitch.dialogs.stats import StatsDialog
from opensnitch.alerts import alert, rxtx
from opensnitch.notifications import DesktopNotifications
from opensnitch.firewall import Rules as FwRules
from opensnitch.nodes import Nodes
@ -37,6 +38,7 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
_status_change_trigger = QtCore.pyqtSignal(bool)
_notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
_show_message_trigger = QtCore.pyqtSignal(str, str, int, int)
_bytes_updated_trigger = QtCore.pyqtSignal(int, int)
# .desktop filename located under /usr/share/applications/
DESKTOP_FILENAME = "opensnitch_ui.desktop"
@ -150,6 +152,7 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
self._stats_dialog._status_changed_trigger.connect(self._on_stats_status_changed)
self._stats_dialog.settings_saved.connect(self._on_settings_saved)
self._stats_dialog.close_trigger.connect(self._on_close)
self._bytes_updated_trigger.connect(self._stats_dialog.on_bytes_updated)
self._show_message_trigger.connect(self._show_systray_message)
def _setup_icons(self):
@ -301,48 +304,29 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
self._stats_dialog.update(is_local_request, request.stats, main_need_refresh or details_need_refresh)
@QtCore.pyqtSlot(str, str, ui_pb2.Alert)
def _on_new_alert(self, proto, addr, alert):
# TODO: move to its own module
def _on_new_alert(self, proto, addr, pb_alert):
is_local_request = self._is_local_request(proto, addr)
try:
is_local = self._is_local_request(proto, addr)
what = "GENERIC"
body = alert.text
if alert.what == ui_pb2.Alert.KERNEL_EVENT:
body = "%s\n%s" % (alert.text, alert.proc.path)
what = "KERNEL EVENT"
if is_local is False:
body = "node: {0}:{1}\n\n{2}\n{3}".format(proto, addr, alert.text, alert.proc.path)
if alert.action == ui_pb2.Alert.SHOW_ALERT:
icon = QtWidgets.QSystemTrayIcon.Information
_title = QtCore.QCoreApplication.translate("messages", "Info")
atype = "INFO"
if alert.type == ui_pb2.Alert.ERROR:
atype = "ERROR"
_title = QtCore.QCoreApplication.translate("messages", "Error")
icon = QtWidgets.QSystemTrayIcon.Critical
if alert.type == ui_pb2.Alert.WARNING:
atype = "WARNING"
_title = QtCore.QCoreApplication.translate("messages", "Warning")
icon = QtWidgets.QSystemTrayIcon.Warning
urgency = DesktopNotifications.URGENCY_NORMAL
if alert.priority == ui_pb2.Alert.LOW:
urgency = DesktopNotifications.URGENCY_LOW
elif alert.priority == ui_pb2.Alert.HIGH:
urgency = DesktopNotifications.URGENCY_CRITICAL
if pb_alert.what == ui_pb2.Alert.GENERIC:
al = alerts.Alert(proto, addr, is_local_request, pb_alert)
if pb_alert.action == ui_pb2.Alert.SHOW_ALERT:
_title, body, icon, urgency = al.build()
self._show_message_trigger.emit(_title, body, icon, urgency)
self._db.insert("alerts",
"(time, node, type, action, priority, what, body, status)",
(
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
proto+":"+addr, atype, "", "", what, body, 0
))
if pb_alert.action == ui_pb2.Alert.SAVE_TO_DB:
al.save()
# proc_exit/rxtx events are not saved to the alerts table
if pb_alert.what == ui_pb2.Alert.KERNEL_NET_RXTX:
rxtxAlert = rxtx.RxTx(proto, addr, pb_alert)
if pb_alert.action == ui_pb2.Alert.SAVE_TO_DB:
rxtxAlert.save()
self._bytes_updated_trigger.emit(rxtxAlert.bytesSent, rxtxAlert.bytesRecv)
self._stats_dialog.update(is_local_request, None, True)
else:
print("PostAlert() unknown alert action:", alert.action)
print("PostAlert() unknown alert action:", pb_alert.text)
except Exception as e: