blob: e0f7ff1e06705406c490ac534a023ed7f73e7dd8 [file] [log] [blame]
Sean McCullough364f7412025-06-02 00:55:44 +00001package loop
2
3import (
4 "context"
bankseancff0ff82025-06-25 16:43:47 +00005 "os"
6 "strings"
Sean McCullough364f7412025-06-02 00:55:44 +00007 "testing"
Sean McCullough138ec242025-06-02 22:42:06 +00008 "time"
Sean McCullough364f7412025-06-02 00:55:44 +00009)
10
11// TestPortMonitoring tests the port monitoring functionality
12func TestPortMonitoring(t *testing.T) {
13 // Test with ss output format
14 ssOutput := `Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
15tcp LISTEN 0 1024 127.0.0.1:40975 0.0.0.0:*
16tcp LISTEN 0 4096 *:22 *:*
17tcp LISTEN 0 4096 *:80 *:*
18udp UNCONN 0 0 127.0.0.1:123 0.0.0.0:*
19`
20
21 expected := map[string]bool{
22 "tcp:127.0.0.1:40975": true,
23 "tcp:*:22": true,
24 "tcp:*:80": true,
25 }
26
27 result := parseSSPorts(ssOutput)
28
29 // Check that all expected ports are found
30 for port := range expected {
31 if !result[port] {
32 t.Errorf("Expected port %s not found in ss parsed output", port)
33 }
34 }
35
36 // Check that UDP port is not included (since it's UNCONN, not LISTEN)
37 if result["udp:127.0.0.1:123"] {
38 t.Errorf("UDP UNCONN port should not be included in listening ports")
39 }
40
41 // Check that no extra ports are found
42 for port := range result {
43 if !expected[port] {
44 t.Errorf("Unexpected port %s found in parsed output", port)
45 }
46 }
47}
48
49// TestPortMonitoringLogDifferences tests the port difference logging
50func TestPortMonitoringLogDifferences(t *testing.T) {
51 ctx := context.Background()
52
53 oldPorts := `Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
54tcp LISTEN 0 4096 *:22 *:*
55tcp LISTEN 0 1024 127.0.0.1:8080 0.0.0.0:*
56`
57
58 newPorts := `Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
59tcp LISTEN 0 4096 *:22 *:*
60tcp LISTEN 0 1024 127.0.0.1:9090 0.0.0.0:*
61`
62
63 // Create a port monitor to test the logPortDifferences method
64 pm := NewPortMonitor()
65
66 // This test mainly ensures the method doesn't panic and processes the differences
67 // The actual logging output would need to be captured via a test logger to verify fully
68 pm.logPortDifferences(ctx, oldPorts, newPorts)
69
70 // Test with no differences
71 pm.logPortDifferences(ctx, oldPorts, oldPorts)
72}
73
74// TestPortMonitorCreation tests creating a new port monitor
75func TestPortMonitorCreation(t *testing.T) {
76 pm := NewPortMonitor()
77 if pm == nil {
78 t.Error("NewPortMonitor() returned nil")
79 }
80
81 // Verify initial state
82 pm.mu.Lock()
83 defer pm.mu.Unlock()
84 if pm.lastPorts != "" {
85 t.Error("NewPortMonitor() should have empty lastPorts initially")
86 }
87}
88
89// TestParseSSPortsEdgeCases tests edge cases in ss output parsing
90func TestParseSSPortsEdgeCases(t *testing.T) {
91 tests := []struct {
92 name string
93 output string
94 expected map[string]bool
95 }{
96 {
97 name: "empty output",
98 output: "",
99 expected: map[string]bool{},
100 },
101 {
102 name: "header only",
103 output: "Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess",
104 expected: map[string]bool{},
105 },
106 {
107 name: "non-listen states filtered out",
108 output: "tcp ESTAB 0 0 127.0.0.1:8080 127.0.0.1:45678\nudp UNCONN 0 0 127.0.0.1:123 0.0.0.0:*",
109 expected: map[string]bool{},
110 },
111 {
112 name: "insufficient fields",
113 output: "tcp LISTEN 0",
114 expected: map[string]bool{},
115 },
116 }
117
118 for _, test := range tests {
119 t.Run(test.name, func(t *testing.T) {
120 result := parseSSPorts(test.output)
121 if len(result) != len(test.expected) {
122 t.Errorf("Expected %d ports, got %d", len(test.expected), len(result))
123 }
124 for port := range test.expected {
125 if !result[port] {
126 t.Errorf("Expected port %s not found", port)
127 }
128 }
129 for port := range result {
130 if !test.expected[port] {
131 t.Errorf("Unexpected port %s found", port)
132 }
133 }
134 })
135 }
136}
Sean McCullough138ec242025-06-02 22:42:06 +0000137
138// TestPortEventStorage tests the new event storage functionality
139func TestPortEventStorage(t *testing.T) {
140 pm := NewPortMonitor()
141
142 // Initially should have no events
143 allEvents := pm.GetAllRecentEvents()
144 if len(allEvents) != 0 {
145 t.Errorf("Expected 0 events initially, got %d", len(allEvents))
146 }
147
148 // Simulate port changes that would add events
149 ctx := context.Background()
150 oldPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*"
151 newPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp LISTEN 0 128 0.0.0.0:9090 0.0.0.0:*"
152
153 pm.logPortDifferences(ctx, oldPorts, newPorts)
154
155 // Should now have events
156 allEvents = pm.GetAllRecentEvents()
157 if len(allEvents) != 2 {
158 t.Errorf("Expected 2 events (1 opened, 1 closed), got %d", len(allEvents))
159 }
160
161 // Check event types
162 foundOpened := false
163 foundClosed := false
164 for _, event := range allEvents {
165 if event.Type == "opened" && event.Port == "tcp:0.0.0.0:9090" {
166 foundOpened = true
167 }
168 if event.Type == "closed" && event.Port == "tcp:0.0.0.0:8080" {
169 foundClosed = true
170 }
171 }
172
173 if !foundOpened {
174 t.Error("Expected to find 'opened' event for port tcp:0.0.0.0:9090")
175 }
176 if !foundClosed {
177 t.Error("Expected to find 'closed' event for port tcp:0.0.0.0:8080")
178 }
179}
180
181// TestPortEventFiltering tests the time-based filtering
182func TestPortEventFiltering(t *testing.T) {
183 pm := NewPortMonitor()
184 ctx := context.Background()
185
186 // Record time before adding events
187 beforeTime := time.Now()
188 time.Sleep(1 * time.Millisecond) // Small delay to ensure timestamp difference
189
190 // Add some events
191 oldPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*"
192 newPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp LISTEN 0 128 0.0.0.0:9090 0.0.0.0:*"
193 pm.logPortDifferences(ctx, oldPorts, newPorts)
194
195 // Get events since beforeTime - should get all events
196 recentEvents := pm.GetRecentEvents(beforeTime)
197 if len(recentEvents) != 2 {
198 t.Errorf("Expected 2 recent events, got %d", len(recentEvents))
199 }
200
201 // Get events since now - should get no events
202 nowTime := time.Now()
203 recentEvents = pm.GetRecentEvents(nowTime)
204 if len(recentEvents) != 0 {
205 t.Errorf("Expected 0 recent events since now, got %d", len(recentEvents))
206 }
207}
bankseancff0ff82025-06-25 16:43:47 +0000208
209// TestParseAddress tests the hex address parsing for /proc/net files
210func TestParseAddress(t *testing.T) {
211 pm := NewPortMonitor()
212
213 tests := []struct {
214 name string
215 addrPort string
216 isIPv6 bool
217 expectIP string
218 expectPort int
219 expectErr bool
220 }{
221 {
222 name: "IPv4 localhost:80",
223 addrPort: "0100007F:0050", // 127.0.0.1:80 in little-endian hex
224 isIPv6: false,
225 expectIP: "127.0.0.1",
226 expectPort: 80,
227 expectErr: false,
228 },
229 {
230 name: "IPv4 any:22",
231 addrPort: "00000000:0016", // 0.0.0.0:22
232 isIPv6: false,
233 expectIP: "*",
234 expectPort: 22,
235 expectErr: false,
236 },
237 {
238 name: "IPv4 high port",
239 addrPort: "0100007F:1F90", // 127.0.0.1:8080
240 isIPv6: false,
241 expectIP: "127.0.0.1",
242 expectPort: 8080,
243 expectErr: false,
244 },
245 {
246 name: "IPv6 any port 22",
247 addrPort: "00000000000000000000000000000000:0016", // [::]:22
248 isIPv6: true,
249 expectIP: "*",
250 expectPort: 22,
251 expectErr: false,
252 },
253 {
254 name: "Invalid format - no colon",
255 addrPort: "0100007F0050",
256 isIPv6: false,
257 expectErr: true,
258 },
259 {
260 name: "Invalid port hex",
261 addrPort: "0100007F:ZZZZ",
262 isIPv6: false,
263 expectErr: true,
264 },
265 {
266 name: "Invalid IPv4 hex length",
267 addrPort: "0100:0050",
268 isIPv6: false,
269 expectErr: true,
270 },
271 }
272
273 for _, test := range tests {
274 t.Run(test.name, func(t *testing.T) {
275 ip, port, err := pm.parseAddress(test.addrPort, test.isIPv6)
276 if test.expectErr {
277 if err == nil {
278 t.Errorf("Expected error but got none")
279 }
280 return
281 }
282
283 if err != nil {
284 t.Errorf("Unexpected error: %v", err)
285 return
286 }
287
288 if ip != test.expectIP {
289 t.Errorf("Expected IP %s, got %s", test.expectIP, ip)
290 }
291
292 if port != test.expectPort {
293 t.Errorf("Expected port %d, got %d", test.expectPort, port)
294 }
295 })
296 }
297}
298
299// TestParseProcData tests parsing of mock /proc/net data
300func TestParseProcData(t *testing.T) {
301 pm := NewPortMonitor()
302
303 // Test TCP data with listening sockets
304 tcpData := ` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
305 0: 0100007F:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1
306 1: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 2
307 2: 0100007F:1F90 0200007F:C350 01 00000000:00000000 00:00000000 00000000 0 0 3`
308
309 var result strings.Builder
310 result.WriteString("Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\n")
311
312 // Create temp file with test data
313 tmpFile := "/tmp/test_tcp"
314 err := os.WriteFile(tmpFile, []byte(tcpData), 0o644)
315 if err != nil {
316 t.Fatalf("Failed to create temp file: %v", err)
317 }
318 defer os.Remove(tmpFile)
319
320 err = pm.parseProc(tmpFile, "tcp", &result)
321 if err != nil {
322 t.Fatalf("parseProc failed: %v", err)
323 }
324
325 output := result.String()
326 t.Logf("Generated output:\n%s", output)
327
328 // Should contain listening ports (state 0A = LISTEN)
329 if !strings.Contains(output, "127.0.0.1:80") {
330 t.Error("Expected to find 127.0.0.1:80 in output")
331 }
332 if !strings.Contains(output, "*:22") {
333 t.Error("Expected to find *:22 in output")
334 }
335 // Should not contain established connection (state 01)
336 if strings.Contains(output, "127.0.0.1:8080") {
337 t.Error("Should not find established connection 127.0.0.1:8080 in output")
338 }
339}
340
341// TestGetListeningPortsFromProcFallback tests the complete /proc fallback
342func TestGetListeningPortsFromProcFallback(t *testing.T) {
343 pm := NewPortMonitor()
344
345 // This test verifies the method runs without error
346 // The actual files may or may not exist, but it should handle both cases gracefully
347 output, err := pm.getListeningPortsFromProc()
348 if err != nil {
349 t.Logf("getListeningPortsFromProc failed (may be expected if /proc/net files don't exist): %v", err)
350 // Don't fail the test - this might be expected in some environments
351 return
352 }
353
354 t.Logf("Generated /proc fallback output:\n%s", output)
355
356 // Should at least have a header
357 if !strings.Contains(output, "Netid State") {
358 t.Error("Expected header in /proc fallback output")
359 }
360}
361
362// TestUpdatePortStateWithFallback tests updatePortState with both ss and /proc fallback
363func TestUpdatePortStateWithFallback(t *testing.T) {
364 pm := NewPortMonitor()
365 ctx := context.Background()
366
367 // Call updatePortState - should try ss first, then fall back to /proc if ss fails
368 pm.updatePortState(ctx)
369
370 // The method should complete without panicking
371 // We can't easily test the exact behavior without mocking, but we can ensure it runs
372 // Check if any port state was captured
373 pm.mu.Lock()
374 lastPorts := pm.lastPorts
375 pm.mu.Unlock()
376
377 t.Logf("Captured port state (length %d):", len(lastPorts))
378 if len(lastPorts) > 0 {
379 t.Logf("First 200 chars: %s", lastPorts[:min(200, len(lastPorts))])
380 }
381}
382
383func min(a, b int) int {
384 if a < b {
385 return a
386 }
387 return b
388}