383 lines
7.8 KiB
Go
383 lines
7.8 KiB
Go
//go:build linux
|
|
|
|
package collector
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// DefaultCollector implements the Collector interface using /proc filesystem
|
|
type DefaultCollector struct{}
|
|
|
|
// GetConnections fetches all network connections by parsing /proc files
|
|
func (dc *DefaultCollector) GetConnections() ([]Connection, error) {
|
|
inodeMap, err := buildInodeToProcessMap()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to build inode map: %w", err)
|
|
}
|
|
|
|
var connections []Connection
|
|
|
|
tcpConns, err := parseProcNet("/proc/net/tcp", "tcp", 4, inodeMap)
|
|
if err == nil {
|
|
connections = append(connections, tcpConns...)
|
|
}
|
|
|
|
tcpConns6, err := parseProcNet("/proc/net/tcp6", "tcp6", 6, inodeMap)
|
|
if err == nil {
|
|
connections = append(connections, tcpConns6...)
|
|
}
|
|
|
|
udpConns, err := parseProcNet("/proc/net/udp", "udp", 4, inodeMap)
|
|
if err == nil {
|
|
connections = append(connections, udpConns...)
|
|
}
|
|
|
|
udpConns6, err := parseProcNet("/proc/net/udp6", "udp6", 6, inodeMap)
|
|
if err == nil {
|
|
connections = append(connections, udpConns6...)
|
|
}
|
|
|
|
return connections, nil
|
|
}
|
|
|
|
// GetAllConnections returns both network and Unix domain socket connections
|
|
func GetAllConnections() ([]Connection, error) {
|
|
networkConns, err := GetConnections()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
unixConns, err := GetUnixSockets()
|
|
if err == nil {
|
|
networkConns = append(networkConns, unixConns...)
|
|
}
|
|
|
|
return networkConns, nil
|
|
}
|
|
|
|
type processInfo struct {
|
|
pid int
|
|
command string
|
|
uid int
|
|
user string
|
|
}
|
|
|
|
func buildInodeToProcessMap() (map[int64]*processInfo, error) {
|
|
inodeMap := make(map[int64]*processInfo)
|
|
|
|
procDir, err := os.Open("/proc")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer procDir.Close()
|
|
|
|
entries, err := procDir.Readdir(-1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
pidStr := entry.Name()
|
|
pid, err := strconv.Atoi(pidStr)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
procInfo, err := getProcessInfo(pid)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
fdDir := filepath.Join("/proc", pidStr, "fd")
|
|
fdEntries, err := os.ReadDir(fdDir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, fdEntry := range fdEntries {
|
|
fdPath := filepath.Join(fdDir, fdEntry.Name())
|
|
link, err := os.Readlink(fdPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(link, "socket:[") && strings.HasSuffix(link, "]") {
|
|
inodeStr := link[8 : len(link)-1]
|
|
inode, err := strconv.ParseInt(inodeStr, 10, 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
inodeMap[inode] = procInfo
|
|
}
|
|
}
|
|
}
|
|
|
|
return inodeMap, nil
|
|
}
|
|
|
|
func getProcessInfo(pid int) (*processInfo, error) {
|
|
info := &processInfo{pid: pid}
|
|
|
|
commPath := filepath.Join("/proc", strconv.Itoa(pid), "comm")
|
|
commData, err := os.ReadFile(commPath)
|
|
if err == nil && len(commData) > 0 {
|
|
info.command = strings.TrimSpace(string(commData))
|
|
}
|
|
|
|
if info.command == "" {
|
|
cmdlinePath := filepath.Join("/proc", strconv.Itoa(pid), "cmdline")
|
|
cmdlineData, err := os.ReadFile(cmdlinePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(cmdlineData) > 0 {
|
|
parts := bytes.Split(cmdlineData, []byte{0})
|
|
if len(parts) > 0 && len(parts[0]) > 0 {
|
|
fullPath := string(parts[0])
|
|
baseName := filepath.Base(fullPath)
|
|
if strings.Contains(baseName, " ") {
|
|
baseName = strings.Fields(baseName)[0]
|
|
}
|
|
info.command = baseName
|
|
}
|
|
}
|
|
}
|
|
|
|
statusPath := filepath.Join("/proc", strconv.Itoa(pid), "status")
|
|
statusFile, err := os.Open(statusPath)
|
|
if err != nil {
|
|
return info, nil
|
|
}
|
|
defer statusFile.Close()
|
|
|
|
scanner := bufio.NewScanner(statusFile)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.HasPrefix(line, "Uid:") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 2 {
|
|
uid, err := strconv.Atoi(fields[1])
|
|
if err == nil {
|
|
info.uid = uid
|
|
u, err := user.LookupId(strconv.Itoa(uid))
|
|
if err == nil {
|
|
info.user = u.Username
|
|
} else {
|
|
info.user = strconv.Itoa(uid)
|
|
}
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
func parseProcNet(path, proto string, ipVersion int, inodeMap map[int64]*processInfo) ([]Connection, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
var connections []Connection
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
scanner.Scan()
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 10 {
|
|
continue
|
|
}
|
|
|
|
localAddr, localPort, err := parseHexAddr(fields[1])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
remoteAddr, remotePort, err := parseHexAddr(fields[2])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
stateHex := fields[3]
|
|
state := parseState(stateHex, proto)
|
|
|
|
inode, _ := strconv.ParseInt(fields[9], 10, 64)
|
|
|
|
conn := Connection{
|
|
TS: time.Now(),
|
|
Proto: proto,
|
|
IPVersion: fmt.Sprintf("IPv%d", ipVersion),
|
|
State: state,
|
|
Laddr: localAddr,
|
|
Lport: localPort,
|
|
Raddr: remoteAddr,
|
|
Rport: remotePort,
|
|
Inode: inode,
|
|
}
|
|
|
|
if procInfo, exists := inodeMap[inode]; exists {
|
|
conn.PID = procInfo.pid
|
|
conn.Process = procInfo.command
|
|
conn.UID = procInfo.uid
|
|
conn.User = procInfo.user
|
|
}
|
|
|
|
conn.Interface = guessNetworkInterface(localAddr)
|
|
|
|
connections = append(connections, conn)
|
|
}
|
|
|
|
return connections, scanner.Err()
|
|
}
|
|
|
|
func parseState(hexState, proto string) string {
|
|
state, err := strconv.ParseInt(hexState, 16, 32)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
tcpStates := map[int64]string{
|
|
0x01: "ESTABLISHED",
|
|
0x02: "SYN_SENT",
|
|
0x03: "SYN_RECV",
|
|
0x04: "FIN_WAIT1",
|
|
0x05: "FIN_WAIT2",
|
|
0x06: "TIME_WAIT",
|
|
0x07: "CLOSE",
|
|
0x08: "CLOSE_WAIT",
|
|
0x09: "LAST_ACK",
|
|
0x0A: "LISTEN",
|
|
0x0B: "CLOSING",
|
|
}
|
|
|
|
if strings.HasPrefix(proto, "tcp") {
|
|
if s, exists := tcpStates[state]; exists {
|
|
return s
|
|
}
|
|
} else {
|
|
if state == 0x07 {
|
|
return "CLOSE"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func parseHexAddr(hexAddr string) (string, int, error) {
|
|
parts := strings.Split(hexAddr, ":")
|
|
if len(parts) != 2 {
|
|
return "", 0, fmt.Errorf("invalid address format")
|
|
}
|
|
|
|
hexIP := parts[0]
|
|
|
|
port, err := strconv.ParseInt(parts[1], 16, 32)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
|
|
if len(hexIP) == 8 {
|
|
ip1, _ := strconv.ParseInt(hexIP[6:8], 16, 32)
|
|
ip2, _ := strconv.ParseInt(hexIP[4:6], 16, 32)
|
|
ip3, _ := strconv.ParseInt(hexIP[2:4], 16, 32)
|
|
ip4, _ := strconv.ParseInt(hexIP[0:2], 16, 32)
|
|
addr := fmt.Sprintf("%d.%d.%d.%d", ip1, ip2, ip3, ip4)
|
|
|
|
if addr == "0.0.0.0" {
|
|
addr = "*"
|
|
}
|
|
|
|
return addr, int(port), nil
|
|
} else if len(hexIP) == 32 {
|
|
var ipv6Parts []string
|
|
for i := 0; i < 32; i += 8 {
|
|
word := hexIP[i : i+8]
|
|
p1 := word[6:8] + word[4:6] + word[2:4] + word[0:2]
|
|
ipv6Parts = append(ipv6Parts, p1)
|
|
}
|
|
|
|
fullAddr := strings.Join(ipv6Parts, "")
|
|
var formatted []string
|
|
for i := 0; i < len(fullAddr); i += 4 {
|
|
formatted = append(formatted, fullAddr[i:i+4])
|
|
}
|
|
addr := strings.Join(formatted, ":")
|
|
|
|
addr = simplifyIPv6(addr)
|
|
|
|
if addr == "::" || addr == "0:0:0:0:0:0:0:0" {
|
|
addr = "*"
|
|
}
|
|
|
|
return addr, int(port), nil
|
|
}
|
|
|
|
return "", 0, fmt.Errorf("unsupported address format")
|
|
}
|
|
|
|
func GetUnixSockets() ([]Connection, error) {
|
|
connections := []Connection{}
|
|
|
|
file, err := os.Open("/proc/net/unix")
|
|
if err != nil {
|
|
return connections, nil
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
scanner.Scan()
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 7 {
|
|
continue
|
|
}
|
|
|
|
inode, _ := strconv.ParseInt(fields[6], 10, 64)
|
|
path := ""
|
|
if len(fields) > 7 {
|
|
path = fields[7]
|
|
}
|
|
|
|
conn := Connection{
|
|
TS: time.Now(),
|
|
Proto: "unix",
|
|
Laddr: path,
|
|
Raddr: "",
|
|
State: "CONNECTED",
|
|
Inode: inode,
|
|
Interface: "unix",
|
|
}
|
|
|
|
connections = append(connections, conn)
|
|
}
|
|
|
|
return connections, nil
|
|
}
|
|
|