diff --git a/daemon/procmon/details.go b/daemon/procmon/details.go index 3090e4c6..b17a216e 100644 --- a/daemon/procmon/details.go +++ b/daemon/procmon/details.go @@ -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//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//root/ - 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 } diff --git a/daemon/procmon/ebpf/ebpf.go b/daemon/procmon/ebpf/ebpf.go index 5ad83f71..45310f75 100644 --- a/daemon/procmon/ebpf/ebpf.go +++ b/daemon/procmon/ebpf/ebpf.go @@ -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 /include/uapi/linux/bpf.h +// mimics union bpf_attr's anonymous struct used by BPF_MAP_*_ELEM commands +// from /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{}) { diff --git a/daemon/procmon/ebpf/events.go b/daemon/procmon/ebpf/events.go index e8bd281f..4e0cc662 100644 --- a/daemon/procmon/ebpf/events.go +++ b/daemon/procmon/ebpf/events.go @@ -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) - continue + 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 event.Type { + 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 + } + 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)) } diff --git a/daemon/procmon/ebpf/find.go b/daemon/procmon/ebpf/find.go index 0890e1c3..9caf98e3 100644 --- a/daemon/procmon/ebpf/find.go +++ b/daemon/procmon/ebpf/find.go @@ -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. diff --git a/daemon/procmon/ebpf/utils.go b/daemon/procmon/ebpf/utils.go index 874e8586..b392bd46 100644 --- a/daemon/procmon/ebpf/utils.go +++ b/daemon/procmon/ebpf/utils.go @@ -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) diff --git a/daemon/procmon/process.go b/daemon/procmon/process.go index d1edc97a..54971df1 100644 --- a/daemon/procmon/process.go +++ b/daemon/procmon/process.go @@ -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,34 +80,30 @@ 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 - Statm *procStatm - Parent *Process - IOStats *procIOstats - NetStats *procNetStats - Env map[string]string - Checksums map[string]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 + mu *sync.RWMutex + Statm *procStatm + Parent *Process + 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 // Path is the absolute path to the binary Path string @@ -96,8 +111,8 @@ type Process struct { // RealPath is the path to the binary taking into account its root fs. // The simplest form of accessing the RealPath is by prepending /proc//root/ to the path: // /usr/bin/curl -> /proc//root/usr/bin/curl - RealPath string - CWD string + RealPath 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,21 +247,34 @@ 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), - Uid: uint64(p.UID), - Comm: p.Comm, - Path: p.Path, - Args: p.Args, - Env: p.Env, - Cwd: p.CWD, - Checksums: p.Checksums, - IoReads: uint64(ioStats.RChar), - IoWrites: uint64(ioStats.WChar), - NetReads: netStats.ReadBytes, - NetWrites: netStats.WriteBytes, - ProcessTree: p.Tree, + Pid: uint64(p.ID), + Ppid: uint64(p.PPID), + Uid: uint64(p.UID), + Comm: p.Comm, + Path: p.Path, + Args: p.Args, + Env: p.Env, + Cwd: p.CWD, + Checksums: p.Checksums, + IoReads: uint64(ioStats.RChar), + IoWrites: uint64(ioStats.WChar), + NetReads: netStats.ReadBytes, + NetWrites: netStats.WriteBytes, + BytesSent: bsent, + BytesRecv: brecv, + Tree: p.Tree, } } diff --git a/daemon/rule/operator.go b/daemon/rule/operator.go index 0634c226..3f6c53e4 100644 --- a/daemon/rule/operator.go +++ b/daemon/rule/operator.go @@ -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] diff --git a/daemon/ui/alerts.go b/daemon/ui/alerts.go index 1496979a..24d2d4e3 100644 --- a/daemon/ui/alerts.go +++ b/daemon/ui/alerts.go @@ -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) { diff --git a/daemon/ui/client.go b/daemon/ui/client.go index 68354c99..70dadf46 100644 --- a/daemon/ui/client.go +++ b/daemon/ui/client.go @@ -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)) } - c.alertsChan <- *NewAlert(atype, awhat, action, prio, data) + switch data.(type) { + case *protocol.Alert: + c.alertsChan <- *(data.(*protocol.Alert)) + default: + c.alertsChan <- *NewAlert(atype, awhat, action, prio, data) + } } func (c *Client) monitorConfigWorker() { diff --git a/daemon/ui/config_utils.go b/daemon/ui/config_utils.go index dadd2fe3..c67e86b8 100644 --- a/daemon/ui/config_utils.go +++ b/daemon/ui/config_utils.go @@ -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 { diff --git a/ebpf_prog/common.h b/ebpf_prog/common.h index 70d3dea9..bdbfa7b3 100644 --- a/ebpf_prog/common.h +++ b/ebpf_prog/common.h @@ -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,14 +103,50 @@ 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 struct bpf_map_def SEC("maps/heapstore") heapstore = { - .type = BPF_MAP_TYPE_PERCPU_ARRAY, - .key_size = sizeof(u32), - .value_size = sizeof(struct data_t), - .max_entries = 1 + .type = BPF_MAP_TYPE_PERCPU_ARRAY, + .key_size = sizeof(u32), + .value_size = sizeof(struct data_t), + .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 diff --git a/ebpf_prog/opensnitch-procs.c b/ebpf_prog/opensnitch-procs.c index 2da48f7c..3b33a5f9 100644 --- a/ebpf_prog/opensnitch-procs.c +++ b/ebpf_prog/opensnitch-procs.c @@ -1,6 +1,7 @@ #define KBUILD_MODNAME "opensnitch-procs" #include "common.h" +#include struct bpf_map_def SEC("maps/proc-events") events = { // Since kernel 4.4 @@ -11,10 +12,10 @@ 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), - .value_size = sizeof(struct data_t), - .max_entries = 256, + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(u64), + .value_size = sizeof(struct data_t), + .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; }; @@ -129,7 +287,7 @@ int tracepoint__syscalls_sys_enter_execve(struct trace_sys_enter_execve* ctx) #else // in case of failure adding the item to the map, send it directly u64 pid_tgid = bpf_get_current_pid_tgid(); - if (bpf_map_update_elem(&execMap, &pid_tgid, data, BPF_ANY) != 0) { + if (bpf_map_update_elem(&execMap, &pid_tgid, data, BPF_ANY) != 0) { // With some commands, this helper fails with error -28 (ENOSPC). Misleading error? cmd failed maybe? // BUG: after coming back from suspend state, this helper fails with error -95 (EOPNOTSUPP) @@ -180,7 +338,7 @@ int tracepoint__syscalls_sys_enter_execveat(struct trace_sys_enter_execveat* ctx #else // in case of failure adding the item to the map, send it directly u64 pid_tgid = bpf_get_current_pid_tgid(); - if (bpf_map_update_elem(&execMap, &pid_tgid, data, BPF_ANY) != 0) { + if (bpf_map_update_elem(&execMap, &pid_tgid, data, BPF_ANY) != 0) { // With some commands, this helper fails with error -28 (ENOSPC). Misleading error? cmd failed maybe? // BUG: after coming back from suspend state, this helper fails with error -95 (EOPNOTSUPP) @@ -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 diff --git a/proto/ui.proto b/proto/ui.proto index 3ba8e202..e8ed25f8 100644 --- a/proto/ui.proto +++ b/proto/ui.proto @@ -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 bytes_sent = 15; + map 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 process_env = 12; - map process_checksums = 13; - repeated StringInt process_tree = 14; + uint64 process_bytessent = 11; + uint64 process_bytesrecv = 12; + repeated string process_args = 13; + map process_env = 14; + map process_checksums = 15; + repeated StringInt process_tree = 16; } message Operator { diff --git a/ui/opensnitch/alerts/__init__.py b/ui/opensnitch/alerts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ui/opensnitch/alerts/_utils.py b/ui/opensnitch/alerts/_utils.py new file mode 100644 index 00000000..06537b62 --- /dev/null +++ b/ui/opensnitch/alerts/_utils.py @@ -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 + diff --git a/ui/opensnitch/alerts/alert.py b/ui/opensnitch/alerts/alert.py new file mode 100644 index 00000000..057fccc4 --- /dev/null +++ b/ui/opensnitch/alerts/alert.py @@ -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 + )) diff --git a/ui/opensnitch/alerts/rxtx.py b/ui/opensnitch/alerts/rxtx.py new file mode 100644 index 00000000..58c34f8a --- /dev/null +++ b/ui/opensnitch/alerts/rxtx.py @@ -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") diff --git a/ui/opensnitch/config.py b/ui/opensnitch/config.py index e57c778e..0767107b 100644 --- a/ui/opensnitch/config.py +++ b/ui/opensnitch/config.py @@ -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" diff --git a/ui/opensnitch/database/__init__.py b/ui/opensnitch/database/__init__.py index c9deee7a..345d0a19 100644 --- a/ui/opensnitch/database/__init__.py +++ b/ui/opensnitch/database/__init__.py @@ -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="") diff --git a/ui/opensnitch/dialogs/stats.py b/ui/opensnitch/dialogs/stats.py index 497ef712..ec9a30ac 100644 --- a/ui/opensnitch/dialogs/stats.py +++ b/ui/opensnitch/dialogs/stats.py @@ -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( - self.TABLES[cur_idx]['name'], - self.TABLES[cur_idx]['display_fields']) + where_clause + " " + self._get_order() + self._get_limit() - ) + 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() + 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): diff --git a/ui/opensnitch/res/stats.ui b/ui/opensnitch/res/stats.ui index a32a3159..18376e82 100644 --- a/ui/opensnitch/res/stats.ui +++ b/ui/opensnitch/res/stats.ui @@ -286,49 +286,51 @@ - - - 0 - - - - - QFrame::NoFrame - - - QFrame::Plain - - - false - - - QAbstractItemView::SelectRows - - - false - - - true - - - true - - - false - - - false - - - - - - - Qt::Vertical - - - - + + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Plain + + + false + + + QAbstractItemView::SelectRows + + + false + + + true + + + true + + + false + + + false + + + + + + + Qt::Vertical + + + + + @@ -504,9 +506,6 @@ - - QAbstractItemView::AnyKeyPressed|QAbstractItemView::EditKeyPressed - QAbstractItemView::SelectRows @@ -1206,7 +1205,8 @@ - 8 + DejaVu Sans + 9 true @@ -1258,7 +1258,8 @@ - 8 + DejaVu Sans + 9 true @@ -1310,7 +1311,8 @@ - 8 + DejaVu Sans + 9 true @@ -1362,7 +1364,8 @@ - 8 + DejaVu Sans + 9 true @@ -1377,6 +1380,13 @@ + + + + - + + + diff --git a/ui/opensnitch/service.py b/ui/opensnitch/service.py index ca00b105..8f6a5441 100644 --- a/ui/opensnitch/service.py +++ b/ui/opensnitch/service.py @@ -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) + 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) - 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 pb_alert.action == ui_pb2.Alert.SAVE_TO_DB: + al.save() - 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 + # 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() - 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 + self._bytes_updated_trigger.emit(rxtxAlert.bytesSent, rxtxAlert.bytesRecv) + self._stats_dialog.update(is_local_request, None, True) - 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 - )) else: - print("PostAlert() unknown alert action:", alert.action) + print("PostAlert() unknown alert action:", pb_alert.text) except Exception as e: