~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
159 lines · 3.7 KB Raw History
package portscan

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

const tcpListen = "0A"

const udpBound = "07"

type listenSocket struct {
	Port  int
	Inode uint64
}

func parseProcNetTCP(r io.Reader) ([]listenSocket, error) {
	return parseProcNet(r, tcpListen)
}

func parseProcNetUDP(r io.Reader) ([]listenSocket, error) {
	return parseProcNet(r, udpBound)
}

func parseProcNet(r io.Reader, state string) ([]listenSocket, error) {
	sc := bufio.NewScanner(r)
	var out []listenSocket
	for sc.Scan() {
		fields := strings.Fields(sc.Text())
		if len(fields) == 0 || fields[0] == "sl" {
			continue
		}
		if len(fields) < 10 {
			return nil, fmt.Errorf("portscan: %q is not a socket-table row", sc.Text())
		}
		if fields[3] != state {
			continue
		}
		_, portHex, ok := strings.Cut(fields[1], ":")
		if !ok {
			return nil, fmt.Errorf("portscan: local_address %q has no port", fields[1])
		}
		port, err := strconv.ParseUint(portHex, 16, 16)
		if err != nil {
			return nil, fmt.Errorf("portscan: local port %q: %w", portHex, err)
		}
		inode, err := strconv.ParseUint(fields[9], 10, 64)
		if err != nil {
			return nil, fmt.Errorf("portscan: inode %q: %w", fields[9], err)
		}
		out = append(out, listenSocket{Port: int(port), Inode: inode})
	}
	if err := sc.Err(); err != nil {
		return nil, fmt.Errorf("portscan: reading the socket table: %w", err)
	}
	return out, nil
}

func socketInode(link string) (uint64, bool) {
	rest, ok := strings.CutPrefix(link, "socket:[")
	if !ok {
		return 0, false
	}
	rest, ok = strings.CutSuffix(rest, "]")
	if !ok {
		return 0, false
	}
	inode, err := strconv.ParseUint(rest, 10, 64)
	if err != nil {
		return 0, false
	}
	return inode, true
}

func walkInodeOwners(root string, want map[uint64]bool) (map[uint64][]int, error) {
	entries, err := os.ReadDir(root)
	if err != nil {
		return nil, fmt.Errorf("portscan: reading the process table %s: %w", root, err)
	}
	out := map[uint64][]int{}
	for _, entry := range entries {
		pid, err := strconv.Atoi(entry.Name())
		if err != nil {
			continue
		}
		fdDir := filepath.Join(root, entry.Name(), "fd")
		fds, err := os.ReadDir(fdDir)
		if err != nil {
			continue
		}
		for _, fd := range fds {
			target, err := os.Readlink(filepath.Join(fdDir, fd.Name()))
			if err != nil {
				continue
			}
			inode, ok := socketInode(target)
			if !ok || !want[inode] {
				continue
			}
			out[inode] = append(out[inode], pid)
		}
	}
	return out, nil
}

func listenersIn(root string) (map[int][]int, error) {
	return ownersIn(root, "tcp", parseProcNetTCP)
}

func datagramListenersIn(root string) (map[int][]int, error) {
	return ownersIn(root, "udp", parseProcNetUDP)
}

func ownersIn(root, table string, parse func(io.Reader) ([]listenSocket, error)) (map[int][]int, error) {
	socks, err := readTable(filepath.Join(root, "net", table), true, parse)
	if err != nil {
		return nil, err
	}
	six, err := readTable(filepath.Join(root, "net", table+"6"), false, parse)
	if err != nil {
		return nil, err
	}
	socks = append(socks, six...)

	want := make(map[uint64]bool, len(socks))
	for _, s := range socks {
		want[s.Inode] = true
	}
	owners, err := walkInodeOwners(root, want)
	if err != nil {
		return nil, err
	}
	byPort := map[int][]int{}
	for _, s := range socks {
		if len(owners[s.Inode]) == 0 {
			continue
		}
		byPort[s.Port] = append(byPort[s.Port], owners[s.Inode]...)
	}
	return byPort, nil
}

func readTable(path string, required bool, parse func(io.Reader) ([]listenSocket, error)) ([]listenSocket, error) {
	f, err := os.Open(path)
	if err != nil {
		if !required && errors.Is(err, os.ErrNotExist) {
			return nil, nil
		}
		return nil, fmt.Errorf("portscan: opening %s: %w", path, err)
	}
	defer f.Close()
	return parse(f)
}