Skip to content

Commit 75f1291

Browse files
committed
Add network connection retrieval for AIX platform
1 parent e699d49 commit 75f1291

2 files changed

Lines changed: 374 additions & 2 deletions

File tree

net/net_aix.go

Lines changed: 287 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package net
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"regexp"
1011
"strconv"
@@ -295,6 +296,290 @@ func ConnectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, p
295296
return connectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, maxConn, true)
296297
}
297298

298-
func connectionsPidMaxWithoutUidsWithContext(_ context.Context, _ string, _ int32, _ int, _ bool) ([]ConnectionStat, error) {
299-
return []ConnectionStat{}, common.ErrNotImplementedError
299+
func connectionsPidMaxWithoutUidsWithContext(ctx context.Context, _ string, pid int32, maxConn int, _ bool) ([]ConnectionStat, error) {
300+
// If pid is 0, return all connections
301+
if pid == 0 {
302+
return getAIXConnections(ctx, maxConn)
303+
}
304+
305+
// For specific PID, filter connections to just that process
306+
return getAIXConnectionsForPid(ctx, pid, maxConn)
307+
}
308+
309+
// getAIXConnections retrieves all network connections from AIX
310+
func getAIXConnections(ctx context.Context, maxConn int) ([]ConnectionStat, error) {
311+
var conns []ConnectionStat
312+
313+
// Get all listening sockets using netstat
314+
output, err := invoke.CommandWithContext(ctx, "netstat", "-Aan")
315+
if err != nil {
316+
return conns, err
317+
}
318+
319+
lines := strings.Split(string(output), "\n")
320+
count := 0
321+
322+
for _, line := range lines {
323+
if maxConn > 0 && count >= maxConn {
324+
break
325+
}
326+
327+
line = strings.TrimSpace(line)
328+
if line == "" || strings.HasPrefix(line, "Address") {
329+
continue
330+
}
331+
332+
// Parse netstat output to find sockets
333+
// Format: Address Family Type Use Recv-Q Send-Q Inode Conn Routes
334+
fields := strings.Fields(line)
335+
if len(fields) < 2 {
336+
continue
337+
}
338+
339+
sockAddr := fields[0]
340+
341+
// Try to resolve the socket to get connection info using rmsock
342+
// For TCP connections
343+
connStat, err := resolveAIXSockToConnection(ctx, sockAddr, "tcp")
344+
if err == nil && connStat != nil {
345+
conns = append(conns, *connStat)
346+
count++
347+
continue
348+
}
349+
350+
// Try for UDP connections
351+
connStat, err = resolveAIXSockToConnection(ctx, sockAddr, "udp")
352+
if err == nil && connStat != nil {
353+
conns = append(conns, *connStat)
354+
count++
355+
}
356+
}
357+
358+
return conns, nil
359+
}
360+
361+
// getAIXConnectionsForPid retrieves network connections for a specific process on AIX
362+
func getAIXConnectionsForPid(ctx context.Context, pid int32, maxConn int) ([]ConnectionStat, error) {
363+
var conns []ConnectionStat
364+
365+
// Get all listening sockets using netstat
366+
output, err := invoke.CommandWithContext(ctx, "netstat", "-Aan")
367+
if err != nil {
368+
return conns, err
369+
}
370+
371+
lines := strings.Split(string(output), "\n")
372+
count := 0
373+
374+
for _, line := range lines {
375+
if maxConn > 0 && count >= maxConn {
376+
break
377+
}
378+
379+
line = strings.TrimSpace(line)
380+
if line == "" || strings.HasPrefix(line, "Address") || strings.HasPrefix(line, "PCB/ADDR") {
381+
continue
382+
}
383+
384+
// Parse netstat output: PCB/ADDR Proto Recv-Q Send-Q Local Address Foreign Address (state)
385+
// Example: f1000f00055cc3c0 tcp4 0 0 192.168.242.122.22 24.236.207.124.40326 ESTABLISHED
386+
fields := strings.Fields(line)
387+
if len(fields) < 7 {
388+
continue
389+
}
390+
391+
sockAddr := fields[0]
392+
proto := fields[1]
393+
localAddr := fields[4]
394+
remoteAddr := fields[5]
395+
state := fields[6]
396+
397+
// Determine protocol type (tcp or udp)
398+
var protocol string
399+
switch {
400+
case strings.HasPrefix(proto, "tcp"):
401+
protocol = "tcp"
402+
case strings.HasPrefix(proto, "udp"):
403+
protocol = "udp"
404+
default:
405+
continue
406+
}
407+
408+
// Try to resolve the socket to get PID using rmsock
409+
resolvedPid := resolveAIXSockToPid(ctx, sockAddr, protocol)
410+
if resolvedPid != pid {
411+
// This connection doesn't belong to our target PID
412+
continue
413+
}
414+
415+
// Parse addresses
416+
laddr := parseAIXAddress(localAddr)
417+
raddr := parseAIXAddress(remoteAddr)
418+
419+
// Determine socket type and family
420+
var socketType uint32
421+
var socketFamily uint32
422+
423+
// Set socket type based on protocol
424+
if protocol == "tcp" {
425+
socketType = syscall.SOCK_STREAM
426+
} else {
427+
socketType = syscall.SOCK_DGRAM
428+
}
429+
430+
// Set socket family based on proto string (tcp4, tcp6, udp4, udp6)
431+
if strings.HasSuffix(proto, "6") {
432+
socketFamily = syscall.AF_INET6
433+
} else {
434+
socketFamily = syscall.AF_INET
435+
}
436+
437+
connStat := ConnectionStat{
438+
Fd: 0,
439+
Family: socketFamily,
440+
Type: socketType,
441+
Laddr: laddr,
442+
Raddr: raddr,
443+
Status: state,
444+
Pid: pid,
445+
}
446+
447+
conns = append(conns, connStat)
448+
count++
449+
}
450+
451+
return conns, nil
452+
}
453+
454+
// resolveAIXSockToConnection uses AIX rmsock command to resolve a socket address to connection info
455+
func resolveAIXSockToConnection(ctx context.Context, sockAddr, protocol string) (*ConnectionStat, error) {
456+
if protocol != "tcp" && protocol != "udp" {
457+
return nil, fmt.Errorf("unsupported protocol: %s", protocol)
458+
}
459+
460+
// Execute rmsock to resolve socket
461+
// Format for TCP: rmsock <socket_address> tcpcb
462+
// Format for UDP: rmsock <socket_address> inpcb
463+
var tcpOrUDP string
464+
if protocol == "tcp" {
465+
tcpOrUDP = "tcpcb"
466+
} else {
467+
tcpOrUDP = "inpcb"
468+
}
469+
470+
output, err := invoke.CommandWithContext(ctx, "rmsock", sockAddr, tcpOrUDP)
471+
if err != nil {
472+
return nil, err
473+
}
474+
475+
// Parse rmsock output to extract connection info
476+
outputStr := string(output)
477+
478+
// Try to find PID in the output
479+
pid := parseAIXRmsockPid(outputStr)
480+
if pid == 0 {
481+
return nil, errors.New("could not extract PID from rmsock output")
482+
}
483+
484+
// Build connection stat from parsed info
485+
connStat := &ConnectionStat{
486+
Fd: 0,
487+
Family: 0,
488+
Type: 0,
489+
Laddr: Addr{IP: "", Port: 0},
490+
Raddr: Addr{IP: "", Port: 0},
491+
Status: "",
492+
Pid: pid,
493+
}
494+
495+
return connStat, nil
496+
}
497+
498+
// resolveAIXSockToConnectionForPid resolves socket to connection only if it matches the target PID
499+
func resolveAIXSockToConnectionForPid(ctx context.Context, sockAddr, protocol string, targetPid int32) (*ConnectionStat, error) {
500+
connStat, err := resolveAIXSockToConnection(ctx, sockAddr, protocol)
501+
if err != nil {
502+
return nil, err
503+
}
504+
505+
if connStat == nil {
506+
return nil, errors.New("connection stat is nil")
507+
}
508+
509+
if connStat.Pid != targetPid {
510+
return nil, fmt.Errorf("PID mismatch: expected %d, got %d", targetPid, connStat.Pid)
511+
}
512+
513+
return connStat, nil
514+
}
515+
516+
// parseAIXRmsockPid extracts PID from rmsock output
517+
// Expected format: "The socket 0xf1000f00055be808 is being held by process 14287304 (sshd)."
518+
func parseAIXRmsockPid(output string) int32 {
519+
// Use regex to extract PID from rmsock output
520+
// Pattern: "process <PID> ("
521+
re := regexp.MustCompile(`process\s+(\d+)\s+\(`)
522+
matches := re.FindStringSubmatch(output)
523+
if len(matches) > 1 {
524+
if pid, err := strconv.ParseInt(matches[1], 10, 32); err == nil {
525+
return int32(pid)
526+
}
527+
}
528+
return 0
529+
}
530+
531+
// resolveAIXSockToPid uses rmsock to get the PID holding a socket, returns 0 if unable to resolve
532+
func resolveAIXSockToPid(ctx context.Context, sockAddr, protocol string) int32 {
533+
if protocol != "tcp" && protocol != "udp" {
534+
return 0
535+
}
536+
537+
var tcpOrUDP string
538+
if protocol == "tcp" {
539+
tcpOrUDP = "tcpcb"
540+
} else {
541+
tcpOrUDP = "inpcb"
542+
}
543+
544+
output, err := invoke.CommandWithContext(ctx, "rmsock", sockAddr, tcpOrUDP)
545+
// Note: rmsock may exit with status 1 even on successful resolution
546+
// So we try to parse the output regardless of error status
547+
548+
outputStr := string(output)
549+
pid := parseAIXRmsockPid(outputStr)
550+
551+
if pid == 0 && err != nil {
552+
// If we got a "Wait for exiting processes" message, it's a transient cleanup situation - skip silently
553+
if strings.Contains(outputStr, "Wait for exiting processes") {
554+
return 0
555+
}
556+
// For other errors, log debug info if we couldn't parse a PID
557+
// Uncomment for debugging: fmt.Fprintf(os.Stderr, "DEBUG: rmsock %s %s failed: %v, output: %s\n", sockAddr, tcpOrUdp, err, outputStr)
558+
}
559+
560+
return pid
561+
}
562+
563+
// parseAIXAddress parses an AIX address string like "192.168.242.122.22" or "24.236.207.124.40326"
564+
// Format: IP_OCTETS separated by dots, with port as last octet(s) after the IP
565+
func parseAIXAddress(addrStr string) Addr {
566+
if addrStr == "*.*" {
567+
return Addr{IP: "", Port: 0}
568+
}
569+
570+
parts := strings.Split(addrStr, ".")
571+
if len(parts) < 2 {
572+
return Addr{IP: "", Port: 0}
573+
}
574+
575+
// Last part is the port
576+
port := 0
577+
if p, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
578+
port = p
579+
}
580+
581+
// Join all but last part as IP
582+
ip := strings.Join(parts[:len(parts)-1], ".")
583+
584+
return Addr{IP: ip, Port: uint32(port)}
300585
}

