Skip to content

Commit f74105d

Browse files
committed
net/aix: implement ProtoCounters by parsing netstat -s
Replaces the ErrNotImplementedError stub with a real implementation that runs "netstat -s" and parses its output. Supported protocols and key mappings: tcp: OutSegs, InSegs, RetransSegs, ActiveOpens, PassiveOpens, AttemptFails, InCsumErrors, InErrs udp: InDatagrams, OutDatagrams, NoPorts, InCsumErrors, RcvbufErrors, InErrors ip: InReceives, InDelivers, ForwDatagrams, OutRequests, InHdrErrors, InUnknownProtos, InDiscards, OutNoRoutes, OutDiscards, ReasmOKs, ReasmReqds, ReasmFails, FragCreates ipv6: same as ip The parser strips parenthetical sub-counts (e.g. "6302116893 bytes") using strings.Index/LastIndex, and uses tab-indentation depth to disambiguate top-level totals from sub-lines with the same words. Tested on AIX 7.2 TL2 and AIX 7.3.
1 parent 5661255 commit f74105d

3 files changed

Lines changed: 545 additions & 2 deletions

File tree

net/net_aix.go

Lines changed: 234 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,240 @@ func ConntrackStatsWithContext(_ context.Context, _ bool) ([]ConntrackStat, erro
3131
return nil, common.ErrNotImplementedError
3232
}
3333

