netctx

package
v0.32.0 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: GPL-3.0 Imports: 18 Imported by: 0

Documentation

Overview

Package netctx is a sensor's network self-description: what the host owner declares (site, routes it reaches, DNS suffixes, internet mode), what the SDK detects on the host (interfaces, routes, resolvers, the egress path to the platform) and a fingerprint that changes when the sensor moves. The sensor manifest carries the report (member "network") to a platform that lists the "network" hello feature.

Design: https://github.com/openctemio/openctem/blob/develop/api/docs/rfcs/RFC-089-reachability-aware-routing.md

Security. The report is a hint the platform may use only to take work away from this sensor or to rank sensors that are already authorized; it never authorizes anything. It discloses internal topology, so:

  • the default mode is summary (private prefixes listed, public ones counted), and the host owner can choose off (only the fingerprint and the internet mode leave the host);
  • the report never names a prefix outside the local policy's targets.allow when that is set (entries are narrowed or dropped and counted in Trimmed);
  • every list is bounded and sorted, so the report is deterministic;
  • detection reads local tables only (interfaces, the routing table, resolv.conf). The only network call is the Echo the caller supplies (the platform's egress echo): no cloud metadata endpoint, no LAN scanning, no DNS query.

Detection never changes local enforcement: the local policy keeps the final say on what this sensor scans.

Stability: Experimental (docs/STABILITY.md).

Index

Constants

View Source
const (
	// EnvSite is the site label of this host ("hn-dc1"): at most 64 of
	// [A-Za-z0-9._-].
	EnvSite = "SENSOR_NETWORK_SITE"
	// EnvReach lists the CIDRs this host routes to beyond its own links
	// (VPN, peering), comma-separated.
	EnvReach = "SENSOR_NETWORK_REACH"
	// EnvDNSSuffixes lists the DNS suffixes this host resolves with its
	// internal view ("corp.example"), comma-separated.
	EnvDNSSuffixes = "SENSOR_NETWORK_DNS_SUFFIXES"
	// EnvInternet overrides the detected internet mode: auto (default),
	// direct, proxy or none.
	EnvInternet = "SENSOR_INTERNET"
	// EnvReport is how much the report says: summary (default), full or
	// off.
	EnvReport = "SENSOR_NETWORK_REPORT"
	// EnvDNSResolvers is the resolvers a sensor's tools use instead of the
	// system's ("10.0.0.2,10.0.0.3:53"); read for the report and the DNS
	// view check, declared by the sensor that reads it.
	EnvDNSResolvers = "SENSOR_DNS_RESOLVERS"
)

Settings the host owner declares (environment variables).

View Source
const (
	MaxOnLink        = 32
	MaxReach         = 64
	MaxDNSSuffixes   = 32
	MaxResolvers     = 8
	MaxSearchDomains = 8
	MaxSiteLen       = 64
)

Bounds of a report and of a declaration.

View Source
const (
	ReasonDeclared        = "declared"
	ReasonEchoPublic      = "echo_public"
	ReasonEchoPrivate     = "echo_private"
	ReasonEchoUnsupported = "echo_unsupported"
	ReasonEchoFailed      = "echo_failed"
	ReasonNoEcho          = "no_echo"
	ReasonNoDefaultRoute  = "no_default_route"
)

Egress reasons (Egress.Reason): why Internet is what it is.

View Source
const DefaultEchoTimeout = 10 * time.Second

DefaultEchoTimeout bounds one egress echo.

View Source
const DefaultInterval = 5 * time.Minute

DefaultInterval is how often a Watcher detects again.

View Source
const ReportVersion = 1

ReportVersion is the report's schema version.

Variables

View Source
var ErrEchoUnsupported = errors.New("the platform does not serve the egress echo")

ErrEchoUnsupported is returned by an Echo when the platform does not serve the egress echo.

View Source
var ErrInvalidDeclaration = errors.New("invalid network declaration")

ErrInvalidDeclaration is wrapped by every ParseDeclared error.

Functions

func Fingerprint

func Fingerprint(d Declared, det Detected) string

Fingerprint is "sha256:" + the hex SHA-256 of the facts that change when the sensor moves: its site, its private on-link prefixes, its default gateways, its resolvers and search domains (each sorted). It is computed before any trimming, so a local policy edit does not read as a move. It is a change detector, not a secret: the facts are low-entropy.

Types

type Declared

type Declared struct {
	Site        string
	Reach       []netip.Prefix
	DNSSuffixes []string
	Internet    Internet
	Mode        Mode
}

Declared is what the host owner states about this host's network.

func ParseDeclared

func ParseDeclared(lookup func(string) (string, bool)) (Declared, error)

ParseDeclared reads the declaration from the environment (lookup is os.LookupEnv in a sensor). A value it cannot use is an error naming the setting, never a silent default: the host owner asked for something.

type Detected

type Detected struct {
	// OnLink are the prefixes of the host's own interfaces (container and
	// virtual bridges left out), sorted.
	OnLink []netip.Prefix
	// RoutesKnown: the routing table could be read.
	RoutesKnown bool
	// Routes are the usable routes (rejected and loopback left out).
	Routes []Route
	// DefaultV4 and DefaultV6: a default route exists (meaningful only
	// when RoutesKnown).
	DefaultV4, DefaultV6 bool
	// Gateways are the default routes' next hops, sorted.
	Gateways []netip.Addr
	// SystemResolvers are resolv.conf's name servers; Resolvers are the
	// ones the sensor's tools use (SENSOR_DNS_RESOLVERS when set).
	SystemResolvers, Resolvers []netip.Addr
	// ResolversFromEnv: Resolvers come from SENSOR_DNS_RESOLVERS.
	ResolversFromEnv bool
	// ResolvConfRead: resolv.conf could be read.
	ResolvConfRead bool
	// SearchDomains are resolv.conf's search (or domain) entries.
	SearchDomains []string
}

Detected is what detection found on the host.

func Detect

func Detect(src Sources) Detected

Detect reads the host's network facts from src. It makes no network call. A source that fails leaves its facts empty (unknown), never an error: a sensor runs without them.

func (Detected) Routed

func (d Detected) Routed(p netip.Prefix) bool

Routed reports whether p is covered by a route of d (a default route covers everything of its family). Unknown routes report true: nothing to warn about.

type Echo

type Echo func(ctx context.Context) (EchoResult, error)

Echo asks the platform which address this sensor's requests come from. It is the only network call detection makes.

type EchoResult

type EchoResult struct {
	Address netip.Addr
	// ViaProxy: the request went through a proxy (the control proxy).
	ViaProxy bool
}

EchoResult is what the platform's egress echo answered: the address it saw this sensor's request come from.

type Egress

type Egress struct {
	Internet Internet
	// Declared: the host owner set it (SENSOR_INTERNET).
	Declared bool
	Reason   string
	// Address is the address the platform saw (zero: no echo).
	Address  netip.Addr
	ViaProxy bool
}

Egress is the internet mode and how it was decided.

func ResolveEgress

func ResolveEgress(ctx context.Context, d Declared, det Detected, echo Echo, timeout time.Duration) Egress

ResolveEgress decides the internet mode. A declaration wins. Otherwise an echo the platform saw from a public address means the path crosses the internet (direct, or proxy when it went through one). An echo failure is "unknown", never "none": a blocked echo must not take internet work away from a sensor that has internet. "none" is detected only when the routing table is known and has no default route (and no proxy carried the echo).

type Iface

type Iface struct {
	Name  string
	Flags net.Flags
	// Addrs are the interface's addresses with their prefix lengths.
	Addrs []netip.Prefix
}

Iface is one network interface as detection sees it.

type Internet

type Internet string

Internet is how this host reaches the internet.

const (
	InternetAuto    Internet = "auto"
	InternetDirect  Internet = "direct"
	InternetProxy   Internet = "proxy"
	InternetNone    Internet = "none"
	InternetUnknown Internet = "unknown"
)

Internet modes. Auto is a declaration only (detect it); Unknown is a detection result only.

type Mode

type Mode string

Mode is how much a report says.

const (
	// ModeSummary lists private prefixes and counts public ones (default).
	ModeSummary Mode = "summary"
	// ModeFull lists public on-link prefixes too.
	ModeFull Mode = "full"
	// ModeOff sends only the fingerprint and the internet mode.
	ModeOff Mode = "off"
)

Report modes.

type Options

type Options struct {
	Declared Declared
	// Sources are where detection reads (zero value: DefaultSources).
	Sources *Sources
	// Echo is the platform's egress echo (nil: the internet mode is
	// declared, "none" without a default route, or "unknown").
	Echo        Echo
	EchoTimeout time.Duration
	// Scope returns the local policy's targets.allow (nil: no limit). It
	// is called on every refresh, so a reloaded policy applies.
	Scope func() Scope
}

Options configure a Watcher.

type Report

type Report struct {
	V    int  `json:"v"`
	Mode Mode `json:"mode"`
	// Fingerprint changes when the sensor moves (another network, other
	// gateways, resolvers, search domains or site); not when its egress
	// address changes.
	Fingerprint string   `json:"fingerprint"`
	Internet    Internet `json:"internet"`
	// InternetDeclared: the host owner set Internet.
	InternetDeclared bool `json:"internet_declared,omitempty"`
	// EgressAddress is the address the platform's echo saw (summary and
	// full only).
	EgressAddress string          `json:"egress_address,omitempty"`
	Declared      *ReportDeclared `json:"declared,omitempty"`
	Detected      *ReportDetected `json:"detected,omitempty"`
	// Trimmed counts prefixes narrowed or left out because the local
	// policy does not allow them, or because a list was over its bound.
	Trimmed int `json:"trimmed,omitempty"`
}

Report is the network member of the sensor manifest.

func BuildReport

func BuildReport(d Declared, det Detected, eg Egress, scope Scope) *Report

BuildReport assembles the report: bounded, sorted, never wider than scope, and as small as the declared mode asks.

type ReportDeclared

type ReportDeclared struct {
	Site        string   `json:"site,omitempty"`
	Reach       []string `json:"reach,omitempty"`
	DNSSuffixes []string `json:"dns_suffixes,omitempty"`
}

ReportDeclared is the host owner's declaration as reported.

type ReportDetected

type ReportDetected struct {
	// OnLink are the on-link prefixes: private ones (summary), all (full).
	OnLink []string `json:"on_link,omitempty"`
	// PublicOnLink counts the public on-link prefixes summary leaves out.
	PublicOnLink int `json:"public_on_link,omitempty"`
	// Routes says whether the routing table was read: "known" or
	// "unknown" (DefaultV4/DefaultV6 mean nothing then).
	Routes        string   `json:"routes"`
	DefaultV4     bool     `json:"default_route_v4,omitempty"`
	DefaultV6     bool     `json:"default_route_v6,omitempty"`
	Resolvers     []string `json:"resolvers,omitempty"`
	SearchDomains []string `json:"search_domains,omitempty"`
}

ReportDetected is what detection found, as reported.

type Route

type Route struct {
	Prefix  netip.Prefix
	Gateway netip.Addr // zero: on-link
	Iface   string
}

Route is one routing table entry.

type Scope

type Scope struct {
	// Set: targets.allow is set (or no target is allowed at all); only
	// prefixes inside Allow are named.
	Set   bool
	Allow []netip.Prefix
	// Deny are targets.deny ranges: a prefix inside one is not named.
	Deny []netip.Prefix
	// NoPrivate: private targets are refused, so no private prefix is
	// named.
	NoPrivate bool
}

Scope is what the local policy lets this sensor scan; the report names no prefix outside it.

type Sources

type Sources struct {
	// Interfaces lists the host's interfaces.
	Interfaces func() ([]Iface, error)
	// RouteV4 and RouteV6 return the Linux routing tables
	// (/proc/net/route, /proc/net/ipv6_route); nil, or an error, means
	// "unknown" (other systems).
	RouteV4, RouteV6 func() ([]byte, error)
	// ResolvConf returns /etc/resolv.conf.
	ResolvConf func() ([]byte, error)
	// Lookup reads the environment (SENSOR_DNS_RESOLVERS).
	Lookup func(string) (string, bool)
}

Sources are where detection reads from. The zero value reads the host (DefaultSources); tests replace each one.

func DefaultSources

func DefaultSources() Sources

DefaultSources reads this host.

type State

type State struct {
	Declared Declared
	Detected Detected
	Egress   Egress
	Report   *Report
	At       time.Time
}

State is one detection and the report built from it.

type Watcher

type Watcher struct {
	// contains filtered or unexported fields
}

Watcher detects the host's network facts on demand and on an interval and keeps the latest report. It is safe for concurrent use.

func NewWatcher

func NewWatcher(o Options) *Watcher

NewWatcher returns a watcher; nothing is detected until Refresh or Run.

func (*Watcher) Refresh

func (w *Watcher) Refresh(ctx context.Context) (State, bool)

Refresh detects now and reports whether the report changed (the first refresh always does).

func (*Watcher) Report

func (w *Watcher) Report() *Report

Report is the latest report (nil before the first refresh). The report is never modified once returned.

func (*Watcher) Rescope

func (w *Watcher) Rescope() (State, bool)

Rescope rebuilds the report from the latest detection under the current Scope, without detecting again or calling the echo (the local policy was reloaded: a narrower policy must narrow the report at once). It reports whether the report changed; false before the first refresh.

func (*Watcher) Run

func (w *Watcher) Run(ctx context.Context, interval time.Duration, onChange func(State))

Run refreshes every interval (DefaultInterval when ≤ 0) until ctx ends, calling onChange (when set) after a refresh that changed the report.

func (*Watcher) State

func (w *Watcher) State() (State, bool)

State is the latest detection (false before the first refresh).

Jump to

Keyboard shortcuts

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