Move procspy out of vendor into probe/endpoint.

This commit is contained in:
Tom Wilkie
2015-12-09 10:59:27 +00:00
parent 8db21fbb2d
commit b94751ac10
20 changed files with 13 additions and 121 deletions

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 Harmen
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.

View File

@@ -1,20 +0,0 @@
.PHONY: all build buildall test install bench
all: test build buildall install
build:
go build
go vet
golint .
buildall:
GOOS=darwin go build
GOOS=linux go build
test:
go test
install:
go install
bench:
go test -bench .

View File

@@ -1,64 +0,0 @@
Go module to list all TCP connections, with an option to try to find the owning PID and processname.
Works by reading /proc directly on Linux, and by executing `netstat` and `lsof -i` on Darwin.
Works for IPv4 and IPv6 TCP connections. Only established connections are listed; ports where something is only listening or TIME_WAITs are skipped.
If you want to find all processes you'll need to run this as root.
Status:
-------
Tested on Linux and Darwin (10.9).
Install:
--------
`go install`
Usage:
------
Only list the connections:
```
cs, err := procspy.Connections(false)
for c := cs.Next(); c != nil; c = cs.Next() {
...
}
```
List the connections and try to find the owning process:
```
cs, err := procspy.Connections(true)
for c := cs.Next(); c != nil; c = cs.Next() {
...
}
```
(See ./example\_test.go)
``` go
package main
import (
"fmt"
"github.com/weaveworks/procspy"
)
func main() {
lookupProcesses := true
cs, err := procspy.Connections(lookupProcesses)
if err != nil {
panic(err)
}
fmt.Printf("TCP Connections:\n")
for c := cs.Next(); c != nil; c = cs.Next() {
fmt.Printf(" - %v\n", c)
}
}
```

View File

@@ -1,33 +0,0 @@
package procspy
import (
"bytes"
"testing"
)
func BenchmarkParseConnectionsBaseline(b *testing.B) {
readFile = func(string, *bytes.Buffer) error { return nil }
benchmarkConnections(b)
// 333 ns/op, 0 allocs/op
}
func BenchmarkParseConnectionsFixture(b *testing.B) {
readFile = func(_ string, buf *bytes.Buffer) error { _, err := buf.Write(fixture); return err }
benchmarkConnections(b)
// 15553 ns/op, 12 allocs/op
}
func benchmarkConnections(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
cbConnections(false)
}
}
var fixture = []byte(` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000:A6C0 00000000:0000 01 00000000:00000000 00:00000000 00000000 105 0 5107 1 ffff8800a6aaf040 100 0 0 10 0
1: 00000000:006F 00000000:0000 01 00000000:00000000 00:00000000 00000000 0 0 5084 1 ffff8800a6aaf740 100 0 0 10 0
2: 0100007F:0019 00000000:0000 01 00000000:00000000 00:00000000 00000000 0 0 10550 1 ffff8800a729b780 100 0 0 10 0
3: A12CF62E:E4D7 57FC1EC0:01BB 01 00000000:00000000 02:000006FA 00000000 1000 0 639474 2 ffff88007e75a740 48 4 26 10 -1
`)

View File

@@ -1,20 +0,0 @@
package procspy_test
import (
"fmt"
"github.com/weaveworks/procspy"
)
func Example() {
lookupProcesses := true
cs, err := procspy.Connections(lookupProcesses)
if err != nil {
panic(err)
}
fmt.Printf("TCP Connections:\n")
for c := cs.Next(); c != nil; c = cs.Next() {
fmt.Printf(" - %v\n", c)
}
}

View File

@@ -1,26 +0,0 @@
package procspy
// SetFixtures declares constant Connection and ConnectionProcs which will
// always be returned by the package-level Connections and Processes
// functions. It's designed to be used in tests.
type fixedConnIter []Connection
func (f *fixedConnIter) Next() *Connection {
if len(*f) == 0 {
return nil
}
car := (*f)[0]
*f = (*f)[1:]
return &car
}
// SetFixtures is used in test scenarios to have known output.
func SetFixtures(c []Connection) {
cbConnections = func(bool) (ConnIter, error) {
f := fixedConnIter(c)
return &f, nil
}
}

View File

