1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
|
//go:build linux
package proc
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"github.com/pranshuparmar/witr/pkg/model"
)
// GetSocketStateForPort returns the socket state for a port
// Linux implementation using /proc/net/tcp and /proc/net/tcp6
func GetSocketStateForPort(port int) *model.SocketInfo {
// Check both IPv4 and IPv6
files := []string{"/proc/net/tcp", "/proc/net/tcp6"}
var states []model.SocketInfo
for _, file := range files {
isIPv6 := strings.HasSuffix(file, "tcp6")
f, err := os.Open(file)
if err != nil {
continue
}
defer f.Close()
scanner := bufio.NewScanner(f)
scanner.Scan()
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 10 {
continue
}
// Field 1: local_address (IP:Port in hex)
// Format example: 0100007F:1388
localAddrHex := fields[1]
// parseAddr is defined in internal/proc/net_linux.go (same package)
localIP, localPort := parseAddr(localAddrHex, isIPv6)
if localPort != port {
continue
}
// Field 2: rem_address (IP:Port in hex)
remoteAddrHex := fields[2]
remoteIP, _ := parseAddr(remoteAddrHex, isIPv6)
// Field 3: st (state in hex)
stateHex := fields[3]
stateVal, _ := strconv.ParseInt(stateHex, 16, 0)
stateStr := mapTCPState(int(stateVal))
info := model.SocketInfo{
Port: port,
State: stateStr,
LocalAddr: localIP,
RemoteAddr: remoteIP,
}
addStateExplanation(&info)
states = append(states, info)
}
}
if len(states) == 0 {
return nil
}
// Prioritize problematic states just like the Darwin implementation
for _, s := range states {
if isProblematicState(s.State) {
return &s
}
}
// Then prioritize LISTEN
for _, s := range states {
if s.State == "LISTEN" {
return &s
}
}
// Default to first found
return &states[0]
}
// mapTCPState maps Linux kernel TCP states (from include/net/tcp_states.h) to strings
func mapTCPState(state int) string {
switch state {
case 1:
return "ESTABLISHED"
case 2:
return "SYN_SENT"
case 3:
return "SYN_RECV"
case 4:
return "FIN_WAIT_1"
case 5:
return "FIN_WAIT_2"
case 6:
return "TIME_WAIT"
case 7:
return "CLOSE"
case 8:
return "CLOSE_WAIT"
case 9:
return "LAST_ACK"
case 10:
return "LISTEN"
case 11:
return "CLOSING"
default:
return fmt.Sprintf("UNKNOWN (%02X)", state)
}
}
func isProblematicState(state string) bool {
switch state {
case "TIME_WAIT", "CLOSE_WAIT", "FIN_WAIT_1", "FIN_WAIT_2":
return true
}
return false
}
func addStateExplanation(info *model.SocketInfo) {
switch info.State {
case "LISTEN":
info.Explanation = "Actively listening for connections"
case "TIME_WAIT":
info.Explanation = "Connection closed, waiting for delayed packets"
info.Workaround = "Wait for timeout (usually 60s) or use SO_REUSEADDR"
case "CLOSE_WAIT":
info.Explanation = "Remote side closed connection, local side has not closed yet"
info.Workaround = "The application should call close() on the socket"
case "FIN_WAIT_1":
info.Explanation = "Local side initiated close, waiting for acknowledgment"
case "FIN_WAIT_2":
info.Explanation = "Local close acknowledged, waiting for remote close"
case "ESTABLISHED":
info.Explanation = "Active connection"
case "SYN_SENT":
info.Explanation = "Connection request sent, waiting for response"
case "SYN_RECEIVED":
info.Explanation = "Connection request received, sending acknowledgment"
case "CLOSING":
info.Explanation = "Both sides initiated close simultaneously"
case "LAST_ACK":
info.Explanation = "Waiting for final acknowledgment of close"
default:
info.Explanation = "Socket in " + info.State + " state"
}
}
|