blob: 66bf7b18f1f8fd1363a456b8d08c98002e46f8fb [file] [log] [blame]
Philip Zeyliger5f26a342025-07-04 01:30:29 +00001package loop
2
3import (
4 "context"
Philip Zeyliger9b39aa62025-07-14 11:56:02 -07005 "os"
6 "os/exec"
Josh Bleecher Snyderd1b7dd62025-07-21 19:36:23 -07007 "runtime"
Philip Zeyliger5f26a342025-07-04 01:30:29 +00008 "testing"
9 "time"
10
11 "tailscale.com/portlist"
12)
13
14// TestPortMonitor_NewPortMonitor tests the creation of a new PortMonitor.
15func TestPortMonitor_NewPortMonitor(t *testing.T) {
16 agent := createTestAgent(t)
17 interval := 2 * time.Second
18
19 pm := NewPortMonitor(agent, interval)
20
21 if pm == nil {
22 t.Fatal("NewPortMonitor returned nil")
23 }
24
25 if pm.agent != agent {
26 t.Errorf("expected agent %v, got %v", agent, pm.agent)
27 }
28
29 if pm.interval != interval {
30 t.Errorf("expected interval %v, got %v", interval, pm.interval)
31 }
32
33 if pm.running {
34 t.Error("expected monitor to not be running initially")
35 }
36
37 if pm.poller == nil {
38 t.Error("expected poller to be initialized")
39 }
40
41 if !pm.poller.IncludeLocalhost {
42 t.Error("expected IncludeLocalhost to be true")
43 }
44}
45
46// TestPortMonitor_DefaultInterval tests that a default interval is set when invalid.
47func TestPortMonitor_DefaultInterval(t *testing.T) {
48 agent := createTestAgent(t)
49
50 pm := NewPortMonitor(agent, 0)
51 if pm.interval != 5*time.Second {
52 t.Errorf("expected default interval 5s, got %v", pm.interval)
53 }
54
55 pm2 := NewPortMonitor(agent, -1*time.Second)
56 if pm2.interval != 5*time.Second {
57 t.Errorf("expected default interval 5s, got %v", pm2.interval)
58 }
59}
60
61// TestPortMonitor_StartStop tests starting and stopping the monitor.
62func TestPortMonitor_StartStop(t *testing.T) {
63 agent := createTestAgent(t)
64 pm := NewPortMonitor(agent, 100*time.Millisecond)
65
66 // Test starting
67 ctx := context.Background()
68 err := pm.Start(ctx)
69 if err != nil {
70 t.Fatalf("failed to start port monitor: %v", err)
71 }
72
73 if !pm.running {
74 t.Error("expected monitor to be running after start")
75 }
76
77 // Test double start fails
78 err = pm.Start(ctx)
79 if err == nil {
80 t.Error("expected error when starting already running monitor")
81 }
82
83 // Test stopping
84 pm.Stop()
85 if pm.running {
86 t.Error("expected monitor to not be running after stop")
87 }
88
89 // Test double stop is safe
90 pm.Stop() // should not panic
91}
92
93// TestPortMonitor_GetPorts tests getting the cached port list.
94func TestPortMonitor_GetPorts(t *testing.T) {
95 agent := createTestAgent(t)
96 pm := NewPortMonitor(agent, 100*time.Millisecond)
97
98 // Initially should be empty
99 ports := pm.GetPorts()
100 if len(ports) != 0 {
101 t.Errorf("expected empty ports initially, got %d", len(ports))
102 }
103
104 // Start monitoring to populate ports
105 ctx := context.Background()
106 err := pm.Start(ctx)
107 if err != nil {
108 t.Fatalf("failed to start port monitor: %v", err)
109 }
110 defer pm.Stop()
111
112 // Allow some time for initial scan
113 time.Sleep(200 * time.Millisecond)
114
115 // Should have some ports now (at least system ports)
116 ports = pm.GetPorts()
117 // We can't guarantee specific ports, but there should be at least some TCP ports
118 // on most systems (like SSH, etc.)
119 t.Logf("Found %d TCP ports", len(ports))
120
121 // Verify all returned ports are TCP
122 for _, port := range ports {
123 if port.Proto != "tcp" {
124 t.Errorf("expected TCP port, got %s", port.Proto)
125 }
126 }
127}
128
Philip Zeyliger5f26a342025-07-04 01:30:29 +0000129// TestPortMonitor_FilterTCPPorts tests the TCP port filtering.
130func TestPortMonitor_FilterTCPPorts(t *testing.T) {
131 ports := []portlist.Port{
132 {Proto: "tcp", Port: 80},
133 {Proto: "udp", Port: 53},
134 {Proto: "tcp", Port: 443},
135 {Proto: "udp", Port: 123},
136 }
137
138 tcpPorts := filterTCPPorts(ports)
139
140 if len(tcpPorts) != 2 {
141 t.Errorf("expected 2 TCP ports, got %d", len(tcpPorts))
142 }
143
144 for _, port := range tcpPorts {
145 if port.Proto != "tcp" {
146 t.Errorf("expected TCP port, got %s", port.Proto)
147 }
148 }
149}
150
151// TestPortMonitor_SortPorts tests the port sorting.
152func TestPortMonitor_SortPorts(t *testing.T) {
153 ports := []portlist.Port{
154 {Proto: "tcp", Port: 443},
155 {Proto: "tcp", Port: 80},
156 {Proto: "tcp", Port: 8080},
157 {Proto: "tcp", Port: 22},
158 }
159
160 sortPorts(ports)
161
162 expected := []uint16{22, 80, 443, 8080}
163 for i, port := range ports {
164 if port.Port != expected[i] {
165 t.Errorf("expected port %d at index %d, got %d", expected[i], i, port.Port)
166 }
167 }
168}
169
170// TestPortMonitor_FindAddedPorts tests finding added ports.
171func TestPortMonitor_FindAddedPorts(t *testing.T) {
172 previous := []portlist.Port{
173 {Proto: "tcp", Port: 80},
174 {Proto: "tcp", Port: 443},
175 }
176
177 current := []portlist.Port{
178 {Proto: "tcp", Port: 80},
179 {Proto: "tcp", Port: 443},
180 {Proto: "tcp", Port: 8080},
181 {Proto: "tcp", Port: 22},
182 }
183
184 added := findAddedPorts(previous, current)
185
186 if len(added) != 2 {
187 t.Errorf("expected 2 added ports, got %d", len(added))
188 }
189
190 addedPorts := make(map[uint16]bool)
191 for _, port := range added {
192 addedPorts[port.Port] = true
193 }
194
195 if !addedPorts[8080] || !addedPorts[22] {
196 t.Errorf("expected ports 8080 and 22 to be added, got %v", added)
197 }
198}
199
200// TestPortMonitor_FindRemovedPorts tests finding removed ports.
201func TestPortMonitor_FindRemovedPorts(t *testing.T) {
202 previous := []portlist.Port{
203 {Proto: "tcp", Port: 80},
204 {Proto: "tcp", Port: 443},
205 {Proto: "tcp", Port: 8080},
206 {Proto: "tcp", Port: 22},
207 }
208
209 current := []portlist.Port{
210 {Proto: "tcp", Port: 80},
211 {Proto: "tcp", Port: 443},
212 }
213
214 removed := findRemovedPorts(previous, current)
215
216 if len(removed) != 2 {
217 t.Errorf("expected 2 removed ports, got %d", len(removed))
218 }
219
220 removedPorts := make(map[uint16]bool)
221 for _, port := range removed {
222 removedPorts[port.Port] = true
223 }
224
225 if !removedPorts[8080] || !removedPorts[22] {
226 t.Errorf("expected ports 8080 and 22 to be removed, got %v", removed)
227 }
228}
229
Philip Zeyliger9b39aa62025-07-14 11:56:02 -0700230// TestPortMonitor_ShouldIgnoreProcess tests the shouldIgnoreProcess function.
231func TestPortMonitor_ShouldIgnoreProcess(t *testing.T) {
Josh Bleecher Snyderd1b7dd62025-07-21 19:36:23 -0700232 if runtime.GOOS != "linux" {
233 // The implementation of shouldIgnoreProcess is specific to Linux (it uses /proc).
234 // On macOS, ignoring SKETCH_IGNORE_PORTS simply won't work, because macOS doesn't expose other processes' environment variables.
235 // This is OK (enough) because our primary operating environment is a Linux container.
236 t.Skip("skipping test on non-Linux OS")
237 }
238
Philip Zeyliger9b39aa62025-07-14 11:56:02 -0700239 agent := createTestAgent(t)
240 pm := NewPortMonitor(agent, 100*time.Millisecond)
241
242 // Test with current process (should not be ignored)
243 currentPid := os.Getpid()
244 if pm.shouldIgnoreProcess(currentPid) {
245 t.Errorf("current process should not be ignored")
246 }
247
248 // Test with invalid PID
249 if pm.shouldIgnoreProcess(0) {
250 t.Errorf("invalid PID should not be ignored")
251 }
252 if pm.shouldIgnoreProcess(-1) {
253 t.Errorf("negative PID should not be ignored")
254 }
255
256 // Test with a process that has SKETCH_IGNORE_PORTS=1
257 cmd := exec.Command("sleep", "5")
258 cmd.Env = append(os.Environ(), "SKETCH_IGNORE_PORTS=1")
259 err := cmd.Start()
260 if err != nil {
261 t.Fatalf("failed to start test process: %v", err)
262 }
263 defer cmd.Process.Kill()
264
265 // Allow a moment for the process to start
266 time.Sleep(100 * time.Millisecond)
267
268 if !pm.shouldIgnoreProcess(cmd.Process.Pid) {
269 t.Errorf("process with SKETCH_IGNORE_PORTS=1 should be ignored")
270 }
271}
272
Philip Zeyliger5f26a342025-07-04 01:30:29 +0000273// createTestAgent creates a minimal test agent for testing.
274func createTestAgent(t *testing.T) *Agent {
275 // Create a minimal agent for testing
276 // We need to initialize the required fields for the PortMonitor to work
277 agent := &Agent{
278 subscribers: make([]chan *AgentMessage, 0),
279 }
280 return agent
281}