34-
func ProtoCountersWithContext(_ context.Context, _ []string) ([]ProtoCountersStat, error) {
35-
return nil, common.ErrNotImplementedError
34+
func ProtoCountersWithContext(ctx context.Context, protocols []string) ([]ProtoCountersStat, error) {
35+
out, err := invoke.CommandWithContext(ctx, "netstat", "-s")
36+
if err != nil {
37+
return nil, err
38+
}
39+
return parseNetstatS(string(out), protocols)
40+
}
41+
42+
// parseNetstatS parses "netstat -s" output on AIX.
43+
//
44+
// Format:
45+
//
46+
// <proto>:
47+
// \t<count> <description>
48+
// \t\t<count> <sub-description>
49+
//
50+
// Descriptions containing parenthetical sub-counts (e.g. "(6302116893 bytes)")
51+
// are normalised by stripping the parenthetical before matching.
52+
func parseNetstatS(output string, protocols []string) ([]ProtoCountersStat, error) {
53+
var stats []ProtoCountersStat
54+
var currentProto string
55+
var currentStats map[string]int64
56+
57+
for _, line := range strings.Split(output, "\n") {
58+
// Protocol header: no leading whitespace, ends with ":"
59+
if line != "" && line[0] != '\t' {
60+
if currentProto != "" && len(currentStats) > 0 {
61+
if len(protocols) == 0 || common.StringsHas(protocols, currentProto) {
62+
stats = append(stats, ProtoCountersStat{
63+
Protocol: currentProto,
64+
Stats: currentStats,
65+
})
66+
}
67+
}
68+
currentProto = strings.TrimSuffix(strings.TrimSpace(line), ":")
69+
currentStats = make(map[string]int64)
70+
continue
71+
}
72+
73+
if currentProto == "" {
74+
continue
75+
}
76+
77+
// Count leading tabs to track indentation depth (1 = top-level metric).
78+
depth := 0
79+
rest := line
80+
for rest != "" && rest[0] == '\t' {
81+
depth++
82+
rest = rest[1:]
83+
}
84+
if depth == 0 || rest == "" {
85+
continue
86+
}
87+
88+
// Split "<number> <description>".
89+
spaceIdx := strings.IndexByte(rest, ' ')
90+
if spaceIdx <= 0 {
91+
continue
92+
}
93+
val, err := strconv.ParseInt(rest[:spaceIdx], 10, 64)
94+
if err != nil {
95+
continue
96+
}
97+
// Normalise: remove parenthetical sub-counts like "(6302116893 bytes)".
98+
desc := normaliseNetstatDesc(strings.TrimSpace(rest[spaceIdx+1:]))
99+
100+
if key := aixProtoKey(currentProto, depth, desc); key != "" {
101+
currentStats[key] += val
102+
}
103+
}
104+
105+
if currentProto != "" && len(currentStats) > 0 {
106+
if len(protocols) == 0 || common.StringsHas(protocols, currentProto) {
107+
stats = append(stats, ProtoCountersStat{
108+
Protocol: currentProto,
109+
Stats: currentStats,
110+
})
111+
}
112+
}
113+
114+
return stats, nil
115+
}
116+
117+
// normaliseNetstatDesc strips a single parenthetical from a netstat -s description,
118+
// e.g. "data packets (6302116893 bytes) retransmitted" → "data packets retransmitted".
119+
func normaliseNetstatDesc(s string) string {
120+
start := strings.Index(s, "(")
121+
if start == -1 {
122+
return s
123+
}
124+
end := strings.LastIndex(s, ")")
125+
if end < start {
126+
return s
127+
}
128+
return strings.Join(strings.Fields(s[:start]+s[end+1:]), " ")
129+
}
130+
131+
// aixProtoKey maps a normalised AIX netstat -s description to a ProtoCountersStat key.
132+
// depth is the tab-indentation level (1 = top-level line under the protocol header).
133+
// Returns "" for lines that should be ignored.
134+
func aixProtoKey(proto string, depth int, desc string) string {
135+
switch proto {
136+
case "tcp":
137+
return aixTCPKey(depth, desc)
138+
case "udp":
139+
return aixUDPKey(desc)
140+
case "ip":
141+
return aixIPKey(depth, desc)
142+
case "ipv6":
143+
return aixIPv6Key(depth, desc)
144+
}
145+
return ""
146+
}
147+
148+
func aixTCPKey(depth int, desc string) string {
149+
switch {
150+
// Top-level totals — depth check avoids matching sub-lines like "N data packets sent".
151+
case depth == 1 && desc == "packets sent":
152+
return "OutSegs"
153+
case depth == 1 && desc == "packets received":
154+
return "InSegs"
155+
// Sub-line: "data packets NNN bytes retransmitted" → normalised "data packets retransmitted"
156+
case strings.Contains(desc, "retransmitted"):
157+
return "RetransSegs"
158+
case desc == "connection requests":
159+
return "ActiveOpens"
160+
case desc == "connection accepts":
161+
return "PassiveOpens"
162+
case desc == "embryonic connections dropped":
163+
return "AttemptFails"
164+
case strings.HasPrefix(desc, "discarded for bad checksums"):
165+
return "InCsumErrors"
166+
// Other input errors accumulate into InErrs.
167+
case strings.HasPrefix(desc, "discarded for bad header") ||
168+
strings.HasPrefix(desc, "discarded because packet too short"):
169+
return "InErrs"
170+
}
171+
return ""
172+
}
173+
174+
func aixUDPKey(desc string) string {
175+
switch {
176+
case desc == "delivered":
177+
return "InDatagrams"
178+
// Both unicast and broadcast "dropped due to no socket" map to NoPorts.
179+
case strings.Contains(desc, "dropped due to no socket"):
180+
return "NoPorts"
181+
case desc == "bad checksums":
182+
return "InCsumErrors"
183+
case desc == "socket buffer overflows":
184+
return "RcvbufErrors"
185+
case desc == "datagrams output":
186+
return "OutDatagrams"
187+
case desc == "incomplete headers" || desc == "bad data length fields":
188+
return "InErrors"
189+
}
190+
return ""
191+
}
192+
193+
func aixIPKey(depth int, desc string) string {
194+
switch {
195+
case desc == "total packets received":
196+
return "InReceives"
197+
// All header-error variants accumulate into InHdrErrors.
198+
case desc == "bad header checksums" ||
199+
desc == "with size smaller than minimum" ||
200+
desc == "with data size < data length" ||
201+
desc == "with header length < data size" ||
202+
desc == "with data length < header length" ||
203+
desc == "with bad options" ||
204+
desc == "with incorrect version number":
205+
return "InHdrErrors"
206+
case strings.Contains(desc, "unknown/unsupported protocol"):
207+
return "InUnknownProtos"
208+
case desc == "packets for this host":
209+
return "InDelivers"
210+
case depth == 1 && desc == "packets forwarded":
211+
return "ForwDatagrams"
212+
case desc == "packets sent from this host":
213+
return "OutRequests"
214+
case desc == "packets reassembled ok":
215+
return "ReasmOKs"
216+
case strings.HasPrefix(desc, "fragments dropped after timeout"):
217+
return "ReasmFails"
218+
case desc == "fragments received":
219+
return "ReasmReqds"
220+
case strings.HasPrefix(desc, "fragments dropped"):
221+
return "InDiscards"
222+
case desc == "fragments created":
223+
return "FragCreates"
224+
case desc == "output packets discarded due to no route":
225+
return "OutNoRoutes"
226+
case strings.HasPrefix(desc, "output packets dropped due to no bufs"):
227+
return "OutDiscards"
228+
}
229+
return ""
230+
}
231+
232+
func aixIPv6Key(depth int, desc string) string {
233+
switch {
234+
case desc == "total packets received":
235+
return "InReceives"
236+
case desc == "with size smaller than minimum" ||
237+
desc == "with data size < data length" ||
238+
desc == "with incorrect version number" ||
239+
desc == "with illegal source":
240+
return "InHdrErrors"
241+
case strings.Contains(desc, "unknown/unsupported protocol"):
242+
return "InUnknownProtos"
243+
case desc == "input packets without enough memory":
244+
return "InDiscards"
245+
case desc == "packets for this host":
246+
return "InDelivers"
247+
case depth == 1 && desc == "packets forwarded":
248+
return "ForwDatagrams"
249+
case desc == "packets sent from this host":
250+
return "OutRequests"
251+
case desc == "packets reassembled ok":
252+
return "ReasmOKs"
253+
case strings.HasPrefix(desc, "fragments dropped after timeout"):
254+
return "ReasmFails"
255+
case desc == "fragments received":
256+
return "ReasmReqds"
257+
case strings.HasPrefix(desc, "fragments dropped"):
258+
return "InDiscards"
259+
case desc == "fragments created":
260+
return "FragCreates"
261+
case desc == "output packets discarded due to no route":
262+
return "OutNoRoutes"
263+
case strings.HasPrefix(desc, "output packets dropped due to no bufs") ||
264+
desc == "output packets without enough memory":
265+
return "OutDiscards"
266+
}
267+
return ""
36268
}
37269

38270
func parseNetstatNetLine(line string) (ConnectionStat, error) {

net/net_aix_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
//go:build aix
3+
4+
package net
5+
6+
import (
7+
"os"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestParseNetstatS(t *testing.T) {
15+
data, err := os.ReadFile("testdata/aix/netstat_s.txt")
16+
require.NoError(t, err)
17+
18+
stats, err := parseNetstatS(string(data), nil)
19+
require.NoError(t, err)
20+
21+
protos := map[string]ProtoCountersStat{}
22+
for _, s := range stats {
23+
protos[s.Protocol] = s
24+
}
25+
26+
var gotProtocols []string
27+
for p := range protos {
28+
gotProtocols = append(gotProtocols, p)
29+
}
30+
assert.ElementsMatch(t, []string{"tcp", "udp", "ip", "ipv6"}, gotProtocols)
31+
32+
t.Run("tcp", func(t *testing.T) {
33+
tcp, ok := protos["tcp"]
34+
require.True(t, ok, "tcp section not found")
35+
assert.Equal(t, map[string]int64{
36+
"OutSegs": 1001,
37+
"InSegs": 2002,
38+
"RetransSegs": 303,
39+
"ActiveOpens": 404,
40+
"PassiveOpens": 505,
41+
"AttemptFails": 606,
42+
"InCsumErrors": 707,
43+
"InErrs": 1603, // 801 (bad header offset) + 802 (packet too short)
44+
}, tcp.Stats)
45+
})
46+
47+
t.Run("udp", func(t *testing.T) {
48+
udp, ok := protos["udp"]
49+
require.True(t, ok, "udp section not found")
50+
assert.Equal(t, map[string]int64{
51+
"InDatagrams": 1100,
52+
"OutDatagrams": 2200,
53+
"NoPorts": 7700, // 3300 (unicast) + 4400 (broadcast)
54+
"InCsumErrors": 5500,
55+
"RcvbufErrors": 6600,
56+
"InErrors": 330, // 110 (incomplete headers) + 220 (bad data length)
57+
}, udp.Stats)
58+
})
59+
60+
t.Run("ip", func(t *testing.T) {
61+
ip, ok := protos["ip"]
62+
require.True(t, ok, "ip section not found")
63+
assert.Equal(t, map[string]int64{
64+
"InReceives": 50000,
65+
"InDelivers": 40000,
66+
"OutRequests": 30000,
67+
"InHdrErrors": 308, // 11+22+33+44+55+66+77 (seven header-error lines)
68+
"InUnknownProtos": 9000,
69+
"ForwDatagrams": 100,
70+
"ReasmOKs": 200,
71+
"ReasmReqds": 900,
72+
"ReasmFails": 300,
73+
"InDiscards": 500,
74+
"FragCreates": 600,
75+
"OutNoRoutes": 700,
76+
"OutDiscards": 800,
77+
}, ip.Stats)
78+
})
79+
80+
t.Run("ipv6", func(t *testing.T) {
81+
ipv6, ok := protos["ipv6"]
82+
require.True(t, ok, "ipv6 section not found")
83+
assert.Equal(t, map[string]int64{
84+
"InReceives": 60000,
85+
"InDelivers": 55000,
86+
"OutRequests": 45000,
87+
"ForwDatagrams": 110,
88+
"ReasmOKs": 220,
89+
"ReasmReqds": 990,
90+
"ReasmFails": 330,
91+
"InDiscards": 651, // 550 (fragments) + 101 (input no memory)
92+
"FragCreates": 660,
93+
"OutNoRoutes": 770,
94+
"InHdrErrors": 110, // 11+22+33+44 (four header-error lines)
95+
"InUnknownProtos": 880,
96+
"OutDiscards": 777, // 333 (no bufs) + 444 (no memory)
97+
}, ipv6.Stats)
98+
})
99+
}
100+
101+
func TestParseNetstatSFiltered(t *testing.T) {
102+
data, err := os.ReadFile("testdata/aix/netstat_s.txt")
103+
require.NoError(t, err)
104+
105+
stats, err := parseNetstatS(string(data), []string{"tcp", "udp"})
106+
require.NoError(t, err)
107+
108+
protos := map[string]ProtoCountersStat{}
109+
for _, s := range stats {
110+
protos[s.Protocol] = s
111+
}
112+
assert.Contains(t, protos, "tcp")
113+
assert.Contains(t, protos, "udp")
114+
assert.NotContains(t, protos, "ip")
115+
assert.NotContains(t, protos, "ipv6")
116+
}
117+
118+
func TestNormaliseNetstatDesc(t *testing.T) {
119+
cases := []struct {
120+
input string
121+
want string
122+
}{
123+
{"packets sent", "packets sent"},
124+
{"data packets (6302116893 bytes) retransmitted", "data packets retransmitted"},
125+
{"823508 segments updated rtt (of 120622 attempts)", "823508 segments updated rtt"},
126+
{"connections closed (including 74 drops)", "connections closed"},
127+
}
128+
for _, tc := range cases {
129+
assert.Equal(t, tc.want, normaliseNetstatDesc(tc.input), "input: %q", tc.input)
130+
}
131+
}

0 commit comments

Comments
 (0)