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

import (
	"errors"
	"fmt"
	"slices"
	"sort"
	"strconv"
	"strings"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/clusterstack"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/portallocator"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/procgroup"
)

type Survivors map[string][]string

type Drop struct {
	Cluster string
	Port    int
	PID     int

	Argv []string

	Reason string
}

type Stack struct {
	Cluster string

	Spec clusterstack.Spec

	Prefix []string

	Argvs [][]string
}

var ErrNoAttribution = errors.New(
	"portscan: refusing to scan: no cluster stack was supplied to attribute a bound process " +
		"to, and a process that is not ours must never become a kill target")

func Scan(leases []portallocator.Lease, stacks []Stack) (Survivors, []Drop, error) {
	expected, err := expectations(stacks)
	if err != nil {
		return nil, nil, err
	}
	stream, err := listeners()
	if err != nil {
		return nil, nil, err
	}
	datagram, err := datagramListeners()
	if err != nil {
		return nil, nil, err
	}
	survivors, dropped := scan(leases, expected, byTransport(leases, stream, datagram), cmdline)
	return survivors, dropped, nil
}

func ListenerPIDs(port int) ([]int, error) {
	stream, err := listeners()
	if err != nil {
		return nil, err
	}
	return append([]int(nil), stream[port]...), nil
}

func byTransport(leases []portallocator.Lease, stream, datagram map[int][]int) map[int][]int {
	out := map[int][]int{}
	for _, l := range leases {
		owners := stream
		if l.Purpose.Datagram() {
			owners = datagram
		}
		for p := l.Port; p < l.Port+l.Width; p++ {
			if pids, ok := owners[p]; ok {
				out[p] = pids
			}
		}
	}
	return out
}

func scan(
	leases []portallocator.Lease,
	expected map[string][][]string,
	owners map[int][]int,
	argvOf func(pid int) ([]string, error),
) (Survivors, []Drop) {
	kept, dropped := attribute(leases, expected, owners, argvOf)
	return associate(leases, kept), dropped
}

type claim struct {
	Cluster string
	Port    int
}

type verdict struct {
	Cluster string
	PID     int
}

func expectations(stacks []Stack) (map[string][][]string, error) {
	if len(stacks) == 0 {
		return nil, ErrNoAttribution
	}
	out := make(map[string][][]string, len(stacks))
	for _, s := range stacks {
		if s.Cluster == "" {
			return nil, errors.New("portscan: a Stack with no Cluster names no lease to attribute to")
		}
		argvs, err := s.spawned()
		if err != nil {
			return nil, err
		}
		if len(argvs) == 0 {
			return nil, fmt.Errorf("portscan: %s: the stack has no component with an argv", s.Cluster)
		}
		out[s.Cluster] = argvs
	}
	return out, nil
}

func (s Stack) spawned() ([][]string, error) {
	if len(s.Argvs) > 0 {
		out := make([][]string, 0, len(s.Argvs))
		for _, argv := range s.Argvs {
			if len(argv) == 0 {
				return nil, fmt.Errorf(
					"portscan: %s: a component with an EMPTY argv identifies nothing, and an "+
						"empty vector matches nothing, so the stack this caller described is not "+
						"the one it spawns", s.Cluster)
			}
			out = append(out, append(append([]string{}, s.Prefix...), argv...))
		}
		return out, nil
	}
	var out [][]string
	for _, c := range procgroup.StackOrder {
		argv, err := s.Spec.Argv(c)
		if errors.Is(err, clusterstack.ErrNotAProcess) {
			continue
		}
		if errors.Is(err, clusterstack.ErrMissingExternalSecrets) && s.Spec.ExternalSecrets == (clusterstack.ExternalSecrets{}) {
			continue
		}
		if errors.Is(err, clusterstack.ErrMissingFront) && len(s.Spec.FrontExec) == 0 {
			continue
		}
		if err != nil {
			return nil, fmt.Errorf("portscan: %s: %w", s.Cluster, err)
		}
		out = append(out, append(append([]string{}, s.Prefix...), argv...))
	}
	return out, nil
}

func attribute(
	leases []portallocator.Lease,
	expected map[string][][]string,
	owners map[int][]int,
	argvOf func(pid int) ([]string, error),
) (map[claim][]int, []Drop) {
	kept := map[claim][]int{}
	var dropped []Drop
	verdicts := map[verdict]bool{}

	for _, l := range leases {
		want := expected[l.Cluster]
		for p := l.Port; p < l.Port+l.Width; p++ {
			held := claim{Cluster: l.Cluster, Port: p}
			for _, pid := range owners[p] {
				key := verdict{Cluster: l.Cluster, PID: pid}
				mine, decided := verdicts[key]
				if !decided {
					argv, err := argvOf(pid)
					mine = err == nil && attributed(want, argv)
					verdicts[key] = mine
					if !mine {
						dropped = append(dropped, Drop{
							Cluster: l.Cluster,
							Port:    p,
							PID:     pid,
							Argv:    argv,
							Reason:  whyNot(err, want),
						})
					}
				}
				if mine {
					kept[held] = append(kept[held], pid)
				}
			}
		}
	}
	sort.Slice(dropped, func(i, j int) bool {
		if dropped[i].Cluster != dropped[j].Cluster {
			return dropped[i].Cluster < dropped[j].Cluster
		}
		if dropped[i].Port != dropped[j].Port {
			return dropped[i].Port < dropped[j].Port
		}
		return dropped[i].PID < dropped[j].PID
	})
	return kept, dropped
}

func attributed(expected [][]string, argv []string) bool {
	return slices.ContainsFunc(expected, func(component []string) bool {
		return containsAll(argv, component)
	})
}

func containsAll(argv, tokens []string) bool {
	if len(tokens) == 0 {
		return false
	}
	for _, t := range tokens {
		if !slices.ContainsFunc(argv, func(a string) bool { return strings.Contains(a, t) }) {
			return false
		}
	}
	return true
}

func whyNot(err error, expected [][]string) string {
	if err != nil {
		return fmt.Sprintf("could not read the process's own command line: %v", err)
	}
	if len(expected) == 0 {
		return "no stack was supplied for this cluster, so its owner cannot be attributed at all"
	}
	return "the command line is not any of this cluster's components as the stack spawns them"
}

func associate(leases []portallocator.Lease, owners map[claim][]int) Survivors {
	byCluster := map[string]map[int]bool{}
	for _, l := range leases {
		for p := l.Port; p < l.Port+l.Width; p++ {
			for _, pid := range owners[claim{Cluster: l.Cluster, Port: p}] {
				if byCluster[l.Cluster] == nil {
					byCluster[l.Cluster] = map[int]bool{}
				}
				byCluster[l.Cluster][pid] = true
			}
		}
	}
	out := make(Survivors, len(byCluster))
	for cluster, pids := range byCluster {
		nums := make([]int, 0, len(pids))
		for pid := range pids {
			nums = append(nums, pid)
		}
		sort.Ints(nums)
		handles := make([]string, len(nums))
		for i, pid := range nums {
			handles[i] = strconv.Itoa(pid)
		}
		out[cluster] = handles
	}
	return out
}