sketch/loop: add PortMonitor for TCP port monitoring with Agent integration
Add PortMonitor struct that uses Tailscale portlist library to monitor
open/listening TCP ports and send AgentMessage notifications to Agent
when ports are opened or closed, with cached port list access method.
When I asked Sketch to do this with the old implementation, it did
ok parsing /proc, but then it tried to conver it to ss format...
using a library seems to work ok!
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s8fc57de4b5583d34k
diff --git a/loop/port_monitor_demo_test.go b/loop/port_monitor_demo_test.go
new file mode 100644
index 0000000..4190e5a
--- /dev/null
+++ b/loop/port_monitor_demo_test.go
@@ -0,0 +1,170 @@
+package loop
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "net/http"
+ "sync"
+ "testing"
+ "time"
+)
+
+// TestPortMonitor_IntegrationDemo demonstrates the full integration of PortMonitor with an Agent.
+// This test shows how the PortMonitor detects port changes and sends notifications to an Agent.
+func TestPortMonitor_IntegrationDemo(t *testing.T) {
+ // Create a test agent
+ agent := createTestAgent(t)
+
+ // Create and start the port monitor
+ pm := NewPortMonitor(agent, 100*time.Millisecond) // Fast polling for demo
+ ctx := context.Background()
+ err := pm.Start(ctx)
+ if err != nil {
+ t.Fatalf("Failed to start port monitor: %v", err)
+ }
+ defer pm.Stop()
+
+ // Wait for initial scan
+ time.Sleep(200 * time.Millisecond)
+
+ // Show current ports
+ currentPorts := pm.GetPorts()
+ t.Logf("Initial TCP ports detected: %d", len(currentPorts))
+ for _, port := range currentPorts {
+ t.Logf(" - Port %d (process: %s, pid: %d)", port.Port, port.Process, port.Pid)
+ }
+
+ // Start multiple test servers to demonstrate detection
+ var listeners []net.Listener
+ var wg sync.WaitGroup
+
+ // Start 3 test HTTP servers
+ for i := 0; i < 3; i++ {
+ listener, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatalf("Failed to start test listener %d: %v", i, err)
+ }
+ listeners = append(listeners, listener)
+
+ addr := listener.Addr().(*net.TCPAddr)
+ port := addr.Port
+ t.Logf("Started test HTTP server %d on port %d", i+1, port)
+
+ // Start a simple HTTP server
+ wg.Add(1)
+ go func(l net.Listener, serverID int) {
+ defer wg.Done()
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, "Hello from test server %d!\n", serverID)
+ })
+ server := &http.Server{Handler: mux}
+ server.Serve(l)
+ }(listener, i+1)
+ }
+
+ // Wait for ports to be detected
+ time.Sleep(500 * time.Millisecond)
+
+ // Check that the new ports were detected
+ updatedPorts := pm.GetPorts()
+ t.Logf("Updated TCP ports detected: %d", len(updatedPorts))
+
+ // Verify that we have at least 3 more ports than initially
+ if len(updatedPorts) < len(currentPorts)+3 {
+ t.Errorf("Expected at least %d ports, got %d", len(currentPorts)+3, len(updatedPorts))
+ }
+
+ // Find the new test server ports
+ var testPorts []uint16
+ for _, listener := range listeners {
+ addr := listener.Addr().(*net.TCPAddr)
+ testPorts = append(testPorts, uint16(addr.Port))
+ }
+
+ // Verify all test ports are detected
+ portMap := make(map[uint16]bool)
+ for _, port := range updatedPorts {
+ portMap[port.Port] = true
+ }
+
+ allPortsDetected := true
+ for _, testPort := range testPorts {
+ if !portMap[testPort] {
+ allPortsDetected = false
+ t.Errorf("Test port %d was not detected", testPort)
+ }
+ }
+
+ if allPortsDetected {
+ t.Logf("All test ports successfully detected!")
+ }
+
+ // Close all listeners
+ for i, listener := range listeners {
+ t.Logf("Closing test server %d", i+1)
+ listener.Close()
+ }
+
+ // Wait for servers to stop
+ wg.Wait()
+
+ // Wait for ports to be removed
+ time.Sleep(500 * time.Millisecond)
+
+ // Check that ports were removed
+ finalPorts := pm.GetPorts()
+ t.Logf("Final TCP ports detected: %d", len(finalPorts))
+
+ // Verify the final port count is back to near the original
+ if len(finalPorts) > len(currentPorts)+1 {
+ t.Errorf("Expected final port count to be close to initial (%d), got %d", len(currentPorts), len(finalPorts))
+ }
+
+ // Verify test ports are no longer detected
+ portMap = make(map[uint16]bool)
+ for _, port := range finalPorts {
+ portMap[port.Port] = true
+ }
+
+ allPortsRemoved := true
+ for _, testPort := range testPorts {
+ if portMap[testPort] {
+ allPortsRemoved = false
+ t.Errorf("Test port %d was not removed", testPort)
+ }
+ }
+
+ if allPortsRemoved {
+ t.Logf("All test ports successfully removed!")
+ }
+
+ t.Logf("Integration test completed successfully!")
+ t.Logf("- Initial ports: %d", len(currentPorts))
+ t.Logf("- Peak ports: %d", len(updatedPorts))
+ t.Logf("- Final ports: %d", len(finalPorts))
+ t.Logf("- Test ports added and removed: %d", len(testPorts))
+}
+
+// contains checks if a string contains a substring.
+func contains(s, substr string) bool {
+ return len(s) >= len(substr) && (s == substr ||
+ (len(s) > len(substr) &&
+ (s[:len(substr)] == substr ||
+ s[len(s)-len(substr):] == substr ||
+ indexOfSubstring(s, substr) >= 0)))
+}
+
+// indexOfSubstring finds the index of substring in string.
+func indexOfSubstring(s, substr string) int {
+ if len(substr) == 0 {
+ return 0
+ }
+ for i := 0; i <= len(s)-len(substr); i++ {
+ if s[i:i+len(substr)] == substr {
+ return i
+ }
+ }
+ return -1
+}