net/net_aix_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
//go:build aix
3+
4+
package net
5+
6+
import (
7+
"context"
8+
"os"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestConnectionsPidWithContext(t *testing.T) {
16+
ctx := context.Background()
17+
pid := int32(os.Getpid())
18+
19+
conns, err := ConnectionsPidWithContext(ctx, "inet", pid)
20+
// Process may not have any connections, check result gracefully
21+
if err != nil {
22+
// It's OK if the function returns an error
23+
t.Logf("ConnectionsPidWithContext error: %v", err)
24+
return
25+
}
26+
// If successful, verify structure
27+
if conns != nil {
28+
assert.IsType(t, []ConnectionStat{}, conns)
29+
for _, conn := range conns {
30+
// Verify connection fields are populated
31+
assert.NotEmpty(t, conn.Family)
32+
assert.NotEmpty(t, conn.Type)
33+
}
34+
}
35+
}
36+
37+
func TestConnectionsPidWithContextAll(t *testing.T) {
38+
ctx := context.Background()
39+
pid := int32(os.Getpid())
40+
41+
// Test with "all" family
42+
conns, err := ConnectionsPidWithContext(ctx, "all", pid)
43+
if err != nil {
44+
// It's OK if the function returns an error
45+
t.Logf("ConnectionsPidWithContext error: %v", err)
46+
return
47+
}
48+
if conns != nil {
49+
assert.IsType(t, []ConnectionStat{}, conns)
50+
}
51+
}
52+
53+
func TestConnectionsPidWithContextUDP(t *testing.T) {
54+
ctx := context.Background()
55+
pid := int32(os.Getpid())
56+
57+
// Test with UDP connections
58+
conns, err := ConnectionsPidWithContext(ctx, "udp", pid)
59+
if err != nil {
60+
// It's OK if the function returns an error
61+
t.Logf("ConnectionsPidWithContext error: %v", err)
62+
return
63+
}
64+
if conns != nil {
65+
assert.IsType(t, []ConnectionStat{}, conns)
66+
}
67+
}
68+
69+
func TestConnectionsWithContext(t *testing.T) {
70+
ctx := context.Background()
71+
72+
// Test getting all connections
73+
conns, err := ConnectionsWithContext(ctx, "inet")
74+
require.NoError(t, err)
75+
assert.NotNil(t, conns)
76+
assert.IsType(t, []ConnectionStat{}, conns)
77+
78+
// Should have at least some connections
79+
assert.NotEmpty(t, conns)
80+
81+
for _, conn := range conns {
82+
// Verify connection fields
83+
assert.NotEmpty(t, conn.Family)
84+
assert.NotEmpty(t, conn.Type)
85+
assert.GreaterOrEqual(t, conn.Pid, int32(0))
86+
}
87+
}

0 commit comments

Comments
 (0)