@@ -1,77 +0,0 @@
package procspy
// lsof-executing implementation.
import (
"fmt"
"strconv"
"strings"
)
var (
lsofFields = "cn" // parseLSOF() depends on the order
)
// parseLsof parses lsof out with `-F cn` argument.
//
// Format description: the first letter is the type of record, records are
// newline seperated, the record starting with 'p' (pid) is a new processid.
// There can be multiple connections for the same 'p' record in which case the
// 'p' is not repeated.
//
// For example, this is one process with two listens and one connection:
//
// p13100
// cmpd
// n[::1]:6600
// n127.0.0.1:6600
// n[::1]:6600->[::1]:50992
//
func parseLSOF(out string) (map[string]Proc, error) {
var (
res = map[string]Proc{} // Local addr -> Proc
cp = Proc{}
)
for _, line := range strings.Split(out, "\n") {
if len(line) <= 1 {
continue
}
var (
field = line[0]
value = line[1:]
)
switch field {
case 'p':
pid, err := strconv.Atoi(value)
if err != nil {
return nil, fmt.Errorf("invalid 'p' field in lsof output: %#v", value)
}
cp.PID = uint(pid)
case 'n':
// 'n' is the last field, with '-F cn'
// format examples:
// "192.168.2.111:44013->54.229.241.196:80"
// "[2003:45:2b57:8900:1869:2947:f942:aba7]:55711->[2a00:1450:4008:c01::11]:443"
// "*:111" <- a listen
addresses := strings.SplitN(value, "->", 2)
if len(addresses) != 2 {
// That's a listen entry.
continue
}
res[addresses[0]] = Proc{
Name: cp.Name,
PID: cp.PID,
}
case 'c':
cp.Name = value
default:
return nil, fmt.Errorf("unexpected lsof field: %c in %#v", field, value)
}
}
return res, nil
}

View File

@@ -1,77 +0,0 @@
package procspy
import (
"reflect"
"testing"
)
func TestLSOFParsing(t *testing.T) {
// List of lsof -> expected entries
for in, expected := range map[string]map[string]Proc{
// Single connection
"p25196\n" +
"ccello-app\n" +
"n127.0.0.1:48094->127.0.0.1:4039\n" +
"n*:4040\n": map[string]Proc{
"127.0.0.1:48094": Proc{
PID: 25196,
Name: "cello-app",
},
},
// Only listen()s.
"cdhclient\n" +
"n*:68\n" +
"n*:38282\n" +
"n*:40625\n": map[string]Proc{},
// A bunch
"p13100\n" +
"cmpd\n" +
"n[::1]:6600\n" +
"n127.0.0.1:6600\n" +
"n[::1]:6600->[::1]:50992\n" +
"p14612\n" +
"cchromium\n" +
"n[2003:45:2b57:8900:1869:2947:f942:aba7]:55711->[2a00:1450:4008:c01::11]:443\n" +
"n192.168.2.111:37158->192.0.72.2:80\n" +
"n192.168.2.111:44013->54.229.241.196:80\n" +
"n192.168.2.111:56385->74.201.105.31:443\n" +
"p21356\n" +
"cssh\n" +
"n192.168.2.111:33963->192.168.2.71:22\n": map[string]Proc{
"[::1]:6600": Proc{
PID: 13100,
Name: "mpd",
},
"[2003:45:2b57:8900:1869:2947:f942:aba7]:55711": Proc{
PID: 14612,
Name: "chromium",
},
"192.168.2.111:37158": Proc{
PID: 14612,
Name: "chromium",
},
"192.168.2.111:44013": Proc{
PID: 14612,
Name: "chromium",
},
"192.168.2.111:56385": Proc{
PID: 14612,
Name: "chromium",
},
"192.168.2.111:33963": Proc{
PID: 21356,
Name: "ssh",
},
},
} {
got, err := parseLSOF(in)
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
if !reflect.DeepEqual(expected, got) {
t.Errorf("Expected:\n %#v\nGot:\n %#v\n", expected, got)
}
}
}

View File

