diff --git a/go.mod b/go.mod index c5ed8bdc7..ae1e7b4ba 100644 --- a/go.mod +++ b/go.mod @@ -3,27 +3,25 @@ module github.com/shirou/gopsutil/v4 go 1.18 require ( + github.com/florianl/go-diag v0.0.1 github.com/google/go-cmp v0.6.0 github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 + github.com/mdlayher/socket v0.5.1 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c github.com/shoenig/go-m1cpu v0.1.6 github.com/stretchr/testify v1.9.0 github.com/tklauser/go-sysconf v0.3.12 github.com/yusufpapurcu/wmi v1.2.4 - golang.org/x/sys v0.20.0 -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.1-0.20240506173926-6dfb94eaa3bd ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-ole/go-ole v1.2.6 // indirect + github.com/josharian/native v1.1.0 // indirect + github.com/mdlayher/netlink v1.7.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/tklauser/numcpus v0.6.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 64e81120b..e7e5cbe01 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,20 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/florianl/go-diag v0.0.1 h1:NjjG/z0eQ7J6+KrIv3hv5weH6Lxq+KBr45HPc/qdRQo= +github.com/florianl/go-diag v0.0.1/go.mod h1:Rkjs8DWYe7g4wuw4BQbYXBuUU7vaf1s2ym8N+QpCZt0= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= 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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= @@ -22,12 +30,16 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.1-0.20240506173926-6dfb94eaa3bd h1:NY5cSUUSJ8nWcXOjwj7e1D9//oy6+uKCq32Dnt/p9EQ= +golang.org/x/sys v0.20.1-0.20240506173926-6dfb94eaa3bd/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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= diff --git a/net/net_linux.go b/net/net_linux.go index a46f1b9dc..04658b548 100644 --- a/net/net_linux.go +++ b/net/net_linux.go @@ -4,19 +4,18 @@ package net import ( - "bytes" "context" - "encoding/hex" "errors" "fmt" "io" - "net" "os" "strconv" "strings" "syscall" + "github.com/florianl/go-diag" "github.com/shirou/gopsutil/v4/internal/common" + "golang.org/x/sys/unix" ) const ( // Conntrack Column numbers @@ -305,24 +304,24 @@ func conntrackStatsFromFile(filename string, percpu bool) ([]ConntrackStat, erro } // http://students.mimuw.edu.pl/lxr/source/include/net/tcp_states.h -var tcpStatuses = map[string]string{ - "01": "ESTABLISHED", - "02": "SYN_SENT", - "03": "SYN_RECV", - "04": "FIN_WAIT1", - "05": "FIN_WAIT2", - "06": "TIME_WAIT", - "07": "CLOSE", - "08": "CLOSE_WAIT", - "09": "LAST_ACK", - "0A": "LISTEN", - "0B": "CLOSING", +var tcpStatuses = map[uint8]string{ + 1: "ESTABLISHED", + 2: "SYN_SENT", + 3: "SYN_RECV", + 4: "FIN_WAIT1", + 5: "FIN_WAIT2", + 6: "TIME_WAIT", + 7: "CLOSE", + 8: "CLOSE_WAIT", + 9: "LAST_ACK", + 10: "LISTEN", + 11: "CLOSING", } type netConnectionKindType struct { + filename string family uint32 sockType uint32 - filename string } var kindTCP4 = netConnectionKindType{ @@ -374,15 +373,15 @@ type inodeMap struct { } type connTmp struct { - fd uint32 - family uint32 - sockType uint32 + status string + path string laddr Addr raddr Addr - status string + fd uint32 + sockType uint32 pid int32 boundPid int32 - path string + family uint8 } // Return a list of network connections opened. @@ -460,7 +459,7 @@ func connectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, p } root := common.HostProcWithContext(ctx) var err error - var inodes map[string][]inodeMap + var inodes map[uint32][]inodeMap if pid == 0 { inodes, err = getProcInodesAllWithContext(ctx, root, max) } else { @@ -476,33 +475,31 @@ func connectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, p return statsFromInodesWithContext(ctx, root, pid, tmap, inodes, skipUids) } -func statsFromInodes(root string, pid int32, tmap []netConnectionKindType, inodes map[string][]inodeMap, skipUids bool) ([]ConnectionStat, error) { - return statsFromInodesWithContext(context.Background(), root, pid, tmap, inodes, skipUids) -} - -func statsFromInodesWithContext(ctx context.Context, root string, pid int32, tmap []netConnectionKindType, inodes map[string][]inodeMap, skipUids bool) ([]ConnectionStat, error) { +func statsFromInodesWithContext(ctx context.Context, root string, pid int32, tmap []netConnectionKindType, inodes map[uint32][]inodeMap, skipUids bool) ([]ConnectionStat, error) { dupCheckMap := make(map[string]struct{}) var ret []ConnectionStat - var err error + // open a netlink socket + nl, err := diag.Open(&diag.Config{}) + if err != nil { + return nil, err + } + defer nl.Close() + for _, t := range tmap { - var path string var connKey string var ls []connTmp - if pid == 0 { - path = fmt.Sprintf("%s/net/%s", root, t.filename) - } else { - path = fmt.Sprintf("%s/%d/net/%s", root, pid, t.filename) - } + switch t.family { case syscall.AF_INET, syscall.AF_INET6: - ls, err = processInetWithContext(ctx, path, t, inodes, pid) + ls, err = processNetDiagWithContext(nl, t, inodes, pid) case syscall.AF_UNIX: - ls, err = processUnix(path, t, inodes, pid) + ls, err = processUnixDiagWithContext(nl, inodes, pid) } if err != nil { return nil, err } + for _, c := range ls { // Build TCP key to id the connection uniquely // socket type, src ip, src port, dst ip, dst port and state should be enough @@ -514,8 +511,8 @@ func statsFromInodesWithContext(ctx context.Context, root string, pid int32, tma conn := ConnectionStat{ Fd: c.fd, - Family: c.family, - Type: c.sockType, + Family: uint32(c.family), + Type: uint32(c.sockType), Laddr: c.laddr, Raddr: c.raddr, Status: c.status, @@ -543,8 +540,8 @@ func statsFromInodesWithContext(ctx context.Context, root string, pid int32, tma } // getProcInodes returns fd of the pid. -func getProcInodes(root string, pid int32, max int) (map[string][]inodeMap, error) { - ret := make(map[string][]inodeMap) +func getProcInodes(root string, pid int32, max int) (map[uint32][]inodeMap, error) { + ret := make(map[uint32][]inodeMap) dir := fmt.Sprintf("%s/%d/fd", root, pid) f, err := os.Open(dir) @@ -559,16 +556,21 @@ func getProcInodes(root string, pid int32, max int) (map[string][]inodeMap, erro for _, dirEntry := range dirEntries { inodePath := fmt.Sprintf("%s/%d/fd/%s", root, pid, dirEntry.Name()) - inode, err := os.Readlink(inodePath) + inodeStr, err := os.Readlink(inodePath) if err != nil { continue } - if !strings.HasPrefix(inode, "socket:[") { + if !strings.HasPrefix(inodeStr, "socket:[") { continue } // the process is using a socket - l := len(inode) - inode = inode[8 : l-1] + l := len(inodeStr) + inodeStr = inodeStr[8 : l-1] + tmp, err := strconv.Atoi(inodeStr) + if err != nil { + return ret, err + } + inode := uint32(tmp) _, ok := ret[inode] if !ok { ret[inode] = make([]inodeMap, 0) @@ -625,8 +627,8 @@ func PidsWithContext(ctx context.Context) ([]int32, error) { // FIXME: Import process occures import cycle. // see remarks on pids() type process struct { - Pid int32 `json:"pid"` uids []int32 + Pid int32 `json:"pid"` } // Uids returns user ids of the process as a slice of the int @@ -668,16 +670,16 @@ func (p *process) fillFromStatus(ctx context.Context) error { return nil } -func getProcInodesAll(root string, max int) (map[string][]inodeMap, error) { +func getProcInodesAll(root string, max int) (map[uint32][]inodeMap, error) { return getProcInodesAllWithContext(context.Background(), root, max) } -func getProcInodesAllWithContext(ctx context.Context, root string, max int) (map[string][]inodeMap, error) { +func getProcInodesAllWithContext(ctx context.Context, root string, max int) (map[uint32][]inodeMap, error) { pids, err := PidsWithContext(ctx) if err != nil { return nil, err } - ret := make(map[string][]inodeMap) + ret := make(map[uint32][]inodeMap) for _, pid := range pids { t, err := getProcInodes(root, pid, max) @@ -697,48 +699,6 @@ func getProcInodesAllWithContext(ctx context.Context, root string, max int) (map return ret, nil } -// decodeAddress decode addresse represents addr in proc/net/* -// ex: -// "0500000A:0016" -> "10.0.0.5", 22 -// "0085002452100113070057A13F025401:0035" -> "2400:8500:1301:1052:a157:7:154:23f", 53 -func decodeAddress(family uint32, src string) (Addr, error) { - return decodeAddressWithContext(context.Background(), family, src) -} - -func decodeAddressWithContext(ctx context.Context, family uint32, src string) (Addr, error) { - t := strings.Split(src, ":") - if len(t) != 2 { - return Addr{}, fmt.Errorf("does not contain port, %s", src) - } - addr := t[0] - port, err := strconv.ParseUint(t[1], 16, 16) - if err != nil { - return Addr{}, fmt.Errorf("invalid port, %s", src) - } - decoded, err := hex.DecodeString(addr) - if err != nil { - return Addr{}, fmt.Errorf("decode error, %w", err) - } - var ip net.IP - - if family == syscall.AF_INET { - if common.IsLittleEndian() { - ip = net.IP(ReverseWithContext(ctx, decoded)) - } else { - ip = net.IP(decoded) - } - } else { // IPv6 - ip, err = parseIPv6HexStringWithContext(ctx, decoded) - if err != nil { - return Addr{}, err - } - } - return Addr{ - IP: ip.String(), - Port: uint32(port), - }, nil -} - // Reverse reverses array of bytes. func Reverse(s []byte) []byte { return ReverseWithContext(context.Background(), s) @@ -751,59 +711,32 @@ func ReverseWithContext(ctx context.Context, s []byte) []byte { return s } -// parseIPv6HexString parse array of bytes to IPv6 string -func parseIPv6HexString(src []byte) (net.IP, error) { - return parseIPv6HexStringWithContext(context.Background(), src) -} +func processNetDiagWithContext(nl *diag.Diag, kind netConnectionKindType, inodes map[uint32][]inodeMap, filterPid int32) ([]connTmp, error) { + var ret []connTmp -func parseIPv6HexStringWithContext(ctx context.Context, src []byte) (net.IP, error) { - if len(src) != 16 { - return nil, fmt.Errorf("invalid IPv6 string") + opt := &diag.NetOption{ + Family: uint8(kind.family), + State: ^uint32(0), } - buf := make([]byte, 0, 16) - for i := 0; i < len(src); i += 4 { - r := ReverseWithContext(ctx, src[i:i+4]) - buf = append(buf, r...) + switch kind.sockType { + case 1: + opt.Protocol = unix.IPPROTO_TCP + case 2: + opt.Protocol = unix.IPPROTO_UDP + default: + return nil, fmt.Errorf("unhandled socket type") } - return net.IP(buf), nil -} -func processInet(file string, kind netConnectionKindType, inodes map[string][]inodeMap, filterPid int32) ([]connTmp, error) { - return processInetWithContext(context.Background(), file, kind, inodes, filterPid) -} - -func processInetWithContext(ctx context.Context, file string, kind netConnectionKindType, inodes map[string][]inodeMap, filterPid int32) ([]connTmp, error) { - if strings.HasSuffix(file, "6") && !common.PathExists(file) { - // IPv6 not supported, return empty. - return []connTmp{}, nil - } - - // Read the contents of the /proc file with a single read sys call. - // This minimizes duplicates in the returned connections - // For more info: - // https://github.com/shirou/gopsutil/pull/361 - contents, err := os.ReadFile(file) + conns, err := nl.NetDump(opt) if err != nil { return nil, err } - lines := bytes.Split(contents, []byte("\n")) - - var ret []connTmp - // skip first line - for _, line := range lines[1:] { - l := strings.Fields(string(line)) - if len(l) < 10 { - continue - } - laddr := l[1] - raddr := l[2] - status := l[3] - inode := l[9] + for _, conn := range conns { pid := int32(0) fd := uint32(0) - i, exists := inodes[inode] + i, exists := inodes[conn.INode] if exists { pid = i[0].pid fd = i[0].fd @@ -811,62 +744,56 @@ func processInetWithContext(ctx context.Context, file string, kind netConnection if filterPid > 0 && filterPid != pid { continue } - if kind.sockType == syscall.SOCK_STREAM { - status = tcpStatuses[status] - } else { - status = "NONE" - } - la, err := decodeAddressWithContext(ctx, kind.family, laddr) + + src, err := diag.ToNetipAddrWithFamily(conn.DiagMsg.Family, conn.ID.Src) if err != nil { continue } - ra, err := decodeAddressWithContext(ctx, kind.family, raddr) + srcPort := diag.Ntohs(conn.ID.SPort) + dst, err := diag.ToNetipAddrWithFamily(conn.DiagMsg.Family, conn.ID.Dst) if err != nil { continue } + dstPort := diag.Ntohs(conn.ID.DPort) + + status := "NONE" + if kind.sockType == syscall.SOCK_STREAM { + status = tcpStatuses[conn.State] + } ret = append(ret, connTmp{ fd: fd, - family: kind.family, + family: conn.Family, sockType: kind.sockType, - laddr: la, - raddr: ra, - status: status, - pid: pid, + laddr: Addr{ + IP: src.String(), + Port: uint32(srcPort), + }, + raddr: Addr{ + IP: dst.String(), + Port: uint32(dstPort), + }, + pid: pid, + status: status, }) } return ret, nil } -func processUnix(file string, kind netConnectionKindType, inodes map[string][]inodeMap, filterPid int32) ([]connTmp, error) { - // Read the contents of the /proc file with a single read sys call. - // This minimizes duplicates in the returned connections - // For more info: - // https://github.com/shirou/gopsutil/pull/361 - contents, err := os.ReadFile(file) +func processUnixDiagWithContext(nl *diag.Diag, inodes map[uint32][]inodeMap, filterPid int32) ([]connTmp, error) { + var ret []connTmp + + conns, err := nl.UnixDump(&diag.UnixOption{ + State: ^uint32(0), + Show: ^uint32(0), + }) if err != nil { return nil, err } - - lines := bytes.Split(contents, []byte("\n")) - - var ret []connTmp - // skip first line - for _, line := range lines[1:] { - tokens := strings.Fields(string(line)) - if len(tokens) < 6 { - continue - } - st, err := strconv.Atoi(tokens[4]) - if err != nil { - return nil, err - } - - inode := tokens[6] - + for _, conn := range conns { var pairs []inodeMap - pairs, exists := inodes[inode] + pairs, exists := inodes[conn.Ino] if !exists { pairs = []inodeMap{ {}, @@ -877,13 +804,13 @@ func processUnix(file string, kind netConnectionKindType, inodes map[string][]in continue } var path string - if len(tokens) == 8 { - path = tokens[len(tokens)-1] + if conn.Name != nil { + path = *conn.Name } ret = append(ret, connTmp{ fd: pair.fd, - family: kind.family, - sockType: uint32(st), + family: conn.Family, + sockType: uint32(conn.Type), laddr: Addr{ IP: path, }, @@ -897,7 +824,7 @@ func processUnix(file string, kind netConnectionKindType, inodes map[string][]in return ret, nil } -func updateMap(src map[string][]inodeMap, add map[string][]inodeMap) map[string][]inodeMap { +func updateMap(src map[uint32][]inodeMap, add map[uint32][]inodeMap) map[uint32][]inodeMap { for key, value := range add { a, exists := src[key] if !exists { diff --git a/net/net_linux_test.go b/net/net_linux_test.go index b7ccaa895..15c559c7e 100644 --- a/net/net_linux_test.go +++ b/net/net_linux_test.go @@ -7,7 +7,6 @@ import ( "net" "os" "strings" - "syscall" "testing" "github.com/stretchr/testify/assert" @@ -129,66 +128,6 @@ func TestConnectionsMax(t *testing.T) { } } -type AddrTest struct { - IP string - Port int - Error bool -} - -func TestDecodeAddress(t *testing.T) { - assert := assert.New(t) - - addr := map[string]AddrTest{ - "11111:0035": { - Error: true, - }, - "0100007F:BLAH": { - Error: true, - }, - "0085002452100113070057A13F025401:0035": { - IP: "2400:8500:1301:1052:a157:7:154:23f", - Port: 53, - }, - "00855210011307F025401:0035": { - Error: true, - }, - } - if common.IsLittleEndian() { - addr["0500000A:0016"] = AddrTest{ - IP: "10.0.0.5", - Port: 22, - } - addr["0100007F:D1C2"] = AddrTest{ - IP: "127.0.0.1", - Port: 53698, - } - } else { - addr["0A000005:0016"] = AddrTest{ - IP: "10.0.0.5", - Port: 22, - } - addr["7F000001:D1C2"] = AddrTest{ - IP: "127.0.0.1", - Port: 53698, - } - } - - for src, dst := range addr { - family := syscall.AF_INET - if len(src) > 13 { - family = syscall.AF_INET6 - } - addr, err := decodeAddress(uint32(family), src) - if dst.Error { - assert.NotNil(err, src) - } else { - assert.Nil(err, src) - assert.Equal(dst.IP, addr.IP, src) - assert.Equal(dst.Port, int(addr.Port), src) - } - } -} - func TestReverse(t *testing.T) { src := []byte{0x01, 0x02, 0x03} assert.Equal(t, []byte{0x03, 0x02, 0x01}, Reverse(src))