model

package
v1.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 30, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package model evaluates whether a packet from a given source zone reaches a listener, using only a facts snapshot. It is pure: no netlink, no /proc, no Docker, no clock. That is what makes it testable.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ForwardNotes added in v1.7.0

func ForwardNotes(f facts.Facts) []facts.Warning

ForwardNotes are the destination rewrites in the ruleset whose forwarded ports whyopen will not turn into rows: a rule that constrains no port at all forwards every one of them, and a range forwards more than a table should list.

They are warnings rather than verdicts because whyopen reports one port per row and these name no single port. Saying nothing was the bug this whole scan exists to fix, so what cannot become a row has to become a sentence.

func MatchRule

func MatchRule(pkt *Packet, r facts.Rule, sets []facts.Set) (Outcome, Action)

MatchRule evaluates one rule against the packet. It returns OutcomeUnknown the moment it meets anything it cannot resolve, so a verdict is never built on a guess. sets is the rule's own table's sets (docs/decisions/0005), needed to resolve a facts.ExprLookup; a rule with none can pass nil.

func Traverse

func Traverse(rs facts.Ruleset, family, hook string, pkt *Packet) (Result, []Hit)

Traverse pushes the packet through every base chain registered on one hook, in ascending priority order, and returns the resulting verdict with the ordered list of rules that produced it.

Types

type Action

type Action struct {
	Kind  string // accept | drop | return | jump | goto | continue | dnat | none
	Chain string
	DNAT  *DNAT
}

Action is what a matching rule does.

type DNAT added in v0.4.0

type DNAT struct {
	IP   netip.Addr
	Port uint16
}

DNAT is the single resolved rewrite target for a matched DNAT rule. Unlike facts.DNATInfo, which genuinely describes a port range (MinPort/MaxPort), a DNAT value names one concrete port whyopen will follow, so the field is just Port. It is exported so that report can render it: the verdict schema report writes is deliberately its own shape, not this type marshalled, but it still has to read this one.

type Endpoint

type Endpoint struct {
	Kind   string `json:"kind"` // socket | publish | forward
	Family string `json:"family"`
	Proto  string `json:"proto"`
	BindIP string `json:"bind_ip"`
	Port   uint16 `json:"port"`
	Owner  string `json:"owner,omitempty"`
}

Endpoint is something that can receive a connection. It comes from a listening socket on the host, from a Docker publish, because a container's socket lives in another network namespace and never appears in the host's /proc/net/tcp, or from a destination rewrite in the ruleset, because a port this host forwards to another machine has neither a socket nor a publish here (see forwards).

type Hit

type Hit struct {
	Family   string     `json:"family"`
	Table    string     `json:"table"`
	Chain    string     `json:"chain"`
	Hook     string     `json:"hook,omitempty"`
	Priority int32      `json:"priority"`
	Handle   uint64     `json:"handle"`
	Action   string     `json:"action"`
	Rule     facts.Rule `json:"-"`
}

Hit is one rule the packet actually reached, in traversal order.

type Outcome

type Outcome int
const (
	OutcomeMatch Outcome = iota
	OutcomeNoMatch
	OutcomeUnknown
	// OutcomeSkipped is a rule carrying an expression whyopen cannot
	// resolve, in a rule with no verdict, so neither outcome of the match
	// changes where the packet goes. It steers traversal exactly like
	// OutcomeNoMatch and exists only so the rule can still be recorded in
	// the path: a reader chasing an unresolved expression should see the
	// rule that carried it, not a gap where it was.
	OutcomeSkipped
)

func (Outcome) String

func (o Outcome) String() string

type Packet

type Packet struct {
	Family     string // ip | ip6
	Proto      string // tcp | udp
	Src        netip.Addr
	Dst        netip.Addr
	SrcPort    uint16
	DstPort    uint16
	InIface    string
	OutIface   string
	CtState    string
	DstIsLocal bool
	// DNATApplied says a rule earlier on this packet's path rewrote its
	// destination, which is what `ct status dnat` asks about. whyopen is
	// the thing that applied the rewrite, so this is not an assumption
	// about conntrack: it is what happened in the traversal.
	//
	// It is set for the hook walked after the rewrite, not within the
	// prerouting walk that performs it, so a `ct status dnat` rule placed
	// after the DNAT rule in the same chain reads as not-yet-rewritten.
	// That errs toward reporting the packet as continuing rather than as
	// stopped, which is the safe direction for an exposure audit.
	DNATApplied bool
	// SrcRouteDev is the device the host would route a reply to Src out
	// of, or empty when whyopen could not resolve one. It answers a fib
	// presence lookup and nothing else. Empty is not "no route exists":
	// it is "whyopen cannot say", which is why the evaluator refuses
	// rather than concluding the route is missing
	// (docs/decisions/0012-fib-and-routes.md).
	SrcRouteDev string
	// InIfaceIndex and OutIfaceIndex are the kernel interface indices of
	// InIface and OutIface, which `meta iif` and `meta oif` compare
	// against. Zero means whyopen does not know the index, which is not
	// the same as index zero: the evaluator refuses rather than compare
	// against a value it made up.
	InIfaceIndex  uint32
	OutIfaceIndex uint32
}

Packet is the synthetic probe pushed through the ruleset. CtState is always "new": whyopen asks whether a fresh inbound connection can be established, which is what makes "ct state related,established accept" correctly fail to match.

type Result

type Result struct {
	Kind   string // accept | drop | unknown
	Reason string
	DNAT   *DNAT
}

Result is the outcome of one hook: accept, drop or unknown. walkChain uses two further kinds internally, neither of which can escape a base chain: "none" for a regular chain that fell through, and "unwind" for a goto that fell through, which resumes at the base chain's policy rather than in any calling chain.

type Verdict

type Verdict struct {
	Endpoint Endpoint `json:"endpoint"`
	Family   string   `json:"family"`
	Result   string   `json:"result"` // reachable | filtered | unknown
	Reason   string   `json:"reason,omitempty"`
	Path     []Hit    `json:"path,omitempty"`
	DNAT     *DNAT    `json:"-"`
}

func Evaluate

func Evaluate(f facts.Facts, zone Zone) []Verdict

Evaluate returns one verdict per endpoint per address family it serves.

type Zone

type Zone struct {
	Name string
	Src4 netip.Addr
	Src6 netip.Addr
}

Zone is where the synthetic packet comes from. v1 ships one.

func InternetZone

func InternetZone() Zone

InternetZone sources from documentation ranges, which are never a local address on a sane host, so the packet cannot be mistaken for local traffic.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL