Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions network/port_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//go:build darwin

package network

import (
"bufio"
"bytes"
"fmt"
"os/exec"
"regexp"
"sort"
"strconv"
)

// DetectListeningTCPPorts scans the system for TCP ports that are currently bound and listening.
// It returns a slice of port numbers, excluding any in the exclude list.
// Works on Linux by reading /proc/net/tcp and /proc/net/tcp6.
func DetectListeningTCPPorts(exclude ...int) ([]int, error) {
excludeSet := make(map[int]struct{})
for _, p := range exclude {
excludeSet[p] = struct{}{}
}

cmd := exec.Command("lsof", "-nP", "-iTCP", "-sTCP:LISTEN")
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to run lsof: %w", err)
}

scanner := bufio.NewScanner(bytes.NewReader(out))
portPattern := regexp.MustCompile(`:(\d+)\s+\(LISTEN\)`)

ports := make(map[int]struct{})

for scanner.Scan() {
line := scanner.Text()
matches := portPattern.FindStringSubmatch(line)
if len(matches) != 2 {
continue
}
port, err := strconv.Atoi(matches[1])
if err != nil {
continue
}
if _, excluded := excludeSet[port]; !excluded {
ports[port] = struct{}{}
}
}

if err := scanner.Err(); err != nil {
return nil, err
}

// Convert to sorted slice
var result []int
for p := range ports {
result = append(result, p)
}
sort.Ints(result)
return result, nil
}
69 changes: 69 additions & 0 deletions network/port_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//go:build linux

package network

import (
"bufio"
"os"
"sort"
"strconv"
"strings"
)

// DetectListeningTCPPorts scans the system for TCP ports that are currently bound and listening.
// It returns a slice of port numbers, excluding any in the exclude list.
// Works on Linux by reading /proc/net/tcp and /proc/net/tcp6.
func DetectListeningTCPPorts(exclude ...int) ([]int, error) {
excludeSet := make(map[int]struct{})
for _, p := range exclude {
excludeSet[p] = struct{}{}
}

files := []string{"/proc/net/tcp", "/proc/net/tcp6"}
var ports []int

for _, path := range files {
f, err := os.Open(path)
if err != nil {
continue // skip if file doesn't exist or can't be read
}
defer f.Close()
Comment on lines +25 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix resource leak: defer in loop.

The defer f.Close() executes at function exit, not at the end of each loop iteration. If both /proc/net/tcp and /proc/net/tcp6 exist, the first file remains open until the function returns.

Apply this diff to close immediately after reading:

 	for _, path := range files {
 		f, err := os.Open(path)
 		if err != nil {
 			continue // skip if file doesn't exist or can't be read
 		}
-		defer f.Close()
 
 		scanner := bufio.NewScanner(f)
 		// Skip the header line
 		if scanner.Scan() {
 			for scanner.Scan() {
 				fields := strings.Fields(scanner.Text())
 				if len(fields) < 4 {
 					continue
 				}
 
 				localAddr := fields[1]
 				state := fields[3]
 				if state != "0A" { // 0A means LISTEN
 					continue
 				}
 
 				// localAddr example: 0100007F:1F90
 				parts := strings.Split(localAddr, ":")
 				if len(parts) != 2 {
 					continue
 				}
 
 				portHex := parts[1]
 				portDec, err := strconv.ParseInt(portHex, 16, 32)
 				if err != nil {
 					continue
 				}
 				port := int(portDec)
 
 				if _, excluded := excludeSet[port]; !excluded {
 					ports = append(ports, port)
 				}
 			}
 		}
+		f.Close()
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, path := range files {
f, err := os.Open(path)
if err != nil {
continue // skip if file doesn't exist or can't be read
}
defer f.Close()
for _, path := range files {
f, err := os.Open(path)
if err != nil {
continue // skip if file doesn't exist or can't be read
}
scanner := bufio.NewScanner(f)
// Skip the header line
if scanner.Scan() {
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 4 {
continue
}
localAddr := fields[1]
state := fields[3]
if state != "0A" { // 0A means LISTEN
continue
}
// localAddr example: 0100007F:1F90
parts := strings.Split(localAddr, ":")
if len(parts) != 2 {
continue
}
portHex := parts[1]
portDec, err := strconv.ParseInt(portHex, 16, 32)
if err != nil {
continue
}
port := int(portDec)
if _, excluded := excludeSet[port]; !excluded {
ports = append(ports, port)
}
}
}
f.Close()
}
🤖 Prompt for AI Agents
In network/port_linux.go around lines 25 to 30, the code uses "defer f.Close()"
inside the loop which defers closing until function exit and leaks file
descriptors when iterating multiple files; replace the defer with an explicit
close after the file is read (or wrap the read logic in a short-lived inner
function and defer f.Close() there) so each opened file is closed immediately at
the end of its iteration.


scanner := bufio.NewScanner(f)
// Skip the header line
if scanner.Scan() {
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 4 {
continue
}

localAddr := fields[1]
state := fields[3]
if state != "0A" { // 0A means LISTEN
continue
}

// localAddr example: 0100007F:1F90
parts := strings.Split(localAddr, ":")
if len(parts) != 2 {
continue
}

portHex := parts[1]
portDec, err := strconv.ParseInt(portHex, 16, 32)
if err != nil {
continue
}
port := int(portDec)

if _, excluded := excludeSet[port]; !excluded {
ports = append(ports, port)
}
}
}
}

sort.Ints(ports)
return ports, nil
}
129 changes: 129 additions & 0 deletions network/port_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package network

import (
"net"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDetectListeningTCPPorts(t *testing.T) {
ports, err := DetectListeningTCPPorts()
require.NoError(t, err)
assert.NotNil(t, ports)
assert.GreaterOrEqual(t, len(ports), 0)

if len(ports) > 1 {
for i := 1; i < len(ports); i++ {
assert.Greater(t, ports[i], ports[i-1], "ports should be sorted in ascending order")
}
}
}

func TestDetectListeningTCPPortsWithExclusion(t *testing.T) {
allPorts, err := DetectListeningTCPPorts()
require.NoError(t, err)

if len(allPorts) == 0 {
t.Skip("no listening ports detected, skipping exclusion test")
}

portToExclude := allPorts[0]
filteredPorts, err := DetectListeningTCPPorts(portToExclude)
require.NoError(t, err)

for _, port := range filteredPorts {
assert.NotEqual(t, portToExclude, port, "excluded port should not be in result")
}
assert.Len(t, filteredPorts, len(allPorts)-1)
}

func TestDetectListeningTCPPortsWithMultipleExclusions(t *testing.T) {
allPorts, err := DetectListeningTCPPorts()
require.NoError(t, err)

if len(allPorts) < 3 {
t.Skip("not enough listening ports detected, skipping multiple exclusion test")
}

excludePorts := []int{allPorts[0], allPorts[1], allPorts[2]}
filteredPorts, err := DetectListeningTCPPorts(excludePorts...)
require.NoError(t, err)

for _, port := range filteredPorts {
for _, excluded := range excludePorts {
assert.NotEqual(t, excluded, port, "excluded port should not be in result")
}
}
assert.Len(t, filteredPorts, len(allPorts)-len(excludePorts))
}

func TestDetectListeningTCPPortsWithNonExistentExclusion(t *testing.T) {
allPorts, err := DetectListeningTCPPorts()
require.NoError(t, err)

nonExistentPort := 65534
filteredPorts, err := DetectListeningTCPPorts(nonExistentPort)
require.NoError(t, err)

assert.Equal(t, len(allPorts), len(filteredPorts), "excluding non-existent port should not change result")
}

func TestDetectListeningTCPPortsWithActualListener(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer listener.Close()

addr := listener.Addr().(*net.TCPAddr)
boundPort := addr.Port

ports, err := DetectListeningTCPPorts()
require.NoError(t, err)

found := false
for _, port := range ports {
if port == boundPort {
found = true
break
}
}
assert.True(t, found, "should detect the port we just bound to")
}

func TestDetectListeningTCPPortsWithActualListenerAndExclude(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer listener.Close()

addr := listener.Addr().(*net.TCPAddr)
boundPort := addr.Port

ports, err := DetectListeningTCPPorts(boundPort)
require.NoError(t, err)

for _, port := range ports {
assert.NotEqual(t, boundPort, port, "excluded port should not appear even though it's listening")
}
}

func TestDetectListeningTCPPortsNoDuplicates(t *testing.T) {
ports, err := DetectListeningTCPPorts()
require.NoError(t, err)

seen := make(map[int]bool)
for _, port := range ports {
assert.False(t, seen[port], "port %d appears multiple times in result", port)
seen[port] = true
}
}

func TestDetectListeningTCPPortsValidRange(t *testing.T) {
ports, err := DetectListeningTCPPorts()
require.NoError(t, err)

for _, port := range ports {
assert.GreaterOrEqual(t, port, 1, "port should be >= 1")
assert.LessOrEqual(t, port, 65535, "port should be <= 65535")
}
}
Loading