Init project
This commit is contained in:
commit
414915edea
|
|
@ -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*
|
||||
|
|
@ -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.
|
||||
|
|
@ -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{}
|
||||
}
|
||||
|
|
@ -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<<tcpEstablished)},
|
||||
{syscall.IPPROTO_TCP, syscall.AF_INET6, uint32(1 | 1<<tcpEstablished)},
|
||||
{syscall.IPPROTO_UDP, syscall.AF_INET, uint32(1 << udpConnection)},
|
||||
{syscall.IPPROTO_UDP, syscall.AF_INET6, uint32(1 << udpConnection)},
|
||||
}
|
||||
|
||||
type Fd struct {
|
||||
fd, proto int
|
||||
}
|
||||
var fds []Fd
|
||||
for _, req := range reqs {
|
||||
fd, err := nl.sockdiagSend(uint8(req.Protocol), req.Family, req.State)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer syscall.Close(fd)
|
||||
fds = append(fds, Fd{fd, req.Protocol})
|
||||
}
|
||||
|
||||
for _, fd := range fds {
|
||||
m, err := nl.sockdiagRecv(fd.fd, fd.proto, inodeMap)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
for k, v := range m {
|
||||
ret[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (nl *netlinkConn) getAllProcsInodes(pids []int32) map[uint32]string {
|
||||
ret := make(map[uint32]string)
|
||||
for _, pid := range pids {
|
||||
procName, inodes, err := nl.getProcInodes(pid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, inode := range inodes {
|
||||
ret[inode] = procName
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (nl *netlinkConn) getProcInodes(pid int32) (string, []uint32, error) {
|
||||
var inodeFds []uint32
|
||||
procName, err := os.Readlink(fmt.Sprintf("/proc/%d/exe", pid))
|
||||
if err != nil {
|
||||
return procName, inodeFds, err
|
||||
}
|
||||
|
||||
f, err := os.Open(fmt.Sprintf("/proc/%d/fd", pid))
|
||||
if err != nil {
|
||||
return procName, inodeFds, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
files, err := f.Readdir(0)
|
||||
if err != nil {
|
||||
return procName, inodeFds, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
inode, err := os.Readlink(fmt.Sprintf("/proc/%d/fd/%s", pid, file.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Socket:[1070205860]
|
||||
if !strings.HasPrefix(inode, "socket:[") {
|
||||
continue
|
||||
}
|
||||
|
||||
inodeInt, err := strconv.Atoi(inode[8 : len(inode)-1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
inodeFds = append(inodeFds, uint32(inodeInt))
|
||||
}
|
||||
return filepath.Base(procName), inodeFds, nil
|
||||
}
|
||||
|
||||
func (nl *netlinkConn) listPids() ([]int32, error) {
|
||||
var ret []int32
|
||||
d, err := os.Open("/proc")
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
fnames, err := d.Readdirnames(-1)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
for _, fname := range fnames {
|
||||
pid, err := strconv.ParseInt(fname, 10, 32)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, int32(pid))
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (nl *netlinkConn) GetOpenSockets() (OpenSockets, error) {
|
||||
pids, err := nl.listPids()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inodeMap := nl.getAllProcsInodes(pids)
|
||||
return nl.getOpenSockets(inodeMap)
|
||||
}
|
||||
|
||||
func (nl *netlinkConn) GetProcSockets(pid int32) (OpenSockets, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func GetSocketFetcher() SocketFetcher {
|
||||
return &netlinkConn{}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/shirou/gopsutil/net"
|
||||
"github.com/shirou/gopsutil/process"
|
||||
)
|
||||
|
||||
type psutilConn struct{}
|
||||
|
||||
func (ps *psutilConn) GetOpenSockets() (OpenSockets, error) {
|
||||
openSockets := make(OpenSockets)
|
||||
if err := ps.getConnections(ProtoTCP, openSockets); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ps.getConnections(ProtoUDP, openSockets); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return openSockets, nil
|
||||
}
|
||||
|
||||
func (ps *psutilConn) GetProcSockets(pid int32) (OpenSockets, error) { return nil, nil }
|
||||
|
||||
func (ps *psutilConn) getProcName(pid int32) string {
|
||||
proc, err := process.NewProcess(pid)
|
||||
if err != nil {
|
||||
return unknownProcessName
|
||||
}
|
||||
exe, err := proc.Exe()
|
||||
if err != nil {
|
||||
return unknownProcessName
|
||||
}
|
||||
return filepath.Base(exe)
|
||||
}
|
||||
|
||||
func (ps *psutilConn) getConnections(proto Protocol, openSockets OpenSockets) error {
|
||||
protos := []string{"tcp", "tcp6"}
|
||||
if proto == ProtoUDP {
|
||||
protos = []string{"udp", "udp6"}
|
||||
}
|
||||
|
||||
for _, p := range protos {
|
||||
connections, err := net.Connections(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, conn := range connections {
|
||||
if proto == ProtoTCP && conn.Status != "ESTABLISHED" {
|
||||
continue
|
||||
}
|
||||
|
||||
localSocket := LocalSocket{
|
||||
IP: conn.Laddr.IP,
|
||||
Port: uint16(conn.Laddr.Port),
|
||||
Protocol: proto,
|
||||
}
|
||||
openSockets[localSocket] = ps.getProcName(conn.Pid)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetSocketFetcher() SocketFetcher {
|
||||
return &psutilConn{}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/dnscache"
|
||||
)
|
||||
|
||||
type DNSResolver struct {
|
||||
done chan struct{}
|
||||
resolver *dnscache.Resolver
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewDnsResolver() *DNSResolver {
|
||||
r := &DNSResolver{
|
||||
done: make(chan struct{}, 1),
|
||||
resolver: &dnscache.Resolver{},
|
||||
}
|
||||
r.start()
|
||||
return r
|
||||
}
|
||||
|
||||
func (c *DNSResolver) start() {
|
||||
c.wg.Add(1)
|
||||
defer c.wg.Done()
|
||||
|
||||
go func() {
|
||||
t := time.NewTicker(2 * time.Minute)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.done:
|
||||
return
|
||||
case <-t.C:
|
||||
c.resolver.Refresh(true)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *DNSResolver) Close() {
|
||||
c.done <- struct{}{}
|
||||
c.wg.Wait()
|
||||
}
|
||||
|
||||
func (c *DNSResolver) Lookup(ip string) string {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
addrs, err := c.resolver.LookupAddr(ctx, ip)
|
||||
if err != nil {
|
||||
return ip
|
||||
}
|
||||
|
||||
if len(addrs) == 0 {
|
||||
return ip
|
||||
}
|
||||
|
||||
sort.Strings(addrs)
|
||||
return addrs[0]
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
module github.com/chenjiandongx/sniffer
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/StackExchange/wmi v1.2.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/gammazero/deque v0.1.0
|
||||
github.com/gizak/termui/v3 v3.1.0
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417
|
||||
github.com/shirou/gopsutil v3.21.10+incompatible
|
||||
github.com/stretchr/testify v1.7.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.9 // indirect
|
||||
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71
|
||||
)
|
||||
|
||||
replace github.com/gizak/termui/v3 v3.1.0 => ../../gizak/termui
|
||||
|
|
@ -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=
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/gammazero/deque"
|
||||
)
|
||||
|
||||
const (
|
||||
unknownProcessName = "<UNKNOWN>"
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <Space> to pause. Use <Tab> 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)
|
||||
}
|
||||
Loading…
Reference in New Issue