@@ -1,18 +0,0 @@
package main
import (
"fmt"
"github.com/weaveworks/procspy"
)
func main() {
cs, err := procspy.Connections(true)
if err != nil {
panic(err)
}
fmt.Printf("TCP Connections:\n")
for c := cs.Next(); c != nil; c = cs.Next() {
fmt.Printf(" - %+v\n", c)
}
}

View File

@@ -1,83 +0,0 @@
package procspy
// netstat reading.
import (
"net"
"strconv"
"strings"
)
// parseDarwinNetstat parses netstat output. (Linux has ip:port, darwin
// ip.port. The 'Proto' column value also differs.)
func parseDarwinNetstat(out string) []Connection {
//
// Active Internet connections
// Proto Recv-Q Send-Q Local Address Foreign Address (state)
// tcp4 0 0 10.0.1.6.58287 1.2.3.4.443 ESTABLISHED
//
res := []Connection{}
for i, line := range strings.Split(out, "\n") {
if i == 0 || i == 1 {
// Skip header
continue
}
// Fields are:
fields := strings.Fields(line)
if len(fields) != 6 {
continue
}
if fields[5] != "ESTABLISHED" {
continue
}
t := Connection{
Transport: "tcp",
}
// Format is <ip>.<port>
locals := strings.Split(fields[3], ".")
if len(locals) < 2 {
continue
}
var (
localAddress = strings.Join(locals[:len(locals)-1], ".")
localPort = locals[len(locals)-1]
)
t.LocalAddress = net.ParseIP(localAddress)
p, err := strconv.Atoi(localPort)
if err != nil {
return nil
}
t.LocalPort = uint16(p)
remotes := strings.Split(fields[4], ".")
if len(remotes) < 2 {
continue
}
var (
remoteAddress = strings.Join(remotes[:len(remotes)-1], ".")
remotePort = remotes[len(remotes)-1]
)
t.RemoteAddress = net.ParseIP(remoteAddress)
p, err = strconv.Atoi(remotePort)
if err != nil {
return nil
}
t.RemotePort = uint16(p)
res = append(res, t)
}
return res
}

View File

@@ -1,58 +0,0 @@
package procspy
import (
"net"
"reflect"
"testing"
)
func TestNetstatDarwin(t *testing.T) {
testString := `Active Internet connections
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 10.0.1.6.58287 1.2.3.4.443 ESTABLISHED
tcp4 0 0 10.0.1.6.58279 2.3.4.5.80 ESTABLISHED
tcp4 0 0 10.0.1.6.58276 44.55.66.77.443 ESTABLISHED
tcp4 0 0 10.0.1.6.1 4.0.4.0.443 GONE
`
res := parseDarwinNetstat(testString)
expected := []Connection{
{
Transport: "tcp",
LocalAddress: net.ParseIP("10.0.1.6"),
LocalPort: 58287,
RemoteAddress: net.ParseIP("1.2.3.4"),
RemotePort: 443,
},
{
Transport: "tcp",
LocalAddress: net.ParseIP("10.0.1.6"),
LocalPort: 58279,
RemoteAddress: net.ParseIP("2.3.4.5"),
RemotePort: 80,
},
{
Transport: "tcp",
LocalAddress: net.ParseIP("10.0.1.6"),
LocalPort: 58276,
RemoteAddress: net.ParseIP("44.55.66.77"),
RemotePort: 443,
},
/*
{
Transport: "tcp",
LocalAddress: "::1",
LocalPort: "6600",
RemoteAddress: "::1",
RemotePort: "41993",
},
*/
}
if len(res) != 3 {
t.Errorf("Wanted 3")
}
if !reflect.DeepEqual(res, expected) {
t.Errorf("OS x netstat 4 error. Got\n%+v\nExpected\n%+v\n", res, expected)
}
}

View File

