diff --git a/internal/common/common.go b/internal/common/common.go index df72e65..4ca8bc9 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -213,6 +213,12 @@ func ReadInts(filename string) ([]int64, error) { return ret, nil } +// Parse Hex to uint32 without error +func HexToUint32(hex string) uint32 { + vv, _ := strconv.ParseUint(hex, 16, 32) + return uint32(vv) +} + // Parse to int32 without error func mustParseInt32(val string) int32 { vv, _ := strconv.ParseInt(val, 10, 32) diff --git a/internal/common/common_test.go b/internal/common/common_test.go index cd33388..3d6ae10 100644 --- a/internal/common/common_test.go +++ b/internal/common/common_test.go @@ -51,6 +51,12 @@ func TestByteToString(t *testing.T) { } } +func TestHexToUint32(t *testing.T) { + if HexToUint32("FFFFFFFF") != 4294967295 { + t.Error("Could not convert") + } +} + func TestmustParseInt32(t *testing.T) { ret := mustParseInt32("11111") if ret != int32(11111) { diff --git a/net/net.go b/net/net.go index fce86c7..da613ac 100644 --- a/net/net.go +++ b/net/net.go @@ -70,6 +70,96 @@ type FilterStat struct { ConnTrackMax int64 `json:"conntrackMax"` } +// ConntrackStat has conntrack summary info +type ConntrackStat struct { + Entries uint32 `json:"entries"` // Number of entries in the conntrack table + Searched uint32 `json:"searched"` // Number of conntrack table lookups performed + Found uint32 `json:"found"` // Number of searched entries which were successful + New uint32 `json:"new"` // Number of entries added which were not expected before + Invalid uint32 `json:"invalid"` // Number of packets seen which can not be tracked + Ignore uint32 `json:"ignore"` // Packets seen which are already connected to an entry + Delete uint32 `json:"delete"` // Number of entries which were removed + DeleteList uint32 `json:"delete_list"` // Number of entries which were put to dying list + Insert uint32 `json:"insert"` // Number of entries inserted into the list + InsertFailed uint32 `json:"insert_failed"` // # insertion attempted but failed (same entry exists) + Drop uint32 `json:"drop"` // Number of packets dropped due to conntrack failure. + EarlyDrop uint32 `json:"early_drop"` // Dropped entries to make room for new ones, if maxsize reached + IcmpError uint32 `json:"icmp_error"` // Subset of invalid. Packets that can't be tracked d/t error + ExpectNew uint32 `json:"expect_new"` // Entries added after an expectation was already present + ExpectCreate uint32 `json:"expect_create"` // Expectations added + ExpectDelete uint32 `json:"expect_delete"` // Expectations deleted + SearchRestart uint32 `json:"search_restart"` // Conntrack table lookups restarted due to hashtable resizes +} + +func NewConntrackStat(e uint32, s uint32, f uint32, n uint32, inv uint32, ign uint32, del uint32, dlst uint32, ins uint32, insfail uint32, drop uint32, edrop uint32, ie uint32, en uint32, ec uint32, ed uint32, sr uint32) *ConntrackStat { + return &ConntrackStat{ + Entries: e, + Searched: s, + Found: f, + New: n, + Invalid: inv, + Ignore: ign, + Delete: del, + DeleteList: dlst, + Insert: ins, + InsertFailed: insfail, + Drop: drop, + EarlyDrop: edrop, + IcmpError: ie, + ExpectNew: en, + ExpectCreate: ec, + ExpectDelete: ed, + SearchRestart: sr, + } +} + +type ConntrackStatList struct { + items []*ConntrackStat +} + +func NewConntrackStatList() *ConntrackStatList { + return &ConntrackStatList{ + items: []*ConntrackStat{}, + } +} + +func (l *ConntrackStatList) Append(c *ConntrackStat) { + l.items = append(l.items, c) +} + +func (l *ConntrackStatList) Items() []ConntrackStat { + items := make([]ConntrackStat, len(l.items), len(l.items)) + for i, el := range l.items { + items[i] = *el + } + return items +} + +// Summary returns a single-element list with totals from all list items. +func (l *ConntrackStatList) Summary() []ConntrackStat { + summary := NewConntrackStat(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + for _, cs := range l.items { + summary.Entries += cs.Entries + summary.Searched += cs.Searched + summary.Found += cs.Found + summary.New += cs.New + summary.Invalid += cs.Invalid + summary.Ignore += cs.Ignore + summary.Delete += cs.Delete + summary.DeleteList += cs.DeleteList + summary.Insert += cs.Insert + summary.InsertFailed += cs.InsertFailed + summary.Drop += cs.Drop + summary.EarlyDrop += cs.EarlyDrop + summary.IcmpError += cs.IcmpError + summary.ExpectNew += cs.ExpectNew + summary.ExpectCreate += cs.ExpectCreate + summary.ExpectDelete += cs.ExpectDelete + summary.SearchRestart += cs.SearchRestart + } + return []ConntrackStat{*summary} +} + var constMap = map[string]int{ "unix": syscall.AF_UNIX, "TCP": syscall.SOCK_STREAM, @@ -108,6 +198,11 @@ func (n InterfaceAddr) String() string { return string(s) } +func (n ConntrackStat) String() string { + s, _ := json.Marshal(n) + return string(s) +} + func Interfaces() ([]InterfaceStat, error) { return InterfacesWithContext(context.Background()) } diff --git a/net/net_darwin.go b/net/net_darwin.go index ebebc2f..1daed86 100644 --- a/net/net_darwin.go +++ b/net/net_darwin.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "github.com/shirou/gopsutil/internal/common" "os/exec" "regexp" "strconv" @@ -271,6 +272,14 @@ func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { return nil, errors.New("NetFilterCounters not implemented for darwin") } +func ConntrackStats(percpu bool) ([]ConntrackStat, error) { + return ConntrackStatsWithContext(context.Background(), percpu) +} + +func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) { + return nil, common.ErrNotImplementedError +} + // NetProtoCounters returns network statistics for the entire system // If protocols is empty then all protocols are returned, otherwise // just the protocols in the list are returned. diff --git a/net/net_fallback.go b/net/net_fallback.go index 7c5e632..0991347 100644 --- a/net/net_fallback.go +++ b/net/net_fallback.go @@ -24,6 +24,14 @@ func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { return []FilterStat{}, common.ErrNotImplementedError } +func ConntrackStats(percpu bool) ([]ConntrackStat, error) { + return ConntrackStatsWithContext(context.Background(), percpu) +} + +func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) { + return nil, common.ErrNotImplementedError +} + func ProtoCounters(protocols []string) ([]ProtoCountersStat, error) { return ProtoCountersWithContext(context.Background(), protocols) } diff --git a/net/net_freebsd.go b/net/net_freebsd.go index 84b970a..2284d98 100644 --- a/net/net_freebsd.go +++ b/net/net_freebsd.go @@ -112,6 +112,14 @@ func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { return nil, errors.New("NetFilterCounters not implemented for freebsd") } +func ConntrackStats(percpu bool) ([]ConntrackStat, error) { + return ConntrackStatsWithContext(context.Background(), percpu) +} + +func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) { + return nil, errors.New("ConntrackStats not implemented for freebsd") +} + // NetProtoCounters returns network statistics for the entire system // If protocols is empty then all protocols are returned, otherwise // just the protocols in the list are returned. diff --git a/net/net_linux.go b/net/net_linux.go index 71842fb..5e348bb 100644 --- a/net/net_linux.go +++ b/net/net_linux.go @@ -18,6 +18,26 @@ import ( "github.com/shirou/gopsutil/internal/common" ) +const ( // Conntrack Column numbers + CT_ENTRIES = iota + CT_SEARCHED + CT_FOUND + CT_NEW + CT_INVALID + CT_IGNORE + CT_DELETE + CT_DELETE_LIST + CT_INSERT + CT_INSERT_FAILED + CT_DROP + CT_EARLY_DROP + CT_ICMP_ERROR + CT_EXPECT_NEW + CT_EXPECT_CREATE + CT_EXPECT_DELETE + CT_SEARCH_RESTART +) + // NetIOCounters returnes network I/O statistics for every network // interface installed on the system. If pernic argument is false, // return only sum of all information (which name is 'all'). If true, @@ -232,6 +252,58 @@ func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { return stats, nil } +// ConntrackStats returns more detailed info about the conntrack table +func ConntrackStats(percpu bool) ([]ConntrackStat, error) { + return ConntrackStatsWithContext(context.Background(), percpu) +} + +// ConntrackStatsWithContext returns more detailed info about the conntrack table +func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) { + return conntrackStatsFromFile(common.HostProc("net/stat/nf_conntrack"), percpu) +} + +// conntrackStatsFromFile returns more detailed info about the conntrack table +// from `filename` +// If 'percpu' is false, the result will contain exactly one item with totals/summary +func conntrackStatsFromFile(filename string, percpu bool) ([]ConntrackStat, error) { + lines, err := common.ReadLines(filename) + if err != nil { + return nil, err + } + + statlist := NewConntrackStatList() + + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) == 17 && fields[0] != "entries" { + statlist.Append(NewConntrackStat( + common.HexToUint32(fields[CT_ENTRIES]), + common.HexToUint32(fields[CT_SEARCHED]), + common.HexToUint32(fields[CT_FOUND]), + common.HexToUint32(fields[CT_NEW]), + common.HexToUint32(fields[CT_INVALID]), + common.HexToUint32(fields[CT_IGNORE]), + common.HexToUint32(fields[CT_DELETE]), + common.HexToUint32(fields[CT_DELETE_LIST]), + common.HexToUint32(fields[CT_INSERT]), + common.HexToUint32(fields[CT_INSERT_FAILED]), + common.HexToUint32(fields[CT_DROP]), + common.HexToUint32(fields[CT_EARLY_DROP]), + common.HexToUint32(fields[CT_ICMP_ERROR]), + common.HexToUint32(fields[CT_EXPECT_NEW]), + common.HexToUint32(fields[CT_EXPECT_CREATE]), + common.HexToUint32(fields[CT_EXPECT_DELETE]), + common.HexToUint32(fields[CT_SEARCH_RESTART]), + )) + } + } + + if percpu { + return statlist.Items(), nil + } + return statlist.Summary(), nil +} + // http://students.mimuw.edu.pl/lxr/source/include/net/tcp_states.h var TCPStatuses = map[string]string{ "01": "ESTABLISHED", diff --git a/net/net_linux_test.go b/net/net_linux_test.go index 566a17b..cf19fcd 100644 --- a/net/net_linux_test.go +++ b/net/net_linux_test.go @@ -161,3 +161,142 @@ func TestReverse(t *testing.T) { src := []byte{0x01, 0x02, 0x03} assert.Equal(t, []byte{0x03, 0x02, 0x01}, Reverse(src)) } + +func TestConntrackStatFileParsing(t *testing.T) { + tmpfile, err := ioutil.TempFile("", "proc_net_stat_conntrack") + defer os.Remove(tmpfile.Name()) + assert.Nil(t, err, "Temporary file creation failed: ", err) + + data := []byte(` +entries searched found new invalid ignore delete delete_list insert insert_failed drop early_drop icmp_error expect_new expect_create expect_delete search_restart +0000007b 00000000 00000000 00000000 000b115a 00000084 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 0000004a +0000007b 00000000 00000000 00000000 0007eee5 00000068 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000035 +0000007b 00000000 00000000 00000000 0090346b 00000057 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000025 +0000007b 00000000 00000000 00000000 0005920f 00000069 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000064 +0000007b 00000000 00000000 00000000 000331ff 00000059 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 0000003b +0000007b 00000000 00000000 00000000 000314ea 00000066 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000054 +0000007b 00000000 00000000 00000000 0002b270 00000055 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 0000003d +0000007b 00000000 00000000 00000000 0002f67d 00000057 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000042 +`) + + // Expected results + slist := NewConntrackStatList() + + slist.Append(&ConntrackStat{ + Entries: 123, + Searched: 0, + Found: 0, + New: 0, + Invalid: 725338, + Ignore: 132, + Delete: 0, + DeleteList: 0, + Insert: 0, + InsertFailed: 0, + Drop: 0, + EarlyDrop: 0, + IcmpError: 0, + ExpectNew: 0, + ExpectCreate: 0, + ExpectDelete: 0, + SearchRestart: 74, + }) + slist.Append(&ConntrackStat{123, 0, 0, 0, 519909, 104, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 53}) + + slist.Append(&ConntrackStat{123, 0, 0, 0, 9450603, 87, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37}) + slist.Append(&ConntrackStat{123, 0, 0, 0, 365071, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100}) + + slist.Append(&ConntrackStat{123, 0, 0, 0, 209407, 89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59}) + slist.Append(&ConntrackStat{123, 0, 0, 0, 201962, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 84}) + + slist.Append(&ConntrackStat{123, 0, 0, 0, 176752, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 61}) + slist.Append(&ConntrackStat{123, 0, 0, 0, 194173, 87, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66}) + + // Write data to tempfile + _, err = tmpfile.Write(data) + assert.Nil(t, err, "Temporary file writing failed: ", err) + + // Function under test + stats, err := conntrackStatsFromFile(tmpfile.Name(), true) + assert.Equal(t, 8, len(stats), "Expected 8 results") + + summary := &ConntrackStat{} + for i, exp := range slist.Items() { + st := stats[i] + + assert.Equal(t, exp.Entries, st.Entries) + summary.Entries += st.Entries + + assert.Equal(t, exp.Searched, st.Searched) + summary.Searched += st.Searched + + assert.Equal(t, exp.Found, st.Found) + summary.Found += st.Found + + assert.Equal(t, exp.New, st.New) + summary.New += st.New + + assert.Equal(t, exp.Invalid, st.Invalid) + summary.Invalid += st.Invalid + + assert.Equal(t, exp.Ignore, st.Ignore) + summary.Ignore += st.Ignore + + assert.Equal(t, exp.Delete, st.Delete) + summary.Delete += st.Delete + + assert.Equal(t, exp.DeleteList, st.DeleteList) + summary.DeleteList += st.DeleteList + + assert.Equal(t, exp.Insert, st.Insert) + summary.Insert += st.Insert + + assert.Equal(t, exp.InsertFailed, st.InsertFailed) + summary.InsertFailed += st.InsertFailed + + assert.Equal(t, exp.Drop, st.Drop) + summary.Drop += st.Drop + + assert.Equal(t, exp.EarlyDrop, st.EarlyDrop) + summary.EarlyDrop += st.EarlyDrop + + assert.Equal(t, exp.IcmpError, st.IcmpError) + summary.IcmpError += st.IcmpError + + assert.Equal(t, exp.ExpectNew, st.ExpectNew) + summary.ExpectNew += st.ExpectNew + + assert.Equal(t, exp.ExpectCreate, st.ExpectCreate) + summary.ExpectCreate += st.ExpectCreate + + assert.Equal(t, exp.ExpectDelete, st.ExpectDelete) + summary.ExpectDelete += st.ExpectDelete + + assert.Equal(t, exp.SearchRestart, st.SearchRestart) + summary.SearchRestart += st.SearchRestart + } + + // Test summary grouping + totals, err := conntrackStatsFromFile(tmpfile.Name(), false) + for i, st := range totals { + assert.Equal(t, summary.Entries, st.Entries) + assert.Equal(t, summary.Searched, st.Searched) + assert.Equal(t, summary.Found, st.Found) + assert.Equal(t, summary.New, st.New) + assert.Equal(t, summary.Invalid, st.Invalid) + assert.Equal(t, summary.Ignore, st.Ignore) + assert.Equal(t, summary.Delete, st.Delete) + assert.Equal(t, summary.DeleteList, st.DeleteList) + assert.Equal(t, summary.Insert, st.Insert) + assert.Equal(t, summary.InsertFailed, st.InsertFailed) + assert.Equal(t, summary.Drop, st.Drop) + assert.Equal(t, summary.EarlyDrop, st.EarlyDrop) + assert.Equal(t, summary.IcmpError, st.IcmpError) + assert.Equal(t, summary.ExpectNew, st.ExpectNew) + assert.Equal(t, summary.ExpectCreate, st.ExpectCreate) + assert.Equal(t, summary.ExpectDelete, st.ExpectDelete) + assert.Equal(t, summary.SearchRestart, st.SearchRestart) + + assert.Equal(t, 0, i) // Should only have one element + } +} diff --git a/net/net_openbsd.go b/net/net_openbsd.go index 0faa70e..3cf0a89 100644 --- a/net/net_openbsd.go +++ b/net/net_openbsd.go @@ -156,6 +156,14 @@ func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { return nil, errors.New("NetFilterCounters not implemented for openbsd") } +func ConntrackStats(percpu bool) ([]ConntrackStat, error) { + return ConntrackStatsWithContext(context.Background(), percpu) +} + +func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) { + return nil, common.ErrNotImplementedError +} + // NetProtoCounters returns network statistics for the entire system // If protocols is empty then all protocols are returned, otherwise // just the protocols in the list are returned. diff --git a/net/net_windows.go b/net/net_windows.go index 61eb6ec..f63eb02 100644 --- a/net/net_windows.go +++ b/net/net_windows.go @@ -206,6 +206,15 @@ func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { return nil, errors.New("NetFilterCounters not implemented for windows") } +func ConntrackStats(percpu bool) ([]ConntrackStat, error) { + return ConntrackStatsWithContext(context.Background(), percpu) +} + +func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) { + return nil, common.ErrNotImplementedError +} + + // NetProtoCounters returns network statistics for the entire system // If protocols is empty then all protocols are returned, otherwise // just the protocols in the list are returned.