@@ -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
38270func parseNetstatNetLine (line string ) (ConnectionStat , error ) {
0 commit comments