@@ -1,138 +0,0 @@
package procspy
// /proc-based implementation.
import (
"bytes"
"os"
"path/filepath"
"strconv"
"syscall"
)
var (
procRoot = "/proc"
)
// SetProcRoot sets the location of the proc filesystem.
func SetProcRoot(root string) {
procRoot = root
}
// walkProcPid walks over all numerical (PID) /proc entries, and sees if their
// ./fd/* files are symlink to sockets. Returns a map from socket ID (inode)
// to PID. Will return an error if /proc isn't there.
func walkProcPid(buf *bytes.Buffer) (map[uint64]Proc, error) {
fh, err := os.Open(procRoot)
if err != nil {
return nil, err
}
dirNames, err := fh.Readdirnames(-1)
fh.Close()
if err != nil {
return nil, err
}
var (
res = map[uint64]Proc{}
namespaces = map[uint64]struct{}{}
stat syscall.Stat_t
)
for _, dirName := range dirNames {
pid, err := strconv.ParseUint(dirName, 10, 0)
if err != nil {
// Not a number, so not a PID subdir.
continue
}
fdBase := filepath.Join(procRoot, dirName, "fd")
dfh, err := os.Open(fdBase)
if err != nil {
// Process is be gone by now, or we don't have access.
continue
}
fdNames, err := dfh.Readdirnames(-1)
dfh.Close()
if err != nil {
continue
}
// Read network namespace, and if we haven't seen it before,
// read /proc/<pid>/net/tcp
err = syscall.Lstat(filepath.Join(procRoot, dirName, "/ns/net"), &stat)
if err != nil {
continue
}
if _, ok := namespaces[stat.Ino]; !ok {
namespaces[stat.Ino] = struct{}{}
readFile(filepath.Join(procRoot, dirName, "/net/tcp"), buf)
readFile(filepath.Join(procRoot, dirName, "/net/tcp6"), buf)
}
var name string
for _, fdName := range fdNames {
// Direct use of syscall.Stat() to save garbage.
err = syscall.Stat(filepath.Join(fdBase, fdName), &stat)
if err != nil {
continue
}
// We want sockets only.
if stat.Mode&syscall.S_IFMT != syscall.S_IFSOCK {
continue
}
if name == "" {
if name = procName(filepath.Join(procRoot, dirName)); name == "" {
// Process might be gone by now
break
}
}
res[stat.Ino] = Proc{
PID: uint(pid),
Name: name,
}
}
}
return res, nil
}
// procName does a pid->name lookup.
func procName(base string) string {
fh, err := os.Open(filepath.Join(base, "/comm"))
if err != nil {
return ""
}
name := make([]byte, 64)
l, err := fh.Read(name)
fh.Close()
if err != nil {
return ""
}
if l < 2 {
return ""
}
// drop trailing "\n"
return string(name[:l-1])
}
// readFile reads an arbitrary file into a buffer. It's a variable so it can
// be overwritten for benchmarks. That's bad practice and we should change it
// to be a dependency.
var readFile = func(filename string, buf *bytes.Buffer) error {
f, err := os.Open(filename)
if err != nil {
return err
}
_, err = buf.ReadFrom(f)
f.Close()
return err
}

View File

