commit 414915edea035738cc314c8ffab7eccf4e608045 Author: chenjiandongx Date: Mon Nov 8 23:34:49 2021 +0800 Init project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5507ea0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# IDE +.idea/ +.vscode/ +.DS_Store + +sniffer* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2e9a8cd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021~present chenjiandongx + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a658d6a --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# sniffer diff --git a/conn_darwin.go b/conn_darwin.go new file mode 100644 index 0000000..f686313 --- /dev/null +++ b/conn_darwin.go @@ -0,0 +1,99 @@ +//go:build freebsd || darwin +// +build freebsd darwin + +package main + +import ( + "bytes" + "context" + "fmt" + "os/exec" + "strconv" + "strings" + "time" +) + +type lsofConn struct { + invoker Invoker +} + +type Invoker struct{} + +func (i Invoker) Command(name string, arg ...string) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + return i.CommandWithContext(ctx, name, arg...) +} + +func (i Invoker) CommandWithContext(ctx context.Context, name string, arg ...string) ([]byte, error) { + cmd := exec.CommandContext(ctx, name, arg...) + + var buf bytes.Buffer + cmd.Stdout = &buf + cmd.Stderr = &buf + + if err := cmd.Start(); err != nil { + return buf.Bytes(), err + } + + if err := cmd.Wait(); err != nil { + return buf.Bytes(), err + } + + return buf.Bytes(), nil +} + +func (lc *lsofConn) GetProcSockets(pid int32) (OpenSockets, error) { return nil, nil } + +func (lc *lsofConn) GetOpenSockets() (OpenSockets, error) { + sockets := make(OpenSockets) + output, err := lc.invoker.Command("lsof", "-n", "-P", "-iTCP", "-iUDP", "-s", "TCP:ESTABLISHED", "+c", "0") + if err != nil { + fmt.Println(err) + return sockets, err + } + + lines := strings.Split(string(output), "\n") + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) < 9 { + continue + } + procName := strings.ReplaceAll(fields[0],"\\x20", " ") + + switch fields[7] { + case "TCP": + addr := strings.Split(fields[8], "->") + if len(addr) != 2 { + continue + } + ipport := strings.Split(addr[0], ":") + if len(ipport) != 2 { + continue + } + port, err := strconv.Atoi(ipport[1]) + if err != nil { + continue + } + sockets[LocalSocket{IP: ipport[0], Port: uint16(port), Protocol: ProtoTCP}] = procName + + case "UDP": + ipport := strings.Split(fields[8], ":") + if len(ipport) != 2 { + continue + } + + port, err := strconv.Atoi(ipport[1]) + if err != nil { + continue + } + sockets[LocalSocket{IP: ipport[0], Port: uint16(port), Protocol: ProtoUDP}] = procName + } + } + + return sockets, nil +} + +func GetSocketFetcher() SocketFetcher { + return &lsofConn{} +} diff --git a/conn_linux.go b/conn_linux.go new file mode 100644 index 0000000..abf0209 --- /dev/null +++ b/conn_linux.go @@ -0,0 +1,396 @@ +//go:build linux +// +build linux + +package main + +import ( + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "net" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" + "unsafe" + + "golang.org/x/sys/unix" +) + +const ( + tcpEstablished = uint8(0x01) + udpConnection = uint8(0x07) + + sizeOfInetDiagRequest = 72 + sockDiagByFamily = 20 +) + +var nativeEndian binary.ByteOrder + +// getNativeEndian gets native endianness for the system +func getNativeEndian() binary.ByteOrder { + if nativeEndian == nil { + var x uint32 = 0x01020304 + if *(*byte)(unsafe.Pointer(&x)) == 0x01 { + nativeEndian = binary.BigEndian + } else { + nativeEndian = binary.LittleEndian + } + } + return nativeEndian +} + +type be16 [2]byte + +// Int be16 to int +func (v be16) Int() int { + v2 := *(*uint16)(unsafe.Pointer(&v)) + return int(v.Swap(v2)) +} + +// Swap swaps a 16 bit value if we aren't big endian +func (v be16) Swap(i uint16) uint16 { + if getNativeEndian() == binary.BigEndian { + return i + } + return (i&0xff00)>>8 | (i&0xff)<<8 +} + +// PortHex parses be16 to hex +func (v be16) PortHex() string { + return hex.EncodeToString(v[0:]) +} + +type be32 [4]byte + +// inetDiagSockID sock_diag +/* inet_diag.h +struct inet_diag_sockid { + __be16 idiag_sport; + __be16 idiag_dport; + __be32 idiag_src[4]; + __be32 idiag_dst[4]; + __u32 idiag_if; + __u32 idiag_cookie[2]; +#define INET_DIAG_NOCOOKIE (~0U) +}; +*/ +type inetDiagSockID struct { + IdiagSport be16 + IdiagDport be16 + IdiagSrc [4]be32 + IdiagDst [4]be32 + IdiagIF uint32 + IdiagCookie [2]uint32 +} + +// inetDiagReqV2 sock_diag +/* inet_diag.h +struct inet_diag_req_v2 { + __u8 sdiag_family; + __u8 sdiag_protocol; + __u8 idiag_ext; + __u8 pad; + __u32 idiag_states; + struct inet_diag_sockid id; +}; +*/ +type inetDiagReqV2 struct { + Family uint8 + Protocol uint8 + Ext uint8 + Pad uint8 + States uint32 + ID inetDiagSockID +} + +// inetDiagMsg receiv msg +/* inet_diag.h +Base info structure. It contains Socket identity (addrs/ports/cookie) and, alas, the information shown by netstat. +struct inet_diag_msg { + __u8 idiag_family; + __u8 idiag_state; + __u8 idiag_timer; + __u8 idiag_retrans; + + struct inet_diag_sockid id; + + __u32 idiag_expires; + __u32 idiag_rqueue; + __u32 idiag_wqueue; + __u32 idiag_uid; + __u32 idiag_inode; +}; +*/ +type inetDiagMsg struct { + IDiagFamily uint8 + IDiagState uint8 + IDiagTimer uint8 + IDiagRetrans uint8 + ID inetDiagSockID + IDiagExpires uint32 + IDiagRqueue uint32 + IDiagWqueue uint32 + IDiagUid uint32 + IDiagInode uint32 +} + +// inetDiagRequest diag_request +/* go/src/syscall/ztypes_linux_amd64.go +type NlMsghdr struct { + Len uint32 + Type uint16 + Flags uint16 + Seq uint32 + Pid uint32 +} +*/ +type inetDiagRequest struct { + Nlh syscall.NlMsghdr + ReqDiag inetDiagReqV2 +} + +type netlinkConn struct{} + +// ipv4 be32 to string +func (nl *netlinkConn) ipv4(b be32) string { + return net.IPv4(b[0], b[1], b[2], b[3]).String() +} + +// ipv6 be32 to string +func (nl *netlinkConn) ipv6(b [4]be32) string { + ip := make(net.IP, net.IPv6len) + for i := 0; i < 4; i++ { + for j := 0; j < 4; j++ { + ip[4*i+j] = b[i][j] + } + } + return ip.String() +} + +// ipHex2String ip hex to string +func (nl *netlinkConn) ipHex2String(family uint8, ip [4]be32) (string, error) { + switch family { + case unix.AF_INET: + return nl.ipv4(ip[0]), nil + case unix.AF_INET6: + return nl.ipv6(ip), nil + default: + return "", errors.New("family is not unix.AF_INET or unix.AF_INET6") + } +} + +// sockdiagSend sends netlinkConn msgs +// see https://github.com/sivasankariit/iproute2/blob/1179ab033c31d2c67f406be5bcd5e4c0685855fe/misc/ss.c#L1575-L1640 +func (nl *netlinkConn) sockdiagSend(proto, family uint8, states uint32) (skfd int, err error) { + if skfd, err = unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW, unix.NETLINK_SOCK_DIAG); err != nil { + return -1, err + } + + var diagReq inetDiagRequest + diagReq.Nlh.Type = sockDiagByFamily + + // man 7 netlinkConn: NLM_F_DUMP Convenience macro; equivalent to (NLM_F_ROOT|NLM_F_MATCH). + diagReq.Nlh.Flags = unix.NLM_F_DUMP | unix.NLM_F_REQUEST + diagReq.ReqDiag.Family = family + diagReq.ReqDiag.Protocol = proto + diagReq.ReqDiag.States = states + diagReq.Nlh.Len = uint32(unsafe.Sizeof(diagReq)) + + buffer := make([]byte, sizeOfInetDiagRequest) + *(*inetDiagRequest)(unsafe.Pointer(&buffer[0])) = diagReq + + sockAddrNl := unix.SockaddrNetlink{Family: syscall.AF_NETLINK} + timeout := syscall.NsecToTimeval((200 * time.Millisecond).Nanoseconds()) + if err = syscall.SetsockoptTimeval(skfd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &timeout); err != nil { + return 0, err + } + + if err = unix.Sendmsg(skfd, buffer, nil, &sockAddrNl, 0); err != nil { + return -1, err + } + return skfd, nil +} + +func (nl *netlinkConn) sockdiagRecv(skfd, proto int, inodeMap map[uint32]string) (map[LocalSocket]string, error) { + ret := make(map[LocalSocket]string) + + buffer := make([]byte, os.Getpagesize()) +loop: + for { + n, _, _, _, err := unix.Recvmsg(skfd, buffer, nil, 0) + if err != nil { + return ret, err + } + + if n == 0 { + break loop + } + + msgs, err := syscall.ParseNetlinkMessage(buffer[:n]) + if err != nil { + return ret, err + } + + for _, msg := range msgs { + if msg.Header.Type == syscall.NLMSG_DONE { + break loop + } + + m := (*inetDiagMsg)(unsafe.Pointer(&msg.Data[0])) + srcIP, _ := nl.ipHex2String(m.IDiagFamily, m.ID.IdiagSrc) + + var p Protocol + switch proto { + case syscall.IPPROTO_TCP: + p = ProtoTCP + case syscall.IPPROTO_UDP: + p = ProtoUDP + } + ret[LocalSocket{IP: srcIP, Port: uint16(m.ID.IdiagSport.Int()), Protocol: p}] = inodeMap[m.IDiagInode] + } + } + + return ret, nil +} + +func (nl *netlinkConn) getOpenSockets(inodeMap map[uint32]string) (map[LocalSocket]string, error) { + ret := make(map[LocalSocket]string) + + type Req struct { + Protocol int + Family uint8 + State uint32 + } + + reqs := []Req{ + {syscall.IPPROTO_TCP, syscall.AF_INET, uint32(1 | 1< ../../gizak/termui diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2dc2717 --- /dev/null +++ b/go.sum @@ -0,0 +1,52 @@ +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/gammazero/deque v0.1.0 h1:f9LnNmq66VDeuAlSAapemq/U7hJ2jpIWa4c09q8Dlik= +github.com/gammazero/deque v0.1.0/go.mod h1:KQw7vFau1hHuM8xmI9RbgKFbAsQFWmBpqQ2KenFLk6M= +github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840= +github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs= +github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= +github.com/shirou/gopsutil v3.21.10+incompatible h1:AL2kpVykjkqeN+MFe1WcwSBVUjGjvdU8/ubvCuXAjrU= +github.com/shirou/gopsutil v3.21.10+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo= +github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= +github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ= +github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71 h1:ikCpsnYR+Ew0vu99XlDp55lGgDJdIMx3f4a18jfse/s= +golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pcap.go b/pcap.go new file mode 100644 index 0000000..2a57be1 --- /dev/null +++ b/pcap.go @@ -0,0 +1,335 @@ +package main + +import ( + "errors" + "strconv" + "strings" + "sync" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcap" +) + +type RemoteSocket struct { + IP string + Port uint16 +} + +type LocalSocket struct { + IP string + Port uint16 + Protocol Protocol +} + +type Connection struct { + Local LocalSocket + Remote RemoteSocket +} + +type OpenSockets map[LocalSocket]string +type Utilization map[Connection]*ConnectionInfo + +type SocketFetcher interface { + GetOpenSockets() (OpenSockets, error) + GetProcSockets(pid int32) (OpenSockets, error) +} + +var defaultDevicePrefix = []string{"en", "lo", "eth", "em", "bond"} + +type Protocol string + +const ( + ProtoTCP Protocol = "tcp" + ProtoUDP Protocol = "udp" +) + +type Direction uint8 + +const ( + DirectionUpload Direction = iota + DirectionDownload +) + +type ConnectionInfo struct { + Interface string + UploadPackets int + DownloadPackets int + UploadBytes int + DownloadBytes int +} + +type Segment struct { + Interface string + DataLen int + Connection Connection + Direction Direction +} + +type pcapHandler struct { + device string + handle *pcap.Handle +} + +type PcapClient struct { + bindIPs map[string]bool + handlers []pcapHandler + bpfFilter string + ch chan []Segment + wg sync.WaitGroup + utilization Utilization + utilmut sync.Mutex +} + +func NewPcapClient(bpfFilter string, devices ...string) (*PcapClient, error) { + client := &PcapClient{ + bindIPs: make(map[string]bool), + handlers: make([]pcapHandler, 0), + ch: make(chan []Segment, 8), + utilization: make(Utilization), + bpfFilter: bpfFilter, + } + + if err := client.getAvailableDevices(devices); err != nil { + return nil, err + } + + go client.consume() + for _, handler := range client.handlers { + go client.listen(handler) + } + + return client, nil +} + +func (c *PcapClient) getAvailableDevices(devices []string) error { + all, err := pcap.FindAllDevs() + if err != nil { + return err + } + + wanted := make([]pcap.Interface, 0) + if len(devices) > 0 { + filter := make(map[string]struct{}) + for _, device := range devices { + filter[device] = struct{}{} + } + for _, device := range all { + _, ok := filter[device.Name] + if !ok { + continue + } + wanted = append(wanted, device) + } + all = wanted + } + + if len(all) == 0 { + return errors.New("no available devices") + } + + for _, device := range all { + // todo: should remove 'any' device here? + if device.Name == "any" { + continue + } + + var found bool + for _, prefix := range defaultDevicePrefix { + if strings.HasPrefix(device.Name, prefix) { + found = true + } + } + if !found { + continue + } + + handler, err := c.getHandler(device.Name, c.bpfFilter) + if err != nil { + return err + } + c.handlers = append(c.handlers, pcapHandler{device: device.Name, handle: handler}) + for _, addr := range device.Addresses { + c.bindIPs[addr.IP.String()] = true + } + } + + return nil +} + +func (c *PcapClient) getHandler(device, bpf string) (*pcap.Handle, error) { + handle, err := pcap.OpenLive(device, 65535, false, pcap.BlockForever) + if err != nil { + return nil, err + } + + if c.bpfFilter != "" { + if err := handle.SetBPFFilter(bpf); err != nil { + handle.Close() + return nil, err + } + } + + return handle, nil +} + +func (c *PcapClient) parsePort(s string) uint16 { + idx := strings.Index(s, "(") + if idx == -1 { + i, _ := strconv.Atoi(s) + return uint16(i) + } + + i, _ := strconv.Atoi(s[:idx]) + return uint16(i) +} + +func (c *PcapClient) parsePacket(device string, packet gopacket.Packet) *Segment { + ipLayer := packet.Layer(layers.LayerTypeIPv4) + if ipLayer == nil { + return nil + } + + ipv4pkg := ipLayer.(*layers.IPv4) + if ipv4pkg == nil { + return nil + } + + var direction = DirectionDownload + srcIP := ipv4pkg.SrcIP.String() + dstIP := ipv4pkg.DstIP.String() + if c.bindIPs[srcIP] { + direction = DirectionUpload + } + + var srcPort, dstPort uint16 + var protocol Protocol + var dataLen int + + tcpLayer := packet.Layer(layers.LayerTypeTCP) + tcpPkg, ok := tcpLayer.(*layers.TCP) + if ok { + srcPort = c.parsePort(tcpPkg.SrcPort.String()) + dstPort = c.parsePort(tcpPkg.DstPort.String()) + protocol = ProtoTCP + dataLen = len(tcpPkg.Payload) + } + + if protocol == "" { + udpLayer := packet.Layer(layers.LayerTypeUDP) + udpPkg, ok := udpLayer.(*layers.UDP) + if ok { + srcPort = c.parsePort(udpPkg.SrcPort.String()) + dstPort = c.parsePort(udpPkg.DstPort.String()) + protocol = ProtoUDP + dataLen = len(udpPkg.Payload) + } + } + + // unknown packets, skip it. + if protocol == "" { + return nil + } + + seg := &Segment{ + Interface: device, + DataLen: dataLen, + Direction: direction, + } + + switch seg.Direction { + case DirectionUpload: + seg.Connection = Connection{ + Local: LocalSocket{IP: srcIP, Port: srcPort, Protocol: protocol}, + Remote: RemoteSocket{IP: dstIP, Port: dstPort}, + } + case DirectionDownload: + seg.Connection = Connection{ + Local: LocalSocket{IP: dstIP, Port: dstPort, Protocol: protocol}, + Remote: RemoteSocket{IP: srcIP, Port: srcPort}, + } + } + + return seg +} + +func (c *PcapClient) listen(ph pcapHandler) { + c.wg.Add(1) + defer c.wg.Done() + + ticker := time.Tick(time.Millisecond * 100) + const batch = 1024 + + packetSource := gopacket.NewPacketSource(ph.handle, ph.handle.LinkType()) + packetSource.Lazy = true + packetSource.NoCopy = true + + var segs []Segment + for { + select { + case <-ticker: + if len(segs) > 0 { + c.ch <- segs + segs = segs[:0] + } + + case packet, ok := <-packetSource.Packets(): + if !ok { + return + } + seg := c.parsePacket(ph.device, packet) + if seg != nil { + segs = append(segs, *seg) + } + if len(segs) >= batch { + c.ch <- segs + segs = segs[:0] + } + } + } +} + +func (c *PcapClient) consume() { + c.wg.Add(1) + defer c.wg.Done() + + for segs := range c.ch { + c.utilmut.Lock() + for _, seg := range segs { + if _, ok := c.utilization[seg.Connection]; !ok { + c.utilization[seg.Connection] = &ConnectionInfo{ + Interface: seg.Interface, + } + } + + switch seg.Direction { + case DirectionUpload: + c.utilization[seg.Connection].UploadBytes += seg.DataLen + c.utilization[seg.Connection].UploadPackets += 1 + + case DirectionDownload: + c.utilization[seg.Connection].DownloadBytes += seg.DataLen + c.utilization[seg.Connection].DownloadPackets += 1 + } + } + c.utilmut.Unlock() + } +} + +func (c *PcapClient) Close() { + for _, handler := range c.handlers { + handler.handle.Close() + } + close(c.ch) + c.wg.Wait() +} + +func (c *PcapClient) GetUtilization() Utilization { + c.utilmut.Lock() + defer c.utilmut.Unlock() + + utilization := c.utilization + c.utilization = make(Utilization) + return utilization +} diff --git a/stat.go b/stat.go new file mode 100644 index 0000000..3a4ebe5 --- /dev/null +++ b/stat.go @@ -0,0 +1,261 @@ +package main + +import ( + "sort" + "sync" + + "github.com/gammazero/deque" +) + +const ( + unknownProcessName = "" +) + +type Stat struct { + OpenSockets OpenSockets + Utilization Utilization +} + +type ConnectionData struct { + DownloadBytes int + UploadBytes int + UploadPackets int + DownloadPackets int + ProcessName string + InterfaceName string +} + +type NetworkData struct { + UploadBytes int + DownloadBytes int + UploadPackets int + DownloadPackets int + ConnCount int + Protocol Protocol +} + +func (d *NetworkData) DivideBy(n int) { + d.UploadBytes /= n + d.DownloadBytes /= n + d.UploadPackets /= n + d.DownloadPackets /= n +} + +func (d *ConnectionData) DivideBy(n int) { + d.UploadBytes /= n + d.DownloadBytes /= n + d.UploadPackets /= n + d.DownloadPackets /= n +} + +type ProcessesResult struct { + ProcessName string + Data *NetworkData +} + +type RemoteAddrsResult struct { + Addr string + Data *NetworkData +} + +type ConnectionsResult struct { + Conn Connection + Data *ConnectionData +} + +type Snapshot struct { + Processes map[string]*NetworkData + RemoteAddrs map[string]*NetworkData + Connections map[Connection]*ConnectionData + TotalUploadBytes int + TotalDownloadBytes int + TotalUploadPackets int + TotalDownloadPackets int +} + +func (s *Snapshot) TopNProcesses(n int, mode RenderMode) []ProcessesResult { + var items []ProcessesResult + for k, v := range s.Processes { + items = append(items, ProcessesResult{ProcessName: k, Data: v}) + } + + switch mode { + case RModeBytes: + sort.Slice(items, func(i, j int) bool { + return items[i].Data.DownloadBytes+items[i].Data.UploadBytes > items[j].Data.DownloadBytes+items[j].Data.UploadBytes + }) + case RModePackets: + sort.Slice(items, func(i, j int) bool { + return items[i].Data.DownloadPackets+items[i].Data.UploadPackets > items[j].Data.DownloadPackets+items[j].Data.UploadPackets + }) + } + + if len(items) < n { + n = len(items) + } + return items[:n] +} + +func (s *Snapshot) TopNRemoteAddrs(n int, mode RenderMode) []RemoteAddrsResult { + var items []RemoteAddrsResult + for k, v := range s.RemoteAddrs { + items = append(items, RemoteAddrsResult{Addr: k, Data: v}) + } + + switch mode { + case RModeBytes: + sort.Slice(items, func(i, j int) bool { + return items[i].Data.DownloadBytes+items[i].Data.UploadBytes > items[j].Data.DownloadBytes+items[j].Data.UploadBytes + }) + case RModePackets: + sort.Slice(items, func(i, j int) bool { + return items[i].Data.DownloadPackets+items[i].Data.UploadPackets > items[j].Data.DownloadPackets+items[j].Data.UploadPackets + }) + } + + if len(items) < n { + n = len(items) + } + return items[:n] +} + +func (s *Snapshot) TopNConnections(n int, mode RenderMode) []ConnectionsResult { + var items []ConnectionsResult + for k, v := range s.Connections { + items = append(items, ConnectionsResult{Conn: k, Data: v}) + } + + switch mode { + case RModeBytes: + sort.Slice(items, func(i, j int) bool { + return items[i].Data.DownloadBytes+items[i].Data.UploadBytes > items[j].Data.DownloadBytes+items[j].Data.UploadBytes + }) + case RModePackets: + sort.Slice(items, func(i, j int) bool { + return items[i].Data.DownloadPackets+items[i].Data.UploadPackets > items[j].Data.DownloadPackets+items[j].Data.UploadPackets + }) + } + + if len(items) < n { + n = len(items) + } + return items[:n] +} + +type StatsManager struct { + mut sync.Mutex + ring *deque.Deque +} + +func NewStatsManager() *StatsManager { + return &StatsManager{ + ring: deque.New(), + } +} + +func (s *StatsManager) Put(stat Stat) { + s.mut.Lock() + defer s.mut.Unlock() + + const maxsize = 5 + if s.ring.Len() >= maxsize { + s.ring.PopFront() + } + s.ring.PushBack(stat) +} + +func (s *StatsManager) getProcName(openSockets OpenSockets, localSocket LocalSocket) string { + ips := []string{localSocket.IP, "*"} + for _, ip := range ips { + cloned := localSocket + cloned.IP = ip + + v, ok := openSockets[cloned] + if ok { + return v + } + } + return unknownProcessName +} + +func (s *StatsManager) GetSnapshot() *Snapshot { + s.mut.Lock() + defer s.mut.Unlock() + + processes := map[string]*NetworkData{} + remoteAddr := map[string]*NetworkData{} + connections := map[Connection]*ConnectionData{} + visited := map[Connection]bool{} + + var totalUploadBytes, totalDownloadBytes, totalUploadPackets, totalDownloadPackets int + + size := s.ring.Len() + if size <= 0 { + return nil + } + + for i := 0; i < size; i++ { + stat := s.ring.At(i).(Stat) + for conn, info := range stat.Utilization { + procName := s.getProcName(stat.OpenSockets, conn.Local) + if _, ok := connections[conn]; !ok { + connections[conn] = &ConnectionData{ + InterfaceName: info.Interface, + ProcessName: procName, + } + } + connections[conn].UploadBytes += info.UploadBytes + connections[conn].DownloadBytes += info.DownloadBytes + connections[conn].UploadPackets += info.UploadPackets + connections[conn].DownloadPackets += info.DownloadPackets + + if _, ok := remoteAddr[conn.Remote.IP]; !ok { + remoteAddr[conn.Remote.IP] = &NetworkData{Protocol: conn.Local.Protocol} + } + if !visited[conn] { + remoteAddr[conn.Remote.IP].ConnCount++ + } + remoteAddr[conn.Remote.IP].UploadBytes += info.UploadBytes + remoteAddr[conn.Remote.IP].DownloadBytes += info.UploadBytes + remoteAddr[conn.Remote.IP].UploadPackets += info.UploadPackets + remoteAddr[conn.Remote.IP].DownloadPackets += info.DownloadPackets + + if _, ok := processes[procName]; !ok { + processes[procName] = &NetworkData{} + } + processes[procName].UploadBytes += info.UploadBytes + processes[procName].DownloadBytes += info.DownloadBytes + processes[procName].UploadPackets += info.UploadPackets + processes[procName].DownloadPackets += info.DownloadPackets + if !visited[conn] { + processes[procName].ConnCount++ + } + + totalUploadPackets += info.UploadPackets + totalDownloadPackets += info.DownloadPackets + totalUploadBytes += info.UploadBytes + totalDownloadBytes += info.DownloadBytes + visited[conn] = true + } + } + + for _, v := range processes { + v.DivideBy(size) + } + for _, v := range remoteAddr { + v.DivideBy(size) + } + for _, v := range connections { + v.DivideBy(size) + } + + return &Snapshot{ + Processes: processes, + RemoteAddrs: remoteAddr, + Connections: connections, + TotalUploadBytes: totalUploadBytes / size, + TotalDownloadBytes: totalDownloadBytes / size, + TotalUploadPackets: totalUploadPackets / size, + TotalDownloadPackets: totalDownloadPackets / size, + } +} diff --git a/ui.go b/ui.go new file mode 100644 index 0000000..6d86051 --- /dev/null +++ b/ui.go @@ -0,0 +1,250 @@ +package main + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/dustin/go-humanize" + "github.com/gizak/termui/v3" + "github.com/gizak/termui/v3/widgets" +) + +const ( + maxRows = 64 +) + +type UIComponent struct { + header *widgets.Paragraph + footer *widgets.Paragraph + processes *widgets.Table + remoteAddrs *widgets.Table + connections *widgets.Table + tableRef []*widgets.Table + grid *termui.Grid + shiftIdx int + mode RenderMode + lookup func(string) string +} + +type RenderMode uint8 + +const ( + RModeBytes RenderMode = iota + RModePackets +) + +func NewUIComponent(lookup func(string) string, mode RenderMode) *UIComponent { + ui := &UIComponent{ + header: newHeader(mode), + footer: newFooter(), + processes: newTable("Process Name"), + remoteAddrs: newTable("Process Name"), + connections: newTable("Connections"), + mode: mode, + lookup: lookup, + } + + ui.tableRef = []*widgets.Table{ui.processes, ui.remoteAddrs, ui.connections} + if err := termui.Init(); err != nil { + panic(err) + } + + width, height := termui.TerminalDimensions() + ui.grid = newGrid(ui.shiftIdx, width, height, ui.header, ui.footer, ui.tableRef) + return ui +} + +func newGrid(shift, width, height int, header, footer *widgets.Paragraph, tables []*widgets.Table) *termui.Grid { + grid := termui.NewGrid() + grid.SetRect(0, 0, width, height) + + num := len(tables) + w := (width) / 12 + tables[(shift+1)%num].ColumnWidths = []int{w * 2, w * 2, (w * 2) - 1} + tables[(shift+2)%num].ColumnWidths = []int{w * 2, w * 2, (w * 2) - 1} + tables[(shift+3)%num].ColumnWidths = []int{w * 6, w * 3, (w * 3) - 1} + + grid.Set( + termui.NewRow(0.03, termui.NewCol(1.0, header)), + termui.NewRow(0.47, + termui.NewCol(1.0/2, tables[(shift+1)%num]), termui.NewCol(1.0/2, tables[(shift+2)%num]), + ), + termui.NewRow(0.47, termui.NewCol(1.0, tables[(shift+3)%num])), + termui.NewRow(0.03, termui.NewCol(1.0, footer)), + ) + + return grid +} + +func newHeader(mode RenderMode) *widgets.Paragraph { + var msg string + switch mode { + case RModeBytes: + msg = "Bytes/s" + case RModePackets: + msg = "Packets/s" + } + + text := fmt.Sprintf("Now: %s Total Up / Down <%s>: 0ps / 0ps", time.Now().Format("15:04:05"), msg) + return newParagraph(text) +} + +func newFooter() *widgets.Paragraph { + text := "Press to pause. Use to rearrange tables" + return newParagraph(text) +} + +func newParagraph(text string) *widgets.Paragraph { + paragraph := widgets.NewParagraph() + paragraph.Text = text + paragraph.Border = false + paragraph.TextStyle = termui.NewStyle(termui.ColorClear) + paragraph.TextStyle.Modifier = termui.ModifierBold + + return paragraph +} + +func newTable(title string) *widgets.Table { + table := widgets.NewTable() + table.Title = fmt.Sprintf("Utilization <%s>", title) + table.RowSeparator = false + table.TextAlignment = termui.AlignLeft + table.TextStyle = termui.NewStyle(termui.ColorClear) + table.BorderStyle = termui.NewStyle(termui.ColorClear) + table.RowStyles = map[int]termui.Style{0: termui.NewStyle(termui.ColorCyan)} + return table +} + +func (ui *UIComponent) humanizeNumber(n int) string { + var s string + switch ui.mode { + case RModeBytes: + s = strings.ReplaceAll(humanize.IBytes(uint64(n)), " ", "") + "ps" + case RModePackets: + s = humanize.Comma(int64(n)) + "ps" + } + return s +} + +func (ui *UIComponent) emptyRow(column int) []string { + return make([]string, column) +} + +func (ui *UIComponent) updateHeader(snapshot *Snapshot) { + now := time.Now().Format("15:04:05") + var up, down, msg string + switch ui.mode { + case RModeBytes: + up = ui.humanizeNumber(snapshot.TotalUploadBytes) + down = ui.humanizeNumber(snapshot.TotalDownloadBytes) + msg = "Bytes/s" + case RModePackets: + up = ui.humanizeNumber(snapshot.TotalUploadPackets) + down = ui.humanizeNumber(snapshot.TotalDownloadPackets) + msg = "Packets/s" + } + ui.header.Text = fmt.Sprintf("Now: %s Total Up / Down <%s>: %s / %s", now, msg, up, down) +} + +func (ui *UIComponent) updateProcesses(snapshot *Snapshot) { + rows := make([][]string, 0) + for _, r := range snapshot.TopNProcesses(maxRows, ui.mode) { + var up, down string + switch ui.mode { + case RModeBytes: + up = ui.humanizeNumber(r.Data.UploadBytes) + down = ui.humanizeNumber(r.Data.DownloadBytes) + case RModePackets: + up = ui.humanizeNumber(r.Data.UploadPackets) + down = ui.humanizeNumber(r.Data.DownloadPackets) + } + rows = append(rows, []string{r.ProcessName, strconv.Itoa(r.Data.ConnCount), up + " / " + down}) + } + + header := []string{"Process", "Connections", "Up / Down"} + ui.processes.Rows = [][]string{header, ui.emptyRow(3)} + ui.processes.Rows = append(ui.processes.Rows, rows...) +} + +func (ui *UIComponent) updateRemoteAddrs(snapshot *Snapshot) { + rows := make([][]string, 0) + for _, r := range snapshot.TopNRemoteAddrs(maxRows, ui.mode) { + var up, down string + switch ui.mode { + case RModeBytes: + up = ui.humanizeNumber(r.Data.UploadBytes) + down = ui.humanizeNumber(r.Data.DownloadBytes) + case RModePackets: + up = ui.humanizeNumber(r.Data.UploadPackets) + down = ui.humanizeNumber(r.Data.DownloadPackets) + } + + // only resolve the TCP IPs + addr := r.Addr + if r.Data.Protocol == ProtoTCP { + addr = ui.lookup(r.Addr) + } + rows = append(rows, []string{addr, strconv.Itoa(r.Data.ConnCount), up + " / " + down}) + } + + header := []string{"Remote Address", "Connections", "Up / Down"} + ui.remoteAddrs.Rows = [][]string{header, ui.emptyRow(3)} + ui.remoteAddrs.Rows = append(ui.remoteAddrs.Rows, rows...) +} + +func (ui *UIComponent) updateConnections(snapshot *Snapshot) { + rows := make([][]string, 0) + for _, r := range snapshot.TopNConnections(maxRows, ui.mode) { + var up, down string + switch ui.mode { + case RModeBytes: + up = ui.humanizeNumber(r.Data.UploadBytes) + down = ui.humanizeNumber(r.Data.DownloadBytes) + case RModePackets: + up = ui.humanizeNumber(r.Data.UploadPackets) + down = ui.humanizeNumber(r.Data.DownloadPackets) + } + + // only resolve the TCP IPs + remoteIP := r.Conn.Remote.IP + if r.Conn.Local.Protocol == ProtoTCP { + remoteIP = ui.lookup(r.Conn.Remote.IP) + } + + conn := fmt.Sprintf("<%s>:%d => %s:%d (%s)", + r.Data.InterfaceName, + r.Conn.Local.Port, + remoteIP, + r.Conn.Remote.Port, + r.Conn.Local.Protocol, + ) + + rows = append(rows, []string{conn, r.Data.ProcessName, up + " / " + down}) + } + + header := []string{"Connections", "Process", "Up / Down"} + ui.connections.Rows = [][]string{header, ui.emptyRow(3)} + ui.connections.Rows = append(ui.connections.Rows, rows...) +} + +func (ui *UIComponent) Shift() { + ui.shiftIdx++ + width, height := termui.TerminalDimensions() + ui.grid = newGrid(ui.shiftIdx, width, height, ui.header, ui.footer, ui.tableRef) + termui.Render(ui.grid) +} + +func (ui *UIComponent) Resize(width, height int) { + ui.grid = newGrid(ui.shiftIdx, width, height, ui.header, ui.footer, ui.tableRef) + termui.Render(ui.grid) +} + +func (ui *UIComponent) Render(snapshot *Snapshot) { + ui.updateHeader(snapshot) + ui.updateProcesses(snapshot) + ui.updateRemoteAddrs(snapshot) + ui.updateConnections(snapshot) + termui.Render(ui.grid) +}