blob: 514e92813effa8b4c77b65bd81264424eefc1d27 [file] [log] [blame]
Sean McCullough364f7412025-06-02 00:55:44 +00001package loop
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "os/exec"
8 "strings"
9 "sync"
10 "time"
11)
12
13// PortMonitor handles periodic monitoring of listening ports in containers
14type PortMonitor struct {
15 mu sync.Mutex // protects following
16 lastPorts string // last netstat/ss output for comparison
17}
18
19// NewPortMonitor creates a new PortMonitor instance
20func NewPortMonitor() *PortMonitor {
21 return &PortMonitor{}
22}
23
24// Start begins periodic port monitoring in a background goroutine
25func (pm *PortMonitor) Start(ctx context.Context) {
26 go func() {
27 ticker := time.NewTicker(5 * time.Second) // Check every 5 seconds
28 defer ticker.Stop()
29
30 // Get initial port state
31 pm.updatePortState(ctx)
32
33 for {
34 select {
35 case <-ctx.Done():
36 return
37 case <-ticker.C:
38 pm.updatePortState(ctx)
39 }
40 }
41 }()
42}
43
44// updatePortState runs ss and checks for changes in listening ports
45func (pm *PortMonitor) updatePortState(ctx context.Context) {
46 cmd := exec.CommandContext(ctx, "ss", "-lntu")
47 output, err := cmd.Output()
48 if err != nil {
49 // Log the error but don't fail - port monitoring is not critical
50 slog.DebugContext(ctx, "Failed to run ss command", "error", err)
51 return
52 }
53
54 currentPorts := string(output)
55
56 pm.mu.Lock()
57 defer pm.mu.Unlock()
58
59 // Check if ports have changed
60 if pm.lastPorts != "" && pm.lastPorts != currentPorts {
61 // Ports have changed, log the difference
62 slog.InfoContext(ctx, "Container port changes detected",
63 slog.String("previous_ports", pm.lastPorts),
64 slog.String("current_ports", currentPorts))
65
66 // Parse and compare the port lists for more detailed logging
67 pm.logPortDifferences(ctx, pm.lastPorts, currentPorts)
68 }
69
70 pm.lastPorts = currentPorts
71}
72
73// logPortDifferences parses ss output and logs specific port changes
74func (pm *PortMonitor) logPortDifferences(ctx context.Context, oldPorts, newPorts string) {
75 oldPortSet := parseSSPorts(oldPorts)
76 newPortSet := parseSSPorts(newPorts)
77
78 // Find newly opened ports
79 for port := range newPortSet {
80 if !oldPortSet[port] {
81 slog.InfoContext(ctx, "New port detected", slog.String("port", port))
82 }
83 }
84
85 // Find closed ports
86 for port := range oldPortSet {
87 if !newPortSet[port] {
88 slog.InfoContext(ctx, "Port closed", slog.String("port", port))
89 }
90 }
91}
92
93// parseSSPorts extracts listening ports from ss -lntu output
94// Returns a map with "proto:address:port" as keys
95// ss output format: Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
96func parseSSPorts(output string) map[string]bool {
97 ports := make(map[string]bool)
98 lines := strings.Split(output, "\n")
99
100 for _, line := range lines {
101 fields := strings.Fields(line)
102 if len(fields) < 5 {
103 continue
104 }
105
106 // Skip header line and non-LISTEN states
107 if fields[0] == "Netid" || fields[1] != "LISTEN" {
108 continue
109 }
110
111 proto := fields[0]
112 localAddr := fields[4] // Local Address:Port
113 portKey := fmt.Sprintf("%s:%s", proto, localAddr)
114 ports[portKey] = true
115 }
116
117 return ports
118}