@@ -1,170 +0,0 @@
package procspy
import (
"bytes"
"net"
)
// ProcNet is an iterator to parse /proc/net/tcp{,6} files.
type ProcNet struct {
b []byte
c Connection
wantedState uint
bytesLocal, bytesRemote [16]byte
seen map[uint64]struct{}
}
// NewProcNet gives a new ProcNet parser.
func NewProcNet(b []byte, wantedState uint) *ProcNet {
return &ProcNet{
b: b,
c: Connection{},
wantedState: wantedState,
seen: map[uint64]struct{}{},
}
}
// Next returns the next connection. All buffers are re-used, so if you want
// to keep the IPs you have to copy them.
func (p *ProcNet) Next() *Connection {
again:
if len(p.b) == 0 {
return nil
}
b := p.b
if p.b[2] == 's' {
// Skip header
p.b = nextLine(b)
goto again
}
var (
local, remote, state, inode []byte
)
_, b = nextField(b) // 'sl' column
local, b = nextField(b)
remote, b = nextField(b)
state, b = nextField(b)
if parseHex(state) != p.wantedState {
p.b = nextLine(b)
goto again
}
_, b = nextField(b) // 'tx_queue' column
_, b = nextField(b) // 'rx_queue' column
_, b = nextField(b) // 'tr' column
_, b = nextField(b) // 'uid' column
_, b = nextField(b) // 'timeout' column
inode, b = nextField(b)
p.c.LocalAddress, p.c.LocalPort = scanAddressNA(local, &p.bytesLocal)
p.c.RemoteAddress, p.c.RemotePort = scanAddressNA(remote, &p.bytesRemote)
p.c.inode = parseDec(inode)
p.b = nextLine(b)
if _, alreadySeen := p.seen[p.c.inode]; alreadySeen {
goto again
}
p.seen[p.c.inode] = struct{}{}
return &p.c
}
// scanAddressNA parses 'A12CF62E:00AA' to the address/port. Handles IPv4 and
// IPv6 addresses. The address is a big endian 32 bit ints, hex encoded. We
// just decode the hex and flip the bytes in every group of 4.
func scanAddressNA(in []byte, buf *[16]byte) (net.IP, uint16) {
col := bytes.IndexByte(in, ':')
if col == -1 {
return nil, 0
}
// Network address is big endian. Can be either ipv4 or ipv6.
address := hexDecode32bigNA(in[:col], buf)
return net.IP(address), uint16(parseHex(in[col+1:]))
}
// hexDecode32big decodes sequences of 32bit big endian bytes.
func hexDecode32bigNA(src []byte, buf *[16]byte) []byte {
blocks := len(src) / 8
for block := 0; block < blocks; block++ {
for i := 0; i < 4; i++ {
a := fromHexChar(src[block*8+i*2])
b := fromHexChar(src[block*8+i*2+1])
buf[block*4+3-i] = (a << 4) | b
}
}
return buf[:blocks*4]
}
func nextField(s []byte) ([]byte, []byte) {
// Skip whitespace.
for i, b := range s {
if b != ' ' {
s = s[i:]
break
}
}
// Up until the next whitespace field.
for i, b := range s {
if b == ' ' {
return s[:i], s[i:]
}
}
return nil, nil
}
func nextLine(s []byte) []byte {
i := bytes.IndexByte(s, '\n')
if i == -1 {
return nil
}
return s[i+1:]
}
// Simplified copy of strconv.ParseUint(16).
func parseHex(s []byte) uint {
n := uint(0)
for i := 0; i < len(s); i++ {
n *= 16
n += uint(fromHexChar(s[i]))
}
return n
}
// Simplified copy of strconv.ParseUint(10).
func parseDec(s []byte) uint64 {
n := uint64(0)
for _, c := range s {
n *= 10
n += uint64(c - '0')
}
return n
}
// hexDecode32big decodes sequences of 32bit big endian bytes.
func hexDecode32big(src []byte) []byte {
dst := make([]byte, len(src)/2)
blocks := len(src) / 8
for block := 0; block < blocks; block++ {
for i := 0; i < 4; i++ {
a := fromHexChar(src[block*8+i*2])
b := fromHexChar(src[block*8+i*2+1])
dst[block*4+3-i] = (a << 4) | b
}
}
return dst
}
// fromHexChar converts a hex character into its value.
func fromHexChar(c byte) uint8 {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}

View File

