//go:build linux package proc import ( "bufio" "encoding/hex" "fmt" "net" "os" "strconv" "strings" "sync" "time" "github.com/pranshuparmar/witr/pkg/model" ) // Cached socket table to avoid re-parsing /proc/net/* on every ReadProcess call // during ancestry walks (typically 5-10 calls within milliseconds). var ( socketCache map[string]model.Socket socketCacheTime time.Time socketCacheMu sync.Mutex socketCacheTTL = 2 * time.Second ) func readSocketsCached() (map[string]model.Socket, error) { socketCacheMu.Lock() defer socketCacheMu.Unlock() if socketCache != nil && time.Since(socketCacheTime) < socketCacheTTL { return socketCache, nil } sockets, err := readSockets() if err != nil { return nil, err } socketCache = sockets socketCacheTime = time.Now() return sockets, nil } var stateMap = map[string]string{ "01": "ESTABLISHED", "02": "SYN_SENT", "03": "SYN_RECV", "04": "FIN_WAIT1", "05": "FIN_WAIT2", "06": "TIME_WAIT", "07": "CLOSE", "08": "CLOSE_WAIT", "09": "LAST_ACK", "0A": "LISTEN", "0B": "CLOSING", } func readSockets() (map[string]model.Socket, error) { sockets := make(map[string]model.Socket) parse := func(path, proto string, ipv6 bool) { f, err := os.Open(path) if err != nil { return } defer f.Close() scanner := bufio.NewScanner(f) scanner.Scan() // skip header for scanner.Scan() { fields := strings.Fields(scanner.Text()) if len(fields) < 10 { continue } local := fields[1] stateHex := fields[3] inode := fields[9] state, ok := stateMap[stateHex] if !ok { state = "UNKNOWN" } addr, port := parseAddr(local, ipv6) sockets[inode] = model.Socket{ Inode: inode, Port: port, Address: addr, State: state, Protocol: proto, } } } parse("/proc/net/tcp", "TCP", false) parse("/proc/net/tcp6", "TCP6", true) parse("/proc/net/udp", "UDP", false) parse("/proc/net/udp6", "UDP6", true) return sockets, nil } func parseAddr(raw string, ipv6 bool) (string, int) { parts := strings.Split(raw, ":") if len(parts) < 2 { return "", 0 } portHex := parts[1] port, _ := strconv.ParseInt(portHex, 16, 32) ipHex := parts[0] b, err := hex.DecodeString(ipHex) if err != nil { return "", int(port) } if ipv6 { if len(b) != 16 { return "::", int(port) } // /proc/net/tcp6 stores IPv6 as 4 little-endian 32-bit groups // Reverse bytes within each 4-byte group ip := make(net.IP, 16) for i := 0; i < 4; i++ { ip[i*4+0] = b[i*4+3] ip[i*4+1] = b[i*4+2] ip[i*4+2] = b[i*4+1] ip[i*4+3] = b[i*4+0] } return ip.String(), int(port) } if len(b) < 4 { return "", int(port) } ip := strconv.Itoa(int(b[3])) + "." + strconv.Itoa(int(b[2])) + "." + strconv.Itoa(int(b[1])) + "." + strconv.Itoa(int(b[0])) return ip, int(port) } func ListOpenPorts() ([]model.OpenPort, error) { sockets, err := readSockets() if err != nil { return nil, err } var openPorts []model.OpenPort // Scan proc procs, err := os.ReadDir("/proc") if err != nil { return nil, err } for _, p := range procs { if !p.IsDir() { continue } pid, err := strconv.Atoi(p.Name()) if err != nil { continue } // Scan fds fdPath := fmt.Sprintf("/proc/%d/fd", pid) fds, err := os.ReadDir(fdPath) if err != nil { continue } for _, fd := range fds { link, err := os.Readlink(fmt.Sprintf("%s/%s", fdPath, fd.Name())) if err != nil { continue } if strings.HasPrefix(link, "socket:[") { inode := strings.TrimSuffix(strings.TrimPrefix(link, "socket:["), "]") if s, ok := sockets[inode]; ok { openPorts = append(openPorts, model.OpenPort{ PID: pid, Port: s.Port, Address: s.Address, Protocol: s.Protocol, State: s.State, }) } } } } return openPorts, nil }