245 lines
6.7 KiB
Go
245 lines
6.7 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/karol-broda/snitch/internal/collector"
|
|
"github.com/karol-broda/snitch/internal/color"
|
|
"github.com/karol-broda/snitch/internal/config"
|
|
"github.com/karol-broda/snitch/internal/resolver"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// Runtime holds the shared state for all commands.
|
|
// it handles common filter logic, fetching, filtering, and resolution.
|
|
type Runtime struct {
|
|
// filter options built from flags and args
|
|
Filters collector.FilterOptions
|
|
|
|
// filtered connections ready for rendering
|
|
Connections []collector.Connection
|
|
|
|
// common settings
|
|
ColorMode string
|
|
ResolveAddrs bool
|
|
ResolvePorts bool
|
|
NoCache bool
|
|
}
|
|
|
|
// shared filter flags - used by all commands
|
|
var (
|
|
filterTCP bool
|
|
filterUDP bool
|
|
filterListen bool
|
|
filterEstab bool
|
|
filterIPv4 bool
|
|
filterIPv6 bool
|
|
)
|
|
|
|
// shared resolution flags - used by all commands
|
|
var (
|
|
resolveAddrs bool
|
|
resolvePorts bool
|
|
noCache bool
|
|
)
|
|
|
|
// BuildFilters constructs FilterOptions from command args and shortcut flags.
|
|
func BuildFilters(args []string) (collector.FilterOptions, error) {
|
|
filters, err := ParseFilterArgs(args)
|
|
if err != nil {
|
|
return filters, err
|
|
}
|
|
|
|
// apply ipv4/ipv6 flags
|
|
filters.IPv4 = filterIPv4
|
|
filters.IPv6 = filterIPv6
|
|
|
|
// apply protocol shortcut flags
|
|
if filterTCP && !filterUDP {
|
|
filters.Proto = "tcp"
|
|
} else if filterUDP && !filterTCP {
|
|
filters.Proto = "udp"
|
|
}
|
|
|
|
// apply state shortcut flags
|
|
if filterListen && !filterEstab {
|
|
filters.State = "LISTEN"
|
|
} else if filterEstab && !filterListen {
|
|
filters.State = "ESTABLISHED"
|
|
}
|
|
|
|
return filters, nil
|
|
}
|
|
|
|
// FetchConnections gets connections from the collector and applies filters.
|
|
func FetchConnections(filters collector.FilterOptions) ([]collector.Connection, error) {
|
|
connections, err := collector.GetConnections()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return collector.FilterConnections(connections, filters), nil
|
|
}
|
|
|
|
// NewRuntime creates a runtime with fetched and filtered connections.
|
|
func NewRuntime(args []string, colorMode string) (*Runtime, error) {
|
|
color.Init(colorMode)
|
|
|
|
cfg := config.Get()
|
|
|
|
// configure resolver with cache setting (flag overrides config)
|
|
effectiveNoCache := noCache || !cfg.Defaults.DNSCache
|
|
resolver.SetNoCache(effectiveNoCache)
|
|
|
|
filters, err := BuildFilters(args)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse filters: %w", err)
|
|
}
|
|
|
|
connections, err := FetchConnections(filters)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch connections: %w", err)
|
|
}
|
|
|
|
rt := &Runtime{
|
|
Filters: filters,
|
|
Connections: connections,
|
|
ColorMode: colorMode,
|
|
ResolveAddrs: resolveAddrs,
|
|
ResolvePorts: resolvePorts,
|
|
NoCache: effectiveNoCache,
|
|
}
|
|
|
|
// pre-warm dns cache by resolving all addresses in parallel
|
|
if resolveAddrs {
|
|
rt.PreWarmDNS()
|
|
}
|
|
|
|
return rt, nil
|
|
}
|
|
|
|
// PreWarmDNS resolves all connection addresses in parallel to warm the cache.
|
|
func (r *Runtime) PreWarmDNS() {
|
|
addrs := make([]string, 0, len(r.Connections)*2)
|
|
for _, c := range r.Connections {
|
|
addrs = append(addrs, c.Laddr, c.Raddr)
|
|
}
|
|
resolver.ResolveAddrsParallel(addrs)
|
|
}
|
|
|
|
// SortConnections sorts the runtime's connections in place.
|
|
func (r *Runtime) SortConnections(opts collector.SortOptions) {
|
|
collector.SortConnections(r.Connections, opts)
|
|
}
|
|
|
|
// ParseFilterArgs parses key=value filter arguments.
|
|
// exported for testing.
|
|
func ParseFilterArgs(args []string) (collector.FilterOptions, error) {
|
|
filters := collector.FilterOptions{}
|
|
for _, arg := range args {
|
|
parts := strings.SplitN(arg, "=", 2)
|
|
if len(parts) != 2 {
|
|
return filters, fmt.Errorf("invalid filter format: %s (expected key=value)", arg)
|
|
}
|
|
key, value := parts[0], parts[1]
|
|
if err := applyFilter(&filters, key, value); err != nil {
|
|
return filters, err
|
|
}
|
|
}
|
|
return filters, nil
|
|
}
|
|
|
|
// applyFilter applies a single key=value filter to FilterOptions.
|
|
func applyFilter(filters *collector.FilterOptions, key, value string) error {
|
|
switch strings.ToLower(key) {
|
|
case "proto":
|
|
filters.Proto = value
|
|
case "state":
|
|
filters.State = value
|
|
case "pid":
|
|
pid, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid pid value: %s", value)
|
|
}
|
|
filters.Pid = pid
|
|
case "proc":
|
|
filters.Proc = value
|
|
case "lport":
|
|
port, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid lport value: %s", value)
|
|
}
|
|
filters.Lport = port
|
|
case "rport":
|
|
port, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid rport value: %s", value)
|
|
}
|
|
filters.Rport = port
|
|
case "user":
|
|
uid, err := strconv.Atoi(value)
|
|
if err == nil {
|
|
filters.UID = uid
|
|
} else {
|
|
filters.User = value
|
|
}
|
|
case "laddr":
|
|
filters.Laddr = value
|
|
case "raddr":
|
|
filters.Raddr = value
|
|
case "contains":
|
|
filters.Contains = value
|
|
case "if", "interface":
|
|
filters.Interface = value
|
|
case "mark":
|
|
filters.Mark = value
|
|
case "namespace":
|
|
filters.Namespace = value
|
|
case "inode":
|
|
inode, err := strconv.ParseInt(value, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid inode value: %s", value)
|
|
}
|
|
filters.Inode = inode
|
|
case "since":
|
|
since, sinceRel, err := collector.ParseTimeFilter(value)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid since value: %s", value)
|
|
}
|
|
filters.Since = since
|
|
filters.SinceRel = sinceRel
|
|
default:
|
|
return fmt.Errorf("unknown filter key: %s", key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FilterFlagsHelp returns the help text for common filter flags.
|
|
const FilterFlagsHelp = `
|
|
Filters are specified in key=value format. For example:
|
|
snitch ls proto=tcp state=established
|
|
|
|
Available filters:
|
|
proto, state, pid, proc, lport, rport, user, laddr, raddr, contains, if, mark, namespace, inode, since`
|
|
|
|
// addFilterFlags adds the common filter flags to a command.
|
|
func addFilterFlags(cmd *cobra.Command) {
|
|
cmd.Flags().BoolVarP(&filterTCP, "tcp", "t", false, "Show only TCP connections")
|
|
cmd.Flags().BoolVarP(&filterUDP, "udp", "u", false, "Show only UDP connections")
|
|
cmd.Flags().BoolVarP(&filterListen, "listen", "l", false, "Show only listening sockets")
|
|
cmd.Flags().BoolVarP(&filterEstab, "established", "e", false, "Show only established connections")
|
|
cmd.Flags().BoolVarP(&filterIPv4, "ipv4", "4", false, "Only show IPv4 connections")
|
|
cmd.Flags().BoolVarP(&filterIPv6, "ipv6", "6", false, "Only show IPv6 connections")
|
|
}
|
|
|
|
// addResolutionFlags adds the common resolution flags to a command.
|
|
func addResolutionFlags(cmd *cobra.Command) {
|
|
cfg := config.Get()
|
|
cmd.Flags().BoolVar(&resolveAddrs, "resolve-addrs", !cfg.Defaults.Numeric, "Resolve IP addresses to hostnames")
|
|
cmd.Flags().BoolVar(&resolvePorts, "resolve-ports", false, "Resolve port numbers to service names")
|
|
cmd.Flags().BoolVar(&noCache, "no-cache", !cfg.Defaults.DNSCache, "Disable DNS caching (force fresh lookups)")
|
|
}
|
|
|