@@ -1,162 +0,0 @@
package procspy
import (
"net"
"reflect"
"testing"
)
func TestProcNet(t *testing.T) {
testString := ` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout Inode
0: 00000000:A6C0 00000000:0000 01 00000000:00000000 00:00000000 00000000 105 0 5107 1 ffff8800a6aaf040 100 0 0 10 0
1: 00000000:006F 00000000:0000 01 00000000:00000000 00:00000000 00000000 0 0 5084 1 ffff8800a6aaf740 100 0 0 10 0
2: 0100007F:0019 00000000:0000 01 00000000:00000000 00:00000000 00000000 0 0 10550 1 ffff8800a729b780 100 0 0 10 0
3: A12CF62E:E4D7 57FC1EC0:01BB 01 00000000:00000000 02:000006FA 00000000 1000 0 639474 2 ffff88007e75a740 48 4 26 10 -1
`
p := NewProcNet([]byte(testString), tcpEstablished)
expected := []Connection{
{
LocalAddress: net.IP([]byte{0, 0, 0, 0}),
LocalPort: 0xa6c0,
RemoteAddress: net.IP([]byte{0, 0, 0, 0}),
RemotePort: 0x0,
inode: 5107,
},
{
LocalAddress: net.IP([]byte{0, 0, 0, 0}),
LocalPort: 0x006f,
RemoteAddress: net.IP([]byte{0, 0, 0, 0}),
RemotePort: 0x0,
inode: 5084,
},
{
LocalAddress: net.IP([]byte{0x7f, 0x0, 0x0, 0x01}),
LocalPort: 0x0019,
RemoteAddress: net.IP([]byte{0, 0, 0, 0}),
RemotePort: 0x0,
inode: 10550,
},
{
LocalAddress: net.IP([]byte{0x2e, 0xf6, 0x2c, 0xa1}),
LocalPort: 0xe4d7,
RemoteAddress: net.IP([]byte{0xc0, 0x1e, 0xfc, 0x57}),
RemotePort: 0x01bb,
inode: 639474,
},
}
for i := 0; i < 4; i++ {
have := p.Next()
want := expected[i]
if !reflect.DeepEqual(*have, want) {
t.Errorf("transport 4 error. Got\n%+v\nExpected\n%+v\n", *have, want)
}
}
if got := p.Next(); got != nil {
t.Errorf("p.Next() wasn't empty")
}
}
func TestTransport6(t *testing.T) {
// Abridged copy of my /proc/net/tcp6
testString := ` sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout Inode
0: 00000000000000000000000000000000:19C8 00000000000000000000000000000000:0000 01 00000000:00000000 00:00000000 00000000 0 0 23661201 1 ffff880103fb4800 100 0 0 10 -1
8: 4500032000BE692B8AE31EBD919D9D10:D61C 5014002A080805400000000015100000:01BB 01 00000000:00000000 02:00000045 00000000 1000 0 36856710 2 ffff88010b796080 22 4 30 8 7
`
p := NewProcNet([]byte(testString), tcpEstablished)
expected := []Connection{
{
// state: 10,
LocalAddress: net.IP(make([]byte, 16)),
LocalPort: 0x19c8,
RemoteAddress: net.IP(make([]byte, 16)),
RemotePort: 0x0,
// uid: 0,
inode: 23661201,
},
{
// state: 1,
LocalAddress: net.IP([]byte{
0x20, 0x03, 0, 0x45,
0x2b, 0x69, 0xbe, 0x00,
0xbd, 0x1e, 0xe3, 0x8a,
0x10, 0x9d, 0x9d, 0x91,
}),
LocalPort: 0xd61c,
RemoteAddress: net.IP([]byte{
0x2a, 0x00, 0x14, 0x50,
0x40, 0x05, 0x08, 0x08,
0, 0, 0, 0,
0, 0, 0x10, 0x15,
}),
RemotePort: 0x01bb,
// uid: 1000,
inode: 36856710,
},
}
for i := 0; i < 2; i++ {
have := p.Next()
want := expected[i]
if !reflect.DeepEqual(*have, want) {
t.Errorf("got\n%+v\nExpected\n%+v\n", *have, want)
}
}
if got := p.Next(); got != nil {
t.Errorf("p.Next() wasn't empty")
}
}
func TestTransportNonsense(t *testing.T) {
testString := ` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout Inode
0: 00000000:A6C0 00000000:0000 01 000000
broken line
`
p := NewProcNet([]byte(testString), tcpEstablished)
expected := []Connection{
{
LocalAddress: net.IP([]byte{0, 0, 0, 0}),
LocalPort: 0xa6c0,
RemoteAddress: net.IP([]byte{0, 0, 0, 0}),
RemotePort: 0x0,
},
}
for i := 0; i < 1; i++ {
have := p.Next()
want := expected[i]
if !reflect.DeepEqual(*have, want) {
t.Errorf("Got\n%+v\nExpected\n%+v\n", *have, want)
}
}
if got := p.Next(); got != nil {
t.Errorf("p.Next() wasn't empty")
}
}
func TestProcNetFiltersDuplicates(t *testing.T) {
testString := ` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout Inode
0: 00000000:A6C0 00000000:0000 01 00000000:00000000 00:00000000 00000000 105 0 5107 1 ffff8800a6aaf040 100 0 0 10 0
1: 00000000:A6C0 00000000:0000 01 00000000:00000000 00:00000000 00000000 105 0 5107 1 ffff8800a6aaf040 100 0 0 10 0
`
p := NewProcNet([]byte(testString), tcpEstablished)
expected := Connection{
LocalAddress: net.IP([]byte{0, 0, 0, 0}),
LocalPort: 0xa6c0,
RemoteAddress: net.IP([]byte{0, 0, 0, 0}),
RemotePort: 0x0,
inode: 5107,
}
have := p.Next()
want := expected
if !reflect.DeepEqual(*have, want) {
t.Errorf("transport 4 error. Got\n%+v\nExpected\n%+v\n", *have, want)
}
if got := p.Next(); got != nil {
t.Errorf("p.Next() wasn't empty")
}
}

View File

@@ -1,43 +0,0 @@
// Package procspy lists TCP connections, and optionally tries to find the
// owning processes. Works on Linux (via /proc) and Darwin (via `lsof -i` and
// `netstat`). You'll need root to use Processes().
package procspy
import (
"net"
)
const (
tcpEstablished = 1 // according to /include/net/tcp_states.h
)
// Connection is a (TCP) connection. The Proc struct might not be filled in.
type Connection struct {
Transport string
LocalAddress net.IP
LocalPort uint16
RemoteAddress net.IP
RemotePort uint16
inode uint64
Proc
}
// Proc is a single process with PID and process name.
type Proc struct {
PID uint
Name string
}
// ConnIter is returned by Connections().
type ConnIter interface {
Next() *Connection
}
// Connections returns all established (TCP) connections. If processes is
// false we'll just list all TCP connections, and there is no need to be root.
// If processes is true it'll additionally try to lookup the process owning the
// connection, filling in the Proc field. You will need to run this as root to
// find all processes.
func Connections(processes bool) (ConnIter, error) {
return cbConnections(processes)
}

View File

@@ -1,62 +0,0 @@
package procspy
import (
"net"
"os/exec"
"strconv"
)
const (
netstatBinary = "netstat"
lsofBinary = "lsof"
)
// Connections returns all established (TCP) connections. No need to be root
// to run this. If processes is true it also tries to fill in the process
// fields of the connection. You need to be root to find all processes.
var cbConnections = func(processes bool) (ConnIter, error) {
out, err := exec.Command(
netstatBinary,
"-n", // no number resolving
"-W", // Wide output
// "-l", // full IPv6 addresses // What does this do?
"-p", "tcp", // only TCP
).CombinedOutput()
if err != nil {
// log.Printf("lsof error: %s", err)
return nil, err
}
connections := parseDarwinNetstat(string(out))
if processes {
out, err := exec.Command(
lsofBinary,
"-i", // only Internet files
"-n", "-P", // no number resolving
"-w", // no warnings
"-F", lsofFields, // \n based output of only the fields we want.
).CombinedOutput()
if err != nil {
return nil, err
}
procs, err := parseLSOF(string(out))
if err != nil {
return nil, err
}
for local, proc := range procs {
for i, c := range connections {
localAddr := net.JoinHostPort(
c.LocalAddress.String(),
strconv.Itoa(int(c.LocalPort)),
)
if localAddr == local {
connections[i].Proc = proc
}
}
}
}
f := fixedConnIter(connections)
return &f, nil
}

View File

@@ -1,57 +0,0 @@
package procspy
import (
"bytes"
"sync"
)
var bufPool = sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, 5000))
},
}
type pnConnIter struct {
pn *ProcNet
buf *bytes.Buffer
procs map[uint64]Proc
}
func (c *pnConnIter) Next() *Connection {
n := c.pn.Next()
if n == nil {
// Done!
bufPool.Put(c.buf)
return nil
}
if proc, ok := c.procs[n.inode]; ok {
n.Proc = proc
}
return n
}
// cbConnections sets Connections()
var cbConnections = func(processes bool) (ConnIter, error) {
// buffer for contents of /proc/<pid>/net/tcp
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
var procs map[uint64]Proc
if processes {
var err error
if procs, err = walkProcPid(buf); err != nil {
return nil, err
}
}
if buf.Len() == 0 {
readFile(procRoot+"/net/tcp", buf)
readFile(procRoot+"/net/tcp6", buf)
}
return &pnConnIter{
pn: NewProcNet(buf.Bytes(), tcpEstablished),
buf: buf,
procs: procs,
}, nil
}