simulation

package
v1.13.0 Latest Latest
Warning

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

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

Documentation

Overview

Package simulation builds a throwaway virtual network, runs netdoc inside it, and compares the diagnosis against what the scenario said should break.

Nothing here touches the host's networking. Every interface, route, resolver and firewall rule lives inside namespaces the simulator created and owns, and they cease to exist when the process tree that holds them dies. See netns_linux.go for the mechanism.

Index

Constants

View Source
const (
	// SuggestTransientNotResampled: the resolver recovered while the run was
	// still going and netdoc never asked it again.
	SuggestTransientNotResampled = "transient_fault_not_resampled"
	// SuggestTransientReportedPermanent: a failure that had already healed
	// before the run ended is described without any hint that it was temporary.
	SuggestTransientReportedPermanent = "transient_fault_reported_permanent"
	// SuggestTransientMissed: an impairment opened and closed entirely inside
	// one run and nothing was flagged.
	SuggestTransientMissed = "transient_fault_missed"
	// SuggestTimelineInconsistent: a probe succeeded while a probe it depends on
	// failed, and no fault transition happened during that run to explain it.
	SuggestTimelineInconsistent = "timeline_inconsistent"
)

Timeline-aware suggestion codes. Stable identifiers, like the rest.

View Source
const (
	DifficultyEasy   = "easy"
	DifficultyMedium = "medium"
	DifficultyHard   = "hard"
)

Difficulty levels. A level is reviewed metadata on a challenge family, not a score computed from the topology: it says how directly the symptoms expose the fault, whether the families differ, and how many plausible competing diagnoses the evidence leaves open.

View Source
const (
	ChallengeCorrect      = "correct"
	ChallengeIncorrect    = "incorrect"
	ChallengeUnrecognized = "unrecognized"
	ChallengeGaveUp       = "gave_up"
	ChallengeUnscoreable  = "unscoreable"
)

Scores one contestant can earn. There is deliberately no partial credit: the diagnosis model can say whether an answer names what the simulator observed, and inventing degrees of nearly-right would be scoring on resemblance.

ChallengeUnrecognized is not a softer ChallengeIncorrect. Both lose the round; they say different things about why, and the difference is the whole point of the contract. Incorrect means netdoc had the words for this condition and reached for different ones. Unrecognized means its vocabulary has no way to state the condition at all, so no report it could have written would have won, which is the finding worth acting on.

View Source
const (
	ChallengeHumanWins  = "human_wins"
	ChallengeNetdocWins = "network_doctor_wins"
	ChallengeDraw       = "draw"
	ChallengeNobodyWins = "nobody_wins"
	ChallengeNoResult   = "no_result"
)

Matchups.

View Source
const (
	OutcomeMatched     = "matched"
	OutcomeWrongStatus = "wrong_status"
	OutcomeWrongCause  = "wrong_cause"
	OutcomeWrongFamily = "wrong_family"
	OutcomeWrongFix    = "wrong_fix"
	OutcomeMissing     = "missing"
	OutcomeUnexpected  = "unexpected"
)

Check outcomes, in the order the report prints them.

View Source
const (
	ProcessExited    = "exited"
	ProcessTimedOut  = "timed_out"
	ProcessCancelled = "cancelled"
	ProcessSignaled  = "signaled"
	ProcessExecError = "exec_error"
)
View Source
const (
	SuggestMissedFinding    = "missed_finding"
	SuggestWrongSeverity    = "wrong_severity"
	SuggestWrongCause       = "wrong_cause"
	SuggestFalsePositive    = "false_positive"
	SuggestWrongVerdict     = "wrong_verdict"
	SuggestProbeTimedOut    = "probe_timed_out"
	SuggestNoFixHint        = "no_fix_hint"
	SuggestNondeterministic = "nondeterministic"
	SuggestNoDiagnosis      = "no_diagnosis"
)

Suggestion codes. Stable identifiers so a CI job can allow-list the ones a scenario is known to trip.

View Source
const (
	FamilyStateReachable   = "reachable"
	FamilyStateUnreachable = "unreachable"
	FamilyStateUnavailable = "unavailable"
	TargetStateRefused     = "refused"
)

Address family states a FamilyReachabilityEvidence can carry. Unavailable and unreachable are deliberately distinct: a family the node was never given an address in was not tested, which is not the same claim as a family that was dialed and did not answer.

TargetStateRefused is a third outcome only a dial of one specific port can produce, so it is not a family state: a family is reachable when any endpoint answers, and "refused" is a fact about a port.

View Source
const (
	HuntResultClean     = "clean"
	HuntResultFindings  = "findings"
	HuntResultError     = "error"
	HuntResultCancelled = "cancelled"
	HuntMaxShards       = 500
)
View Source
const (
	FindingFalseNegative           = "comparison_false_negative"
	FindingDiagnosticInstability   = "diagnostic_instability"
	FindingDiagnosticContradiction = "diagnostic_contradiction"
	FindingCoverageGap             = "coverage_gap"
	FindingUnexpectedRuntimeError  = "unexpected_runtime_error"
	FindingNetdocCrash             = "netdoc_crash"
	FindingNetdocHang              = "netdoc_hang"
	FindingCleanupFailure          = "cleanup_failure"
	FindingSimulatorFailure        = "simulator_failure"
	FindingGeneratorDefect         = "generator_defect"
)

Finding categories name what kind of disagreement a case produced. A category the oracle cannot establish from independent evidence is not a category, and there is deliberately no catch-all. "Network Doctor claimed a fault the network did not have" belongs to FindingDiagnosticContradiction, where the finding still names which dimension disagreed, and a probe that spent its deadline is either the correct diagnosis of an injected fault, a whole-process hang (FindingNetdocHang), or harness failure (FindingSimulatorFailure), never a kind of its own.

FindingDiagnosticInstability is the one a hunt never reaches on its own. It classifies a repeat suggestion, and a hunt runs each case once because two runs inside one live topology are not the same experiment. It stays defined so a caller who does ask for repeats, or a campaign report read through this taxonomy, lands somewhere honest.

View Source
const (
	// HuntGeneratorVersion is part of every manifest and seed domain. A future
	// algorithm change must increment it instead of silently changing old cases.
	HuntGeneratorVersion = "v6"

	HuntMaxFaults     = 3
	HuntMaxCases      = 500
	HuntMaxCaseNumber = 999999
)
View Source
const (
	ResultPass    = "PASS"    // every expectation held
	ResultPartial = "PARTIAL" // some expectations held
	ResultFail    = "FAIL"    // none did
	ResultError   = "ERROR"   // the simulation itself did not run
)

Overall results.

View Source
const (
	ServiceDNS  = "dns"
	ServiceHTTP = "http"
	// ServiceTCP accepts a connection and closes it, enough for the direct
	// egress probe, which only proves a handshake completes.
	ServiceTCP = "tcp"
	// ServiceSOCKS5 is a simulator-owned, no-auth CONNECT proxy. It supports
	// address and domain destinations; BIND and UDP ASSOCIATE are intentionally
	// outside the simulator's needs.
	ServiceSOCKS5 = "socks5"
	// ServiceHTTPConnect is a simulator-owned, no-auth HTTP CONNECT proxy. It
	// tunnels one host:port authority per connection and refuses everything
	// else with a status code; forward proxying of ordinary methods, upstream
	// chaining and authentication are outside the simulator's needs.
	ServiceHTTPConnect = "http_connect"
	// ServiceTLS generates an in-memory private CA and leaf key, writes only the
	// public CA certificate to the simulator workspace, and serves bounded TLS.
	ServiceTLS = "tls"
	// ServiceQUIC completes a real QUIC handshake with h3 ALPN over UDP.
	ServiceQUIC = "quic"
	// ServiceEncryptedDNS answers netdoc's encrypted-DNS probe over both
	// transports from one static zone: RFC 8484 DoH on the service port and RFC
	// 7858 DoT on 853. It also accepts the plain TCP connect the direct-egress
	// probe makes, so it stands in for a tcp service on the same port.
	ServiceEncryptedDNS = "encrypted_dns"
	// ServiceTCPReset accepts a TCP handshake and closes with SO_LINGER=0 so a
	// protocol probe observes ECONNRESET rather than connection refusal.
	ServiceTCPReset = "tcp_reset"
)

Service types.

View Source
const (
	TLSCertificateValid            = "valid"
	TLSCertificateExpired          = "expired"
	TLSCertificateNotYetValid      = "not_yet_valid"
	TLSCertificateHostnameMismatch = "hostname_mismatch"
	DoHResponseInvalid             = "invalid"
)
View Source
const (
	// FaultDrop discards matching packets with an nftables rule. Direction
	// decides whether the sender is refused or left waiting; see Fault.
	FaultDrop = "drop"
	// FaultNetem attaches delay/jitter/loss to the node's segment interface.
	FaultNetem = "netem"
	// FaultNoDefaultRoute deletes the node's default route.
	FaultNoDefaultRoute = "no_default_route"
	// FaultReplaceDefaultRoute replaces every default route on a node with one
	// validated on-link next hop.
	FaultReplaceDefaultRoute = "replace_default_route"
	// FaultLinkDown administratively lowers one logical node interface.
	FaultLinkDown = "link_down"
	// FaultPMTUBlackhole narrows one router interface and drops the ICMP
	// fragmentation-needed replies that router would send about it, which is
	// the pair of conditions a path-MTU black hole is made of. Narrowing alone
	// is not one: a router that reports the smaller MTU is discovered and
	// worked around, and narrowing an endpoint instead makes the local kernel
	// refuse the send. Both endpoints must keep believing the path is wide,
	// and the hop that knows better must stay silent.
	FaultPMTUBlackhole = "pmtu_blackhole"
)

Fault types.

View Source
const (
	DirectionOutbound = "outbound"
	DirectionInbound  = "inbound"
)

FaultDrop directions.

View Source
const (
	DNSOutcomeAnswer      = "answer"
	DNSOutcomeSERVFAIL    = "servfail"
	DNSOutcomeREFUSED     = "refused"
	DNSOutcomeTruncated   = "truncated"
	DNSOutcomeWrongAnswer = "wrong_answer"
)
View Source
const (
	// FaultScheduledNetem moves one node interface between netem states.
	FaultScheduledNetem = "scheduled_netem"
	// FaultScheduledDNS moves one simulator DNS service between response
	// behaviours: answering, SERVFAIL, silence, or a bounded delay.
	FaultScheduledDNS = "scheduled_dns"
	// FaultScheduledLink administratively raises or lowers one node interface.
	FaultScheduledLink = "scheduled_link"
)

Scheduled fault types. Each one changes an already-built topology while netdoc is running, at offsets measured from a single simulation epoch.

View Source
const (
	// DNSOutcomeDrop sends nothing at all, so the client waits out its timeout.
	DNSOutcomeDrop = "drop"
	// DNSOutcomeDelay answers correctly, late.
	DNSOutcomeDelay = "delay"
)

Scheduled DNS outcomes. DNSOutcomeAnswer and DNSOutcomeSERVFAIL are shared with the per-query DNSFault schedule.

View Source
const (
	LinkStateUp   = "up"
	LinkStateDown = "down"
)

Link states a scheduled_link event may ask for.

View Source
const (
	EventApplied = "applied"
	// EventSkipped means the run ended, or was cancelled, before this event's
	// offset arrived. It was never applied.
	EventSkipped = "skipped"
	EventFailed  = "error"
)

Results of trying to apply one scheduled event.

View Source
const (
	TriageResultClean    = "clean"
	TriageResultFindings = "findings"
	TriageResultError    = "error"

	IssueStatusNotFiled = "not_filed"
	IssueStatusExisting = "existing"
	IssueStatusCreated  = "created"
)
View Source
const (
	// AuthoredIDVersion is the id version authored challenges resolve through.
	AuthoredIDVersion = "A1"
)
View Source
const (
	// ChallengeIDVersion is the version new ids are minted with. It is part of
	// the id itself, not a note about it: an id names the generation rules that
	// resolve it, so a future change to selection adds a version instead of
	// quietly repointing every id that has already been shared.
	ChallengeIDVersion = "V4"
)
View Source
const (
	// NodeCommand is the hidden argv[1] that makes the binary a node holder.
	NodeCommand = "__node"
)

Holder protocol. The director and the node holder exchange three lines over the holder's stdin/stdout: the holder announces its namespace is ready, the director answers once the namespace is addressed and routed, and the holder confirms its listeners are up. Nothing is reachable before that last line, so a probe can never race the topology. After services-ready the pipe stays open for one more exchange: the fault scheduler sends "dns <service> <outcome> <delay-ms>" and the holder answers dns-applied or dns-error. A scheduled DNS transition is therefore timed by the director's single epoch and confirmed before it is recorded as applied.

View Source
const TestNetdoc = "netdoc"

TestNetdoc is the only test type. Named so a scenario can be explicit, and so an unknown type is rejected rather than silently treated as this one.

Variables

View Source
var ChallengeAnswerMenu = []ChallengeAnswerInfo{
	{ID: AnswerHealthy, Label: "Nothing is wrong with this network",
		Help: "every layer works; the reported problem is elsewhere", Aliases: []string{"ok", "none", "nothing"}},
	{ID: AnswerDNSFailure, Label: "DNS resolution",
		Help: "names do not resolve, or the resolver refuses",

		Aliases: []string{"dns", "nxdomain", "dns_timeout", "servfail"}},
	{ID: AnswerNoDefaultRoute, Label: "No default route",
		Help:    "the routing table has no default at all, so nothing off-link can be reached",
		Aliases: []string{"no_route"}},
	{ID: AnswerWrongDefaultRoute, Label: "Wrong default route",
		Help:    "there is exactly one default route, its gateway answers, and it goes somewhere that cannot reach the internet",
		Aliases: []string{"bad_gateway", "wrong_gateway"}},
	{ID: AnswerMissingRoute, Label: "Missing route to the target's subnet",
		Help:    "the internet is fine; what is missing is the specific route the target's network needs",
		Aliases: []string{"missing_route"}},
	{ID: AnswerPreferredRoute, Label: "Failed preferred route",
		Help:    "two defaults exist and the lower-metric one is selected, but only the other one's path works",
		Aliases: []string{"preferred_route"}},
	{ID: AnswerIPv4Failure, Label: "IPv4 connectivity",
		Help: "the IPv4 path is down while IPv6 works", Aliases: []string{"ipv4"}},
	{ID: AnswerIPv6Failure, Label: "IPv6 connectivity",
		Help: "the IPv6 path is down while IPv4 works", Aliases: []string{"ipv6"}},
	{ID: AnswerPortBlocked, Label: "TCP port blocked",
		Help:    "connections to the target port are silently discarded, so they time out",
		Aliases: []string{"port_blocked", "blocked", "filtered"}},
	{ID: AnswerRefused, Label: "Connection refused",
		Help:    "the host answers the connection immediately with a refusal, because nothing is listening",
		Aliases: []string{"refused"}},
	{ID: AnswerReset, Label: "Connection reset by the service",
		Help: "the target accepts the connection and then tears it down", Aliases: []string{"reset"}},
	{ID: AnswerTLSCertificate, Label: "Expired TLS certificate",
		Help:    "the handshake fails because the certificate is outside its validity dates",
		Aliases: []string{"tls_expired", "expired_certificate"}},
	{ID: AnswerTLSHostname, Label: "TLS certificate name mismatch",
		Help:    "the certificate is valid and trusted, but not for the name that was requested",
		Aliases: []string{"tls_hostname", "hostname_mismatch"}},
	{ID: AnswerHTTPService, Label: "HTTP service error",
		Help: "the server answers, with an error status", Aliases: []string{"http_error"}},
	{ID: AnswerProxy, Label: "Proxy", Help: "the configured proxy is the thing that fails",
		Aliases: []string{"proxy"}},
	{ID: AnswerQUICBlocked, Label: "QUIC / UDP 443",
		Help: "UDP/443 is filtered while TCP/443 is not", Aliases: []string{"quic"}},
	{ID: AnswerPacketLoss, Label: "Packet loss or latency",
		Help: "the path works but drops or delays traffic", Aliases: []string{"loss", "packet_loss"}},
}

ChallengeAnswerMenu is ordered API. Every entry but the last three is a fault a challenge can be set on; those three stay listed because a menu of only the possible faults would be most of the answer, and each one is excluded for a reason written down next to challengeConditions.

The help text is the part a player actually reads, so where two answers sit next to each other it says what separates them rather than what they have in common: refused against blocked, no default route against a wrong one, expired against a name mismatch.

ChallengeDifficulties is ordered API.

View Source
var ErrUnsupported = errors.New("simulation backend unsupported on this platform")

ErrUnsupported is returned by a backend that cannot run on this host. It is a capability answer, not a failure: the caller prints Capabilities.Reason.

HuntSeverities is the hunt severity vocabulary, most severe first.

Functions

func AuthoredChallengeSlugs added in v1.11.3

func AuthoredChallengeSlugs() []string

AuthoredChallengeSlugs lists the slugs, for a usage or error message.

func ChallengeAnswerNames added in v1.11.3

func ChallengeAnswerNames() []string

ChallengeAnswerNames lists every accepted answer id, for a usage message. The ids rather than the labels: a message telling somebody what to pass to a flag has to name the things that flag accepts unquoted.

func DailyDate added in v1.11.3

func DailyDate(at time.Time) string

DailyDate renders an instant as the UTC calendar date a daily is keyed by. The conversion to UTC is the whole point: a player at 23:00 in Auckland and a player at 23:00 in Los Angeles are on different local dates, and a daily that followed the local one would not be the same challenge.

func DeriveHuntCaseSeed

func DeriveHuntCaseSeed(seed int64, base string, caseNumber int) int64

DeriveHuntCaseSeed makes case N independent of every earlier PRNG stream.

func DeriveIterationSeed

func DeriveIterationSeed(seed int64, scenario string, iteration int) int64

DeriveIterationSeed is independent of PRNG history, so iteration 37 can be reproduced without constructing iterations 0 through 36.

func HuntBaseNames

func HuntBaseNames() []string

HuntBaseNames returns the deliberately small set of known-good controls the first generator is allowed to mutate.

func HuntGeneratorVersions added in v1.12.3

func HuntGeneratorVersions() []string

HuntGeneratorVersions returns every generator version this build can replay.

func HuntLaneNames added in v1.12.3

func HuntLaneNames() []string

HuntLaneNames returns the CLI vocabulary. The all-operator lane exists only for exact replay of generators published before the lane split.

func LaunchDirector

func LaunchDirector(ctx context.Context, self string, argv []string, stdin io.Reader, stdout, stderr io.Writer) (int, error)

LaunchDirector re-executes this binary with argv inside a fresh user, network and mount namespace, and returns its exit code. The child is where the backend actually runs; the parent keeps no privileges and no namespaces.

stdin is nil for every automated command, since a simulation reads nothing from the terminal, and is the caller's terminal only for Challenge Mode, where a person is the one being asked.

func LibraryNames

func LibraryNames() []string

LibraryNames lists the built-in scenarios, by file stem.

func NewID

func NewID() string

NewID returns a unique id for one simulation. Every namespace, interface and state file derives from it, so two concurrent runs, or a run started while an abandoned one is still around, cannot collide.

func NormalizeChallengeID added in v1.11.3

func NormalizeChallengeID(raw string) (string, error)

NormalizeChallengeID accepts the id in the form a person would type or paste it and returns the canonical one, `V1-8F42C1`. It is deliberately strict: an id is the whole reproduction contract, so a near miss has to be a rejection rather than a different challenge.

A bare `8F42C1` means V1 and always will. Bare was the only form the first release published, so re-pointing it at whatever version is current would be exactly the silent drift the version prefix exists to prevent.

func ParseDailyDate added in v1.11.3

func ParseDailyDate(raw string) (string, error)

ParseDailyDate accepts a date a person typed and returns the canonical rendering of it. Strict on purpose: a date is half of the reproduction contract for a daily, so a near miss has to be a rejection rather than a different day.

func RandomChallengeID added in v1.11.3

func RandomChallengeID() (string, error)

RandomChallengeID draws a fresh id, at the current version.

func RandomSeed

func RandomSeed() (int64, error)

RandomSeed chooses only the campaign's visible root seed. All behavior after this call is derived deterministically from it.

func RunNode

func RunNode(ctx context.Context, cfgPath string, stdin io.Reader, stdout, stderr io.Writer) (err error)

RunNode is the node holder: the process that owns one simulated machine's network and mount namespaces. It is spawned by the director as `netdoc-sim __node <config.json>` with those namespaces already created by clone(2), so all it has to do is furnish them and stay alive.

It never touches the network itself. The director does the wiring from outside via nsenter, which keeps the holder small enough to read in one go.

func SeverityAtLeast added in v1.10.8

func SeverityAtLeast(severity, floor HuntSeverity) bool

SeverityAtLeast reports whether a finding meets the severity floor. The hunt's low and info findings are mostly known coverage limits of netdoc's probes rather than defects, so a nightly job files from medium up.

func StarterPackNames added in v1.11.3

func StarterPackNames() []string

StarterPackNames lists the pack ids, for a usage or error message.

func StateDir

func StateDir() string

StateDir is where kept-simulation records live. The per-user runtime directory is the right home for them: it is private, and it is emptied on logout, which matches how long a simulation can possibly survive.

func ValidateMergedHuntResult added in v1.12.3

func ValidateMergedHuntResult(result *HuntResult) error

ValidateMergedHuntResult validates a canonical, unsharded result before a downstream consumer trusts its findings or reproduction coordinates.

func WriteAnswerMenu added in v1.11.3

func WriteAnswerMenu(w io.Writer)

WriteAnswerMenu prints the structured diagnosis vocabulary. It lists more faults than a challenge can inject on purpose: a menu of only the possible answers would be most of the answer.

Types

type AuthoredChallenge added in v1.11.3

type AuthoredChallenge struct {
	ID      string `json:"id"`
	Slug    string `json:"slug"`
	Name    string `json:"name"`
	Teaches string `json:"teaches,omitempty"`
}

AuthoredChallenge is one authored case as it is published.

func AuthoredChallengeBySlug added in v1.11.3

func AuthoredChallengeBySlug(raw string) (AuthoredChallenge, bool)

AuthoredChallengeBySlug resolves a case somebody named. Slugs are what a contributor edits and what `netdoc-sim authored` prints, so they are accepted alongside the id everywhere a challenge is chosen.

func AuthoredChallenges added in v1.11.3

func AuthoredChallenges() []AuthoredChallenge

AuthoredChallenges lists the authored cases, in the order they are offered.

type Backend

type Backend interface {
	// Capabilities reports whether this host can run a simulation, and which
	// privileged operations a run performs. Cheap and side-effect free.
	Capabilities(ctx context.Context) Capabilities
	// Prepare builds the topology. It returns a usable Env, or an error plus,
	// when setup got far enough to create anything, a non-nil Env that must
	// still be cleaned up. Callers must handle both being non-nil.
	Prepare(ctx context.Context, s *Scenario, id string) (Env, error)
}

Backend builds virtual networks. The only implementation today is Linux network namespaces; the interface exists so a container, libvirt or platform-native backend can be added without the runner noticing.

func DefaultBackend

func DefaultBackend(dry bool, log io.Writer) Backend

DefaultBackend returns the backend for this platform. With dry set, Prepare prints the commands it would run to log instead of running them, and creates nothing.

type CampaignDNS

type CampaignDNS struct {
	Service        string      `yaml:"service"`
	QueriesPerType int         `yaml:"queries_per_type"`
	FailurePercent NumberRange `yaml:"failure_percent"`
}

type CampaignDNSDelay

type CampaignDNSDelay struct {
	Service string        `yaml:"service"`
	Delay   DurationRange `yaml:"delay"`
}

type CampaignNetem

type CampaignNetem struct {
	Node        string        `yaml:"node"`
	Segment     string        `yaml:"segment"`
	Latency     DurationRange `yaml:"latency"`
	Jitter      DurationRange `yaml:"jitter"`
	LossPercent NumberRange   `yaml:"loss_percent"`
}

type CampaignOptions

type CampaignOptions struct {
	Run       Options
	Runs      int
	Seed      int64
	Iteration *int
	FailFast  bool
}

CampaignOptions controls a sequential campaign. Iteration, when non-nil, runs exactly that independently derived iteration even when it exceeds Runs.

type CampaignResult

type CampaignResult struct {
	Scenario       string             `json:"scenario"`
	Seed           int64              `json:"seed"`
	Runs           int                `json:"runs"`
	Passed         int                `json:"passed"`
	Failed         int                `json:"failed"`
	Errors         int                `json:"errors"`
	FalsePositives int                `json:"false_positives"`
	FalseNegatives int                `json:"false_negatives"`
	Timeouts       int                `json:"timeouts"`
	StableRuns     int                `json:"stable_runs"`
	DivergentRuns  int                `json:"divergent_runs"`
	FailurePercent float64            `json:"failure_percent"`
	MinDuration    time.Duration      `json:"min_duration_ms"`
	MaxDuration    time.Duration      `json:"max_duration_ms"`
	MedianDuration time.Duration      `json:"median_duration_ms"`
	FirstFailure   *Reproduction      `json:"first_failure,omitempty"`
	Fingerprints   []FingerprintCount `json:"fingerprints"`
	Outcomes       []IterationResult  `json:"outcomes"`
	Cancelled      bool               `json:"cancelled"`
	Result         string             `json:"result"`
	Error          string             `json:"error,omitempty"`
	Suggestions    []Suggestion       `json:"suggestions"`
}

func RunCampaign

func RunCampaign(ctx context.Context, scenario *Scenario, backend func() Backend, opts CampaignOptions) *CampaignResult

RunCampaign executes iterations sequentially and delegates each one to Run, retaining its normal report and cleanup result.

func (*CampaignResult) WriteJSON

func (r *CampaignResult) WriteJSON(w io.Writer) error

func (*CampaignResult) WriteText

func (r *CampaignResult) WriteText(w io.Writer)

type CampaignSpec

type CampaignSpec struct {
	Runs  int            `yaml:"runs"`
	Netem *CampaignNetem `yaml:"netem"`
	DNS   *CampaignDNS   `yaml:"dns"`
	// Timeline generates a bounded flapping fault timeline per iteration.
	Timeline *CampaignTimeline `yaml:"timeline"`
	// DNSDelay sweeps one resolver delay, which is how a campaign walks a probe
	// timeout boundary without varying anything else.
	DNSDelay *CampaignDNSDelay `yaml:"dns_delay"`
}

CampaignSpec declares bounded ranges only. Compilation resolves every range before a node or goroutine starts; scenario files cannot contain expressions or executable material.

type CampaignTimeline

type CampaignTimeline struct {
	Node    string `yaml:"node"`
	Segment string `yaml:"segment"`
	// Service, when named, loses its DNS responses for the outage window too.
	Service string `yaml:"service"`
	// ResolverHold is the delay that service opens with. It paces the run so
	// the generated phases actually overlap a probe: netdoc issues all of its
	// resolver queries in the first few milliseconds of a run, so without
	// something holding it there, a timeline measured in hundreds of
	// milliseconds would be changing a network nobody was looking at.
	ResolverHold string `yaml:"resolver_hold"`
	// Latency is the fixed healthy latency every phase carries.
	Latency     string        `yaml:"latency"`
	DegradeAt   DurationRange `yaml:"degrade_at"`
	DegradeLoss NumberRange   `yaml:"degrade_loss_percent"`
	OutageFor   DurationRange `yaml:"outage_for"`
}

CampaignTimeline generates one flapping timeline per iteration. The shape is fixed and only three dimensions vary (when degradation starts, how bad it is, and how long the total outage lasts) so a failing iteration is still something a person can read.

+0                          healthy
+degrade_at                 degraded (loss = degrade_loss_percent)
+degrade_at+400ms           healthy
+degrade_at+800ms           outage (100% loss, and the resolver silent)
+degrade_at+800ms+outage_for healthy again

type Capabilities

type Capabilities struct {
	Backend   string `json:"backend"`
	Supported bool   `json:"supported"`
	// Reason explains an unsupported host in terms the user can act on.
	Reason string `json:"reason,omitempty"`
	// Missing lists required executables that were not found in PATH.
	Missing []string `json:"missing,omitempty"`
	// Privileged lists the operations a run performs, so a user can read the
	// blast radius before granting anything.
	Privileged []string `json:"privileged,omitempty"`
}

Capabilities answers "can this host simulate, and what will it cost me".

type Challenge added in v1.11.3

type Challenge struct {
	ID         string `json:"id"`
	Difficulty string `json:"difficulty"`
	// Node is the node the player investigates from, and Target is what they
	// were asked about. Both come from the scenario's own primary test, so
	// neither is a hint the netdoc run does not also get.
	Node   string `json:"node"`
	Target string `json:"target,omitempty"`
	// Daily is the UTC calendar date this challenge is the daily for, set only
	// when it was selected that way. It is a label on how the player arrived,
	// never an input to generation: the id alone decides the puzzle, and the same
	// id played without -daily is the same network.
	Daily string `json:"daily,omitempty"`

	Base     string                `json:"base_scenario"`
	Seed     int64                 `json:"seed"`
	Case     int                   `json:"case"`
	Manifest GeneratedCaseManifest `json:"manifest"`

	Scenario *Scenario `json:"-"`
	// contains filtered or unexported fields
}

Challenge is one reproducible puzzle. Everything the player may see before they answer is above the line; Base, Seed, Case and Manifest name the case and are therefore the answer, so nothing prints them before the reveal.

func AuthoredChallengeByID added in v1.11.3

func AuthoredChallengeByID(slug string) (*Challenge, error)

AuthoredChallengeByID builds the authored case a slug names. It resolves the slug to that case's ordinary id and goes through BuildChallenge, so choosing by slug and replaying by id cannot produce different networks.

func BuildChallenge added in v1.11.3

func BuildChallenge(raw string) (*Challenge, error)

BuildChallenge resolves an id into the case behind it. It is pure and deterministic: the same id builds the same challenge on any machine whose build knows that id's version, with no state on disk and no network.

func DailyChallenge added in v1.11.3

func DailyChallenge(date string) (*Challenge, error)

DailyChallenge resolves one UTC date to the challenge everybody asking for that date gets. It is pure: no clock is read, no state is consulted, and the returned challenge carries an ordinary id that reproduces it forever.

func FindChallenge added in v1.11.3

func FindChallenge(difficulty string) (*Challenge, error)

FindChallenge draws a fresh challenge, optionally of a requested difficulty. Difficulty is a property of the case an id resolves to, so honouring a request means looking at ids until one carries it.

func StarterChallenge added in v1.11.3

func StarterChallenge(pack string) (*Challenge, error)

StarterChallenge draws one challenge from a pack. The draw is random because the command has no memory of which ones you have played, and inventing a progress file for a game whose whole appeal is that it leaves nothing behind would be the wrong trade. Anyone who wants to work through a pack in order has the ids: `netdoc-sim starters <pack>` prints them, and each plays with -id.

func (*Challenge) Replay added in v1.11.3

func (c *Challenge) Replay() string

Replay is the command that reproduces this exact challenge.

func (*Challenge) WriteBriefing added in v1.11.3

func (c *Challenge) WriteBriefing(w io.Writer)

WriteBriefing prints what the player is told before they start, and prints it again whenever they ask for it mid-session. It is the only briefing renderer there is, deliberately: a separate "recall" format would be a second thing to keep truthful, and the one that drifted would be the one nobody reread.

Everything here is either the id, the difficulty, or something the netdoc run is also given: the node it stands in and the target it is asked about. The base scenario, the seed, the case and the mutation stay out of it.

type ChallengeAnswer added in v1.11.3

type ChallengeAnswer string

ChallengeAnswer is one entry in the structured diagnosis vocabulary both contestants are graded against. Scoring never reads free text.

const (
	AnswerHealthy           ChallengeAnswer = "healthy"
	AnswerDNSFailure        ChallengeAnswer = "dns_failure"
	AnswerNoDefaultRoute    ChallengeAnswer = "no_default_route"
	AnswerWrongDefaultRoute ChallengeAnswer = "wrong_default_route"
	AnswerMissingRoute      ChallengeAnswer = "missing_subnet_route"
	AnswerPreferredRoute    ChallengeAnswer = "preferred_route_failure"
	AnswerIPv4Failure       ChallengeAnswer = "ipv4_failure"
	AnswerIPv6Failure       ChallengeAnswer = "ipv6_failure"
	AnswerPortBlocked       ChallengeAnswer = "tcp_port_blocked"
	AnswerRefused           ChallengeAnswer = "connection_refused"
	AnswerReset             ChallengeAnswer = "connection_reset"
	AnswerTLSCertificate    ChallengeAnswer = "tls_certificate"
	AnswerTLSHostname       ChallengeAnswer = "tls_hostname_mismatch"
	AnswerHTTPService       ChallengeAnswer = "http_service"
	AnswerProxy             ChallengeAnswer = "proxy_failure"
	AnswerQUICBlocked       ChallengeAnswer = "quic_udp_blocked"
	AnswerPacketLoss        ChallengeAnswer = "packet_loss"
)

type ChallengeAnswerInfo added in v1.11.3

type ChallengeAnswerInfo struct {
	ID    ChallengeAnswer `json:"id"`
	Label string          `json:"label"`
	Help  string          `json:"help,omitempty"`
	// Aliases are the extra spellings ChallengeAnswerByName accepts. Each one is
	// a deliberate choice, not a fuzzy match: the shorthand a person types at a
	// terminal, and the term they would have used if this taxonomy had split the
	// condition more finely than it does. They are matched whole, never as a
	// prefix or a substring, because an answer selected by resemblance is a
	// diagnosis nobody committed to.
	Aliases []string `json:"aliases,omitempty"`
}

ChallengeAnswerInfo is one menu entry: the internal identity, the name a person reads, and the words a person is allowed to type for it.

ID and Label are deliberately separate. ID is what a saved result, a recognizer table and a script are keyed by, so it may never change; Label is prose aimed at whoever is reading the menu, so it may be reworded whenever it reads badly. Using the label as the key would make every wording improvement a breaking change to the JSON.

func ChallengeAnswerByID added in v1.11.3

func ChallengeAnswerByID(raw string) (ChallengeAnswerInfo, bool)

ChallengeAnswerByID resolves an answer by its internal identity, and only that. It is the lookup for a stored answer (a result being reread, a recognizer being consulted) where accepting a display name or a shorthand would let one answer arrive under two spellings.

func ChallengeAnswerByName added in v1.11.3

func ChallengeAnswerByName(raw string) (ChallengeAnswerInfo, bool)

ChallengeAnswerByName resolves what a person typed: the id, one of the deliberate aliases, or the display name itself. Every comparison is on the whole string after case and separator normalization, so `dns` selects the DNS answer and `dns thing` selects nothing. There is no prefix or substring matching on purpose: a diagnosis chosen by resemblance is one the player never made, and silently scoring it would be worse than asking again.

type ChallengeContestant added in v1.11.3

type ChallengeContestant struct {
	Answer ChallengeAnswer `json:"answer,omitempty"`
	Label  string          `json:"label,omitempty"`
	Score  string          `json:"score"`
	Detail string          `json:"detail,omitempty"`
	Note   string          `json:"note,omitempty"`
}

ChallengeContestant is one graded answer.

type ChallengeOptions added in v1.11.3

type ChallengeOptions struct {
	Run Options
	// NetdocVersion is what Run.Netdoc answered for -version, asked of that same
	// executable by whoever resolved it. Only the version travels: the path in
	// the result is read straight back off Run.Netdoc, so the binary a result
	// names cannot drift from the binary the run launched.
	NetdocVersion string
	// Play is handed the live network once every fault is in place and before
	// any netdoc process starts, and returns the diagnosis the person committed
	// to. A nil Play submits nothing, which is how a non-interactive answer runs.
	Play func(context.Context, *ChallengeSession) (ChallengeSubmission, error)
}

ChallengeOptions configures one challenge run.

type ChallengeResult added in v1.11.3

type ChallengeResult struct {
	ChallengeID string `json:"challenge_id"`
	Difficulty  string `json:"difficulty"`
	// Daily is the UTC date this was played as the daily for, and is what makes
	// two people's results comparable as the same day's puzzle. Like netdoc and
	// timing, it records the session rather than the challenge: a replay by id
	// reproduces the network and not the way somebody arrived at it.
	Daily            string              `json:"daily,omitempty"`
	IDVersion        string              `json:"id_version"`
	GeneratorVersion string              `json:"generator_version"`
	BaseScenario     string              `json:"base_scenario"`
	Seed             int64               `json:"seed"`
	Case             int                 `json:"case"`
	CaseFingerprint  string              `json:"case_fingerprint"`
	Node             string              `json:"node"`
	Target           string              `json:"target,omitempty"`
	Netdoc           NetdocIdentity      `json:"netdoc"`
	Truth            ChallengeTruth      `json:"truth"`
	Human            ChallengeContestant `json:"human"`
	NetworkDoctor    ChallengeContestant `json:"network_doctor"`
	Result           string              `json:"result"`
	Timing           ChallengeTiming     `json:"timing"`
	Replay           string              `json:"replay"`
	// Error is the simulator falling over, which is not a matchup.
	Error string `json:"error,omitempty"`
}

ChallengeResult is the whole matchup, and the machine-readable artifact.

func RunChallenge added in v1.11.3

func RunChallenge(ctx context.Context, c *Challenge, backend Backend, opts ChallengeOptions) (*ChallengeResult, error)

RunChallenge builds the challenge network, hands it to the player, runs the real netdoc in it, and scores both answers against the simulator's own observations.

netdoc runs through the ordinary simulation path with the ordinary arguments: it is never told a challenge is happening, never handed a mutation manifest or oracle evidence, and its probes are not selected from the hidden answer. A non-nil error means no matchup took place.

func ScoreChallenge added in v1.11.3

func ScoreChallenge(c *Challenge, report *Report, submission ChallengeSubmission) *ChallengeResult

ScoreChallenge grades both contestants against one truth.

The order here is the invariant: truth is established first, from simulator evidence and the manifest alone, and only then is either answer looked at. Neither contestant appears in challengeTruth's inputs, and neither grading function can reach the other's answer.

func (*ChallengeResult) Share added in v1.11.3

func (r *ChallengeResult) Share() string

Share is the copyable block. The same completed result renders the same bytes, every time: it is the artifact people compare, so it cannot depend on where or when it was rendered.

func (*ChallengeResult) WriteJSON added in v1.11.3

func (r *ChallengeResult) WriteJSON(w io.Writer) error

func (*ChallengeResult) WriteText added in v1.11.3

func (r *ChallengeResult) WriteText(w io.Writer)

WriteText is the reveal. It runs only after both answers are in.

type ChallengeSession added in v1.11.3

type ChallengeSession struct {
	Challenge *Challenge
	// contains filtered or unexported fields
}

ChallengeSession is the live network, while the player has it. It exposes the two things an investigation needs, where to stand and how to get a shell there, and nothing that would answer the question for them.

func (*ChallengeSession) Shell added in v1.11.3

func (s *ChallengeSession) Shell(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error

Shell runs an interactive shell inside the challenge node's network and mount namespaces, with the caller's terminal attached. It is the same nsenter the simulator already uses to run netdoc there, so the player sees exactly the network the diagnosis will be made against: the node's own routes, its own /etc/resolv.conf, and the simulator's trust anchors where the netdoc run gets them too.

The shell is not given a new process group: an interactive shell claims the terminal for itself, which is what keeps Ctrl-C inside the challenge from reaching the simulator holding the network open.

func (*ChallengeSession) ShellAvailable added in v1.11.3

func (s *ChallengeSession) ShellAvailable() bool

ShellAvailable reports whether this backend can open a shell in the node.

type ChallengeSubmission added in v1.11.3

type ChallengeSubmission struct {
	Answer  ChallengeAnswer
	GaveUp  bool
	Note    string
	Elapsed time.Duration
}

ChallengeSubmission is what the person committed to, before any netdoc process ran.

type ChallengeTiming added in v1.11.3

type ChallengeTiming struct {
	HumanMS  int64 `json:"human_ms"`
	NetdocMS int64 `json:"network_doctor_ms"`
}

ChallengeTiming is every field of a result that a replay will not reproduce. It is one object rather than two loose fields so a consumer diffing two runs of the same id knows exactly what it has to ignore: everything else in a ChallengeResult is determined by the id and the network.

type ChallengeTruth added in v1.11.3

type ChallengeTruth struct {
	Answer      ChallengeAnswer `json:"answer,omitempty"`
	Label       string          `json:"label,omitempty"`
	Scoreable   bool            `json:"scoreable"`
	Reason      string          `json:"reason,omitempty"`
	Explanation string          `json:"explanation,omitempty"`
	// Injected is what the generator asked for. It is intent rather than truth,
	// is never what a score is computed from, and is printed only after both
	// answers are in.
	Injected       string   `json:"injected,omitempty"`
	ObservedFaults []string `json:"observed_faults"`
	Evidence       []string `json:"evidence"`
}

ChallengeTruth is what the simulator independently established. It is derived from evidence and the mutation manifest only, never from a diagnosis, never from the player's answer, and never from a mutation having merely been scheduled.

type CheckComparison

type CheckComparison struct {
	ID       string `json:"id"`
	Name     string `json:"name,omitempty"`
	Expected string `json:"expected,omitempty"`
	// ExpectedCause is optional so existing status-only scenarios retain their
	// comparison contract.
	ExpectedCause string `json:"expected_cause,omitempty"`
	ExpectedFix   string `json:"expected_fix,omitempty"`
	Actual        string `json:"actual,omitempty"`
	Cause         string `json:"cause,omitempty"`
	ExpectedIPv4  string `json:"expected_ipv4,omitempty"`
	ExpectedIPv6  string `json:"expected_ipv6,omitempty"`
	ActualIPv4    string `json:"actual_ipv4,omitempty"`
	ActualIPv6    string `json:"actual_ipv6,omitempty"`
	Outcome       string `json:"outcome"`
	Detail        string `json:"detail,omitempty"`
	Fix           string `json:"fix,omitempty"`
	Ms            int64  `json:"ms,omitempty"`
}

CheckComparison is one expected-versus-actual pairing.

type CleanupInfo

type CleanupInfo struct {
	Done bool `json:"done"`
	Kept bool `json:"kept"`
	// Workspace is the scratch directory the run used, set while it still
	// exists so a kept simulation can be found and swept later.
	Workspace string   `json:"workspace,omitempty"`
	Detail    string   `json:"detail,omitempty"`
	Errors    []string `json:"errors,omitempty"`
}

CleanupInfo records that the resources went away, and never hides a failure to make them go away.

type ControlledTargetEvidence added in v1.11.3

type ControlledTargetEvidence struct {
	From      string   `json:"from"`
	To        string   `json:"to"`
	Family    string   `json:"family"`
	Via       []string `json:"via"`
	Reachable bool     `json:"reachable"`
	// Outcome is what the dial did, not merely whether it worked. A port that
	// answered a SYN with a reset and a port that swallowed it are both
	// unreachable and are different faults with different fixes, and the dialing
	// end is the only place that difference is visible. Reachable is the same
	// observation narrowed to a bool, kept because most readers only want that.
	Outcome string `json:"outcome"`
}

ControlledTargetEvidence is the simulator's own TCP dial of one literal address and port that a simulator fixture serves, taken from inside a node's namespace. To is always an address:port the scenario owns, which is what keeps a diagnosis out: netdoc's target may be a hostname or anything on the public internet, and its target_tcp verdict is a claim about a run, not a dial the simulator performed. The single producer is the node holder, which never sees netdoc's report.

type DNSEvidence

type DNSEvidence struct {
	Node      string `json:"node"`
	Service   string `json:"service,omitempty"`
	Source    string `json:"source"`
	Name      string `json:"name"`
	QueryType string `json:"query_type"`
	Result    string `json:"result"`
	Count     int    `json:"count"`
}

DNSEvidence aggregates identical queries observed by a DNS service.

type DNSFault

type DNSFault struct {
	A         []string `yaml:"a"`
	AAAA      []string `yaml:"aaaa"`
	WrongA    string   `yaml:"wrong_a"`
	WrongAAAA string   `yaml:"wrong_aaaa"`
}

DNSFault carries one deterministic response sequence per DNS query family. Every queried name walks that sequence independently, so a query for one name cannot advance another name's schedule. When a sequence is exhausted, the service answers normally. WrongA and WrongAAAA are required only when their family schedule contains wrong_answer.

type DNSQueryEvidence

type DNSQueryEvidence struct {
	Node             string `json:"node"`
	Service          string `json:"service"`
	Source           string `json:"source"`
	Name             string `json:"name"`
	QueryType        string `json:"query_type"`
	Sequence         int    `json:"sequence"`
	ScheduledOutcome string `json:"scheduled_outcome"`
	ActualOutcome    string `json:"actual_outcome"`
	// Offset places the query on the fault timeline, relative to T0. It is
	// filled in by the director once the run's epoch is known, and only means
	// anything when OffsetKnown is set.
	Offset time.Duration `json:"offset_ms"`
	// OffsetKnown reports whether the holder's observation carried a wall clock
	// the director could place on the timeline. Without it Offset is zero
	// because there is nothing to put there, which is a different claim from a
	// query observed exactly at T0, so the two are not left to share a value.
	OffsetKnown bool  `json:"offset_known"`
	DelayMs     int64 `json:"delay_ms,omitempty"`
	// contains filtered or unexported fields
}

DNSQueryEvidence preserves scheduled query order rather than aggregating it. Sequence is scoped to service, queried name, and query type.

type DNSRecord

type DNSRecord struct {
	Name    string `yaml:"name"`
	Address string `yaml:"address"`
}

DNSRecord is one static A or AAAA answer. Type is derived from Address so a scenario cannot claim an A record while supplying IPv6 bytes, or vice versa.

type Diagnosis

type Diagnosis struct {
	Checks []DiagnosisCheck `json:"checks"`
	// Findings are netdoc's own structured conclusions about the network it
	// looked at, carried through verbatim so a captured diagnosis is complete
	// and so an oracle rule can eventually recognize a condition by netdoc's
	// stable identity instead of by its prose. Deliberately not the same thing
	// as a HuntCaseFinding, which is a defect found in netdoc itself.
	Findings    []DiagnosisFinding `json:"findings,omitempty"`
	Summary     string             `json:"summary"`
	Verdict     string             `json:"verdict"`
	FailedStage string             `json:"failed_stage"`
	OK          bool               `json:"ok"`
}

Diagnosis is the simulator's narrow view of a netdoc report. It is also embedded in simulator JSON output, so decodeDiagnosis projects the canonical report contract into this type instead of exposing unrelated report fields.

type DiagnosisAttempt

type DiagnosisAttempt struct {
	IP    string `json:"ip"`
	Ms    int64  `json:"ms"`
	Error string `json:"error,omitempty"`
}

type DiagnosisCheck

type DiagnosisCheck struct {
	ID       string             `json:"id"`
	Name     string             `json:"name"`
	Status   string             `json:"status"`
	Cause    string             `json:"cause,omitempty"`
	Ms       int64              `json:"ms"`
	Detail   string             `json:"detail"`
	Fix      string             `json:"fix"`
	Families *DiagnosisFamilies `json:"address_families,omitempty"`
	Attempts []DiagnosisAttempt `json:"attempts,omitempty"`
}

DiagnosisCheck is one probe row.

type DiagnosisFamilies

type DiagnosisFamilies struct {
	IPv4 string `json:"ipv4,omitempty"`
	IPv6 string `json:"ipv6,omitempty"`
}

DiagnosisFamilies carries netdoc's per-family egress verdicts. A family is present only when netdoc actually dialed it, so a key netdoc omitted for a family the selected source has no address for must stay omitted when the simulator re-encodes this into its own report. Serializing the empty string would invent a verdict for a family nobody tested.

type DiagnosisFinding added in v1.13.0

type DiagnosisFinding struct {
	ID       string   `json:"id"`
	Focus    string   `json:"focus,omitempty"`
	Evidence []string `json:"evidence,omitempty"`
}

DiagnosisFinding is one conclusion from netdoc's report: the stable id, the check row it blames, and the rows it rests on.

type DiagnosisFingerprint

type DiagnosisFingerprint struct {
	ID       string             `json:"id"`
	Verdicts []string           `json:"verdicts"`
	Probes   []ProbeFingerprint `json:"probes"`
}

type DurationRange

type DurationRange struct {
	Min string `yaml:"min"`
	Max string `yaml:"max"`
}

type Env

type Env interface {
	// Nodes describes what was actually created.
	Nodes() []NodeInfo
	// ApplyFaults injects impairments into an already-healthy topology.
	ApplyFaults(ctx context.Context, faults []Fault) ([]FaultInfo, error)
	// ApplyTimedEvent applies one already-validated scheduled change while the
	// tests are running, and returns what the kernel says the new state is.
	// Only the fault scheduler calls it, and only before Cleanup begins, since the
	// runner joins the scheduler first.
	ApplyTimedEvent(ctx context.Context, event TimedEvent) (string, error)
	// Exec runs argv inside a node's namespaces. argv is passed to the kernel
	// as a slice; no shell is involved anywhere in this package.
	Exec(ctx context.Context, node string, argv, env []string) ExecResult
	// TrustAnchor returns the simulator-generated public CA bundle for a
	// validated TLS service. Scenario files never supply this path.
	TrustAnchor(service string) (string, error)
	// Evidence reads structured service observations collected so far.
	Evidence(ctx context.Context) (Evidence, error)
	// Cleanup releases everything. It is idempotent, safe after a partial
	// Prepare, and reports errors rather than swallowing them. With keep set it
	// leaves the environment running and says how to reach it.
	Cleanup(ctx context.Context, keep bool) CleanupInfo
}

Env is one live simulated network.

type Evidence

type Evidence struct {
	DNS              []DNSEvidence             `json:"dns"`
	DNSQueries       []DNSQueryEvidence        `json:"dns_queries"`
	SOCKSRequests    []SOCKSEvidence           `json:"socks_requests"`
	TLS              []TLSEvidence             `json:"tls"`
	ServiceStates    []ServiceStateEvidence    `json:"service_states"`
	ServiceReplies   []ServiceReplyEvidence    `json:"service_replies"`
	TCPResets        []TCPResetEvidence        `json:"tcp_resets"`
	PacketConditions []PacketConditionEvidence `json:"packet_conditions"`
	PacketDrops      []PacketDropEvidence      `json:"packet_drops"`
	Links            []LinkEvidence            `json:"links"`
	Routes           []RouteEvidence           `json:"routes"`
	RouteTables      []RouteTableEvidence      `json:"route_tables"`
	Routers          []RouterEvidence          `json:"routers"`
	// ControlledTargets and FamilyReachability are both measured, never
	// derived. See their type comments.
	ControlledTargets  []ControlledTargetEvidence   `json:"controlled_targets"`
	FamilyReachability []FamilyReachabilityEvidence `json:"family_reachability"`
}

Evidence is simulator-owned proof collected from services inside node namespaces. It complements netdoc's report; it is never used to manufacture a diagnostic result.

type ExecResult

type ExecResult struct {
	Stdout    []byte
	Stderr    []byte
	ExitCode  int
	Duration  time.Duration
	TimedOut  bool
	Cancelled bool
	Signal    string
	Err       error
}

ExecResult is the outcome of one command run inside a node.

type Expect

type Expect struct {
	Verdict string          `yaml:"verdict"`
	Summary string          `yaml:"summary"`
	Checks  []ExpectedCheck `yaml:"checks"`
}

Expect is the diagnosis the scenario claims netdoc should reach. Verdict and checks match netdoc's stable machine-readable contract; Summary optionally pins the user-facing diagnosis.

type ExpectedCheck

type ExpectedCheck struct {
	ID     string `yaml:"id"`
	Status string `yaml:"status"`
	Cause  string `yaml:"cause"`
	Fix    string `yaml:"fix"`
	IPv4   string `yaml:"ipv4"`
	IPv6   string `yaml:"ipv6"`
}

ExpectedCheck names one probe row and the result it should carry. Fix optionally pins the user-facing remedy.

type FamilyReachabilityEvidence added in v1.11.3

type FamilyReachabilityEvidence struct {
	Node   string   `json:"node"`
	Family string   `json:"family"`
	Target string   `json:"target,omitempty"`
	Via    []string `json:"via,omitempty"`
	State  string   `json:"state"`
}

FamilyReachabilityEvidence is the simulator's own point-in-time answer to one question: from inside this node's namespace, does a TCP connection to the controlled endpoints of this address family complete?

It is a state rather than a bool because there are three outcomes, and it is its own type rather than a ControlledTargetEvidence so that the only way to fill it in is to dial. The single producer is the node holder, which never sees netdoc's report; anything derived from a diagnosis, a scenario expectation or a fault record belongs somewhere else. Absence of a record for a family is not "unavailable"; it means no observation was taken at all.

type Fault

type Fault struct {
	Type string `yaml:"type"`
	Node string `yaml:"node"`
	// Segment identifies an interface by logical topology name. Via and Metric
	// are used only by replace_default_route.
	Segment string `yaml:"segment"`
	Via     string `yaml:"via"`
	Metric  int    `yaml:"metric"`
	// Family restricts route faults to ipv4 or ipv6. Empty preserves the
	// original IPv4 behavior of existing scenarios.
	Family string `yaml:"family"`
	// To, Protocol and Port select the traffic FaultDrop discards. An empty To
	// matches every destination; a zero Port matches every port.
	To       string `yaml:"to"`
	Protocol string `yaml:"protocol"`
	Port     int    `yaml:"port"`
	// Direction chooses where FaultDrop bites, and the two are not
	// interchangeable. Outbound drops the packet on the way out of this node,
	// which the kernel reports to the sender as a refusal, the way a local firewall behaves.
	// Inbound drops it as it arrives at this node, so the sender hears nothing
	// and waits out its timeout, the way a black hole in the path behaves. Default outbound.
	Direction string `yaml:"direction"`
	// Delay, Jitter and Loss configure FaultNetem. Delay and Jitter are Go
	// durations; Loss is a percentage such as "10%".
	Delay  string `yaml:"delay"`
	Jitter string `yaml:"jitter"`
	Loss   string `yaml:"loss"`
	// Seed makes tc netem's pseudo-random sequence reproducible. Zero asks tc
	// for its default; campaign compilation always supplies a non-zero seed.
	Seed uint32 `yaml:"seed,omitempty"`
	// MTU is the size FaultPMTUBlackhole narrows the named interface to. The
	// endpoints are left alone, so they keep offering full-size packets to a
	// hop that can no longer carry them.
	MTU int `yaml:"mtu,omitempty"`
	// Service names the simulator DNS service a scheduled_dns fault drives. The
	// node is derived from it; a scenario never names one for this fault type.
	Service string `yaml:"service,omitempty"`
	// Events is the timed transition list of a scheduled_* fault. It is fully
	// resolved before the topology exists and immutable once T0 passes.
	Events []ScheduledEvent `yaml:"events,omitempty"`
}

Fault is one impairment applied after the topology is up and healthy.

type FaultEvent

type FaultEvent struct {
	Offset          time.Duration `json:"offset_ms"`
	Type            string        `json:"type"`
	Node            string        `json:"node,omitempty"`
	Segment         string        `json:"segment,omitempty"`
	Family          string        `json:"family,omitempty"`
	Latency         time.Duration `json:"latency_ms,omitempty"`
	Jitter          time.Duration `json:"jitter_ms,omitempty"`
	LossPercent     float64       `json:"loss_percent,omitempty"`
	NetemSeed       uint32        `json:"netem_seed,omitempty"`
	Service         string        `json:"service,omitempty"`
	QueryType       string        `json:"query_type,omitempty"`
	Sequence        int           `json:"sequence,omitempty"`
	ScheduledResult string        `json:"scheduled_result,omitempty"`
	// Delay and State carry the scheduled DNS and link states. Both are
	// omitempty, so a campaign that generates neither keeps the schedule
	// fingerprint it had before timed faults existed.
	Delay time.Duration `json:"delay_ms,omitempty"`
	State string        `json:"state,omitempty"`
}

FaultEvent is one fully resolved impairment. No random choice remains when execution starts; scheduled DNS events use Sequence rather than wall time.

type FaultEventEvidence

type FaultEventEvidence struct {
	Event           TimedEvent    `json:"event"`
	ScheduledOffset time.Duration `json:"scheduled_offset_ms"`
	AppliedOffset   time.Duration `json:"applied_offset_ms"`
	State           string        `json:"state"`
	// Observed is what the environment read back after applying the event:
	// the kernel's own rendering of the new qdisc or link state, with the
	// generated device name and qdisc handle left out.
	Observed string `json:"observed,omitempty"`
	Result   string `json:"result"`
	Error    string `json:"error,omitempty"`
}

FaultEventEvidence is what actually happened to one scheduled event. The scheduled offset is the contract; the applied offset is the OS's answer to it.

type FaultInfo

type FaultInfo struct {
	Type        string        `json:"type"`
	Node        string        `json:"node"`
	Family      string        `json:"family,omitempty"`
	Protocol    string        `json:"protocol,omitempty"`
	Port        int           `json:"port,omitempty"`
	Direction   string        `json:"direction,omitempty"`
	Summary     string        `json:"summary"`
	Command     []string      `json:"command"`
	Latency     time.Duration `json:"latency_ms,omitempty"`
	Jitter      time.Duration `json:"jitter_ms,omitempty"`
	LossPercent float64       `json:"loss_percent,omitempty"`
	Seed        uint32        `json:"seed,omitempty"`
}

FaultInfo is one injected impairment and the exact command that injected it.

type FingerprintCount

type FingerprintCount struct {
	Fingerprint DiagnosisFingerprint `json:"fingerprint"`
	Count       int                  `json:"count"`
}

type GeneratedCase

type GeneratedCase struct {
	Manifest GeneratedCaseManifest `json:"manifest"`
	Scenario *Scenario             `json:"-"`
}

GeneratedCase couples a public manifest to the validated scenario that will execute. Scenario is intentionally omitted from JSON: reports contain the normal simulation artifact, while the manifest is the reproduction contract.

type GeneratedCaseManifest

type GeneratedCaseManifest struct {
	GeneratorVersion string   `json:"generator_version"`
	Lane             HuntLane `json:"lane,omitempty"`
	BaseScenario     string   `json:"base_scenario"`
	HuntSeed         int64    `json:"hunt_seed"`
	Case             int      `json:"case"`
	CaseSeed         int64    `json:"case_seed"`
	// MaxFaults is the fault ceiling this case was drawn under. It is part of
	// the experiment, not a preference: the first number drawn from the case
	// seed is how many mutations to take, and it is drawn modulo this ceiling,
	// so the same scenario, seed and case under a different ceiling is a
	// different network. It is recorded here so a reproduction command can name
	// it rather than inherit whatever the CLI default happens to be.
	MaxFaults       int                 `json:"max_faults"`
	Mutations       []GeneratedMutation `json:"mutations"`
	CaseFingerprint string              `json:"case_fingerprint"`
}

GeneratedCaseManifest is the stable, display-safe reproduction artifact.

type GeneratedMutation

type GeneratedMutation struct {
	ID               string `json:"id"`
	Description      string `json:"description"`
	Node             string `json:"node,omitempty"`
	TargetNode       string `json:"target_node,omitempty"`
	Segment          string `json:"segment,omitempty"`
	Service          string `json:"service,omitempty"`
	Family           string `json:"family,omitempty"`
	PreferredVia     string `json:"preferred_via,omitempty"`
	PreferredSegment string `json:"preferred_segment,omitempty"`
	PreferredMetric  int    `json:"preferred_metric,omitempty"`
	AlternateVia     string `json:"alternate_via,omitempty"`
	AlternateSegment string `json:"alternate_segment,omitempty"`
	AlternateMetric  int    `json:"alternate_metric,omitempty"`
	ControlTarget    string `json:"control_target,omitempty"`
	// TargetEndpoint is the address:port the client's primary test dials, which
	// is what a simulator-side dial of that endpoint is matched against. It is
	// the endpoint a mutation is about; ControlTarget stays what it has always
	// been, the endpoint whose reachability proves some other path still works.
	TargetEndpoint string `json:"target_endpoint,omitempty"`
	// RouteDestination and RouteVia describe the route a routing family acts on:
	// which route, and the next hop it ends up with. An empty RouteVia means the
	// route was taken away rather than repointed. The next hop it had before is
	// PreferredVia, which already means exactly that.
	RouteDestination string  `json:"route_destination,omitempty"`
	RouteVia         string  `json:"route_via,omitempty"`
	LossPercent      float64 `json:"loss_percent,omitempty"`
	LatencyMS        int64   `json:"latency_ms,omitempty"`
	JitterMS         int64   `json:"jitter_ms,omitempty"`
	StartMS          int64   `json:"start_ms,omitempty"`
	DurationMS       int64   `json:"duration_ms,omitempty"`
	NetemSeed        uint32  `json:"netem_seed,omitempty"`
	TargetPort       int     `json:"target_port,omitempty"`
	Status           int     `json:"status,omitempty"`
	// MTU is the size a path-MTU black hole narrows its hop to. The endpoints
	// keep their own, which is what leaves them offering packets that hop can no
	// longer carry.
	MTU int `json:"mtu,omitempty"`
}

GeneratedMutation is a completely materialized semantic operation. It has no command strings, kernel interface names, paths, or deferred randomness.

type HuntCaseFinding

type HuntCaseFinding struct {
	Fingerprint    string           `json:"fingerprint"`
	Category       string           `json:"category"`
	Severity       HuntSeverity     `json:"severity"`
	Code           string           `json:"code"`
	SuggestionCode string           `json:"suggestion_code,omitempty"`
	Probe          string           `json:"probe,omitempty"`
	Expected       string           `json:"expected,omitempty"`
	Actual         string           `json:"actual,omitempty"`
	Cause          string           `json:"cause,omitempty"`
	Family         string           `json:"family,omitempty"`
	Summary        string           `json:"summary"`
	Evidence       string           `json:"evidence,omitempty"`
	Reproduce      HuntReproduction `json:"reproduce"`
}

type HuntCaseResult

type HuntCaseResult struct {
	Manifest             GeneratedCaseManifest `json:"manifest"`
	Truth                ObservedTruth         `json:"truth"`
	TruthFingerprint     string                `json:"truth_fingerprint"`
	DiagnosisFingerprint DiagnosisFingerprint  `json:"diagnosis_fingerprint"`
	Findings             []HuntCaseFinding     `json:"findings"`
	Report               *Report               `json:"report,omitempty"`
	Status               string                `json:"status"`
}

type HuntFinding

type HuntFinding struct {
	Fingerprint    string           `json:"fingerprint"`
	Category       string           `json:"category"`
	Severity       HuntSeverity     `json:"severity"`
	Code           string           `json:"code"`
	SuggestionCode string           `json:"suggestion_code,omitempty"`
	Probe          string           `json:"probe,omitempty"`
	Expected       string           `json:"expected,omitempty"`
	Actual         string           `json:"actual,omitempty"`
	Cause          string           `json:"cause,omitempty"`
	Family         string           `json:"family,omitempty"`
	Summary        string           `json:"summary"`
	Evidence       string           `json:"evidence,omitempty"`
	Occurrences    int              `json:"occurrences"`
	FirstCase      int              `json:"first_case"`
	ExampleCases   []int            `json:"example_cases"`
	Reproduce      HuntReproduction `json:"reproduce"`
}

type HuntLane added in v1.12.3

type HuntLane string
const (
	HuntLaneBugOracle    HuntLane = "bug-oracle"
	HuntLaneStress       HuntLane = "stress"
	HuntLaneAllOperators HuntLane = "all"
)

func ResolveHuntLane added in v1.12.3

func ResolveHuntLane(version string, lane HuntLane) (HuntLane, error)

ResolveHuntLane applies the deliberate command/API default. Current generators default to bug-oracle; historical generators default to their original all-operator universe.

type HuntOptions

type HuntOptions struct {
	Run              Options
	Cases            int
	Seed             int64
	Case             *int
	Shard            *HuntShard
	MaxFaults        int
	GeneratorVersion string
	Lane             HuntLane
	FailFast         bool
	DryRun           bool
}

type HuntReproduction

type HuntReproduction struct {
	BaseScenario string   `json:"base_scenario"`
	Lane         HuntLane `json:"lane,omitempty"`
	Seed         int64    `json:"seed"`
	Case         int      `json:"case"`
	CaseSeed     int64    `json:"case_seed"`
	// MaxFaults is the ceiling the case was drawn under. Without it the visible
	// coordinates below name a different experiment under a different ceiling,
	// so it is part of the reproduction rather than a run preference.
	MaxFaults        int    `json:"max_faults"`
	GeneratorVersion string `json:"generator_version"`
	CaseFingerprint  string `json:"case_fingerprint"`
}

func (HuntReproduction) Command added in v1.11.5

func (r HuntReproduction) Command() string

Command is the single-case reproduction, and the only place its shape is decided. Every coordinate the generator draws from is named rather than left to a flag default, the fault ceiling included, so pasting this reproduces this experiment and not whichever one the defaults of the day would pick. The scenario is a validated base name by the time a hunt runs, but this string is pasted into a shell, so it is sanitized here rather than trusted.

type HuntResult

type HuntResult struct {
	GeneratorVersion    string           `json:"generator_version"`
	Lane                HuntLane         `json:"lane,omitempty"`
	BaseScenario        string           `json:"base_scenario"`
	HuntSeed            int64            `json:"hunt_seed"`
	RequestedCases      int              `json:"requested_cases"`
	MaxFaults           int              `json:"max_faults"`
	FailFast            bool             `json:"fail_fast,omitempty"`
	DryRun              bool             `json:"dry_run,omitempty"`
	Shard               *HuntShard       `json:"shard,omitempty"`
	GeneratedCases      int              `json:"generated_cases"`
	ExecutedCases       int              `json:"executed_cases"`
	UniqueCases         int              `json:"unique_cases"`
	DuplicateCandidates int              `json:"duplicate_candidates"`
	CleanCases          int              `json:"clean_cases"`
	Findings            []HuntFinding    `json:"findings"`
	Suggestions         []HuntSuggestion `json:"suggestions"`
	Cases               []HuntCaseResult `json:"case_results"`
	FailFastStopped     bool             `json:"fail_fast_stopped"`
	Cancelled           bool             `json:"cancelled"`
	RuntimeFailure      bool             `json:"runtime_failure"`
	Result              string           `json:"result"`
	ErrorKind           string           `json:"error_kind,omitempty"`
	Error               string           `json:"error,omitempty"`
}

func MergeHuntResults added in v1.12.3

func MergeHuntResults(inputs ...*HuntResult) (*HuntResult, error)

MergeHuntResults reconstructs one logical hunt from a complete set of shard reports. Input order is irrelevant; case order comes from global case ids.

func RunHunt

func RunHunt(ctx context.Context, baseID string, base *Scenario, backend func() Backend, opts HuntOptions) *HuntResult

RunHunt generates cases independently, skips generator-defined duplicate identities in batch mode, and runs each accepted case sequentially through the normal simulator.

Each case runs netdoc exactly once, and the hunt therefore makes no claim about whether a diagnosis is reproducible. It cannot: a second run inside the same live topology inherits the neighbour, route and resolver caches the first one warmed, so the two are not the same experiment and a verdict that changed between them says only that the first probe paid for a cold path. Comparing two different cases is no better, since the coarse observed truth records that a path was impaired without recording by how much. Determinism is campaign mode's question, where `--iteration N --runs K` repeats one schedule through a whole fresh topology each time.

func (*HuntResult) WriteJSON

func (r *HuntResult) WriteJSON(w io.Writer) error

func (*HuntResult) WriteText

func (r *HuntResult) WriteText(w io.Writer)

type HuntSeverity

type HuntSeverity string
const (
	SeverityCritical HuntSeverity = "critical"
	SeverityHigh     HuntSeverity = "high"
	SeverityMedium   HuntSeverity = "medium"
	SeverityLow      HuntSeverity = "low"
	SeverityInfo     HuntSeverity = "info"
)

func ParseSeverity added in v1.10.8

func ParseSeverity(name string) (HuntSeverity, bool)

ParseSeverity maps a flag value onto the hunt severity vocabulary.

type HuntShard added in v1.12.3

type HuntShard struct {
	Index int `json:"index"`
	Count int `json:"count"`
}

HuntShard selects global case numbers using zero-based modulo partitioning.

func (HuntShard) Includes added in v1.12.3

func (s HuntShard) Includes(caseNumber int) bool

func (HuntShard) Validate added in v1.12.3

func (s HuntShard) Validate() error

type HuntSuggestion

type HuntSuggestion struct {
	Code            string           `json:"code"`
	Description     string           `json:"description"`
	Evidence        int              `json:"evidence_cases"`
	HighestSeverity HuntSeverity     `json:"highest_severity"`
	FirstCase       int              `json:"first_case"`
	ExampleCases    []int            `json:"example_cases"`
	Reproduce       HuntReproduction `json:"reproduce"`
}

type Interface

type Interface struct {
	Segment string `yaml:"segment"`
	// Address is the backward-compatible IPv4 spelling. IPv4 and IPv6 are
	// installed on this same generated kernel interface.
	Address string `yaml:"address"`
	IPv4    string `yaml:"ipv4"`
	IPv6    string `yaml:"ipv6"`
}

Interface attaches a node to one logical segment. Scenario authors never provide the kernel interface name; the backend derives a short safe name.

type InterfaceInfo

type InterfaceInfo struct {
	Segment string `json:"segment"`
	Address string `json:"address"`
	IPv4    string `json:"ipv4,omitempty"`
	IPv6    string `json:"ipv6,omitempty"`
}

InterfaceInfo and RouteInfo expose logical topology names rather than the generated Linux names used to implement them.

type IterationResult

type IterationResult struct {
	Iteration     int                  `json:"iteration"`
	IterationSeed int64                `json:"iteration_seed"`
	Schedule      []FaultEvent         `json:"fault_schedule"`
	ScheduleID    string               `json:"schedule_id"`
	Fingerprint   DiagnosisFingerprint `json:"diagnosis_fingerprint"`
	Report        *Report              `json:"report"`
	Reproduce     Reproduction         `json:"reproduce"`
}

type KernelRoute added in v1.11.3

type KernelRoute struct {
	Destination string `json:"destination"`
	Via         string `json:"via,omitempty"`
	Segment     string `json:"segment,omitempty"`
	Metric      int    `json:"metric,omitempty"`
}

KernelRoute is one line of a node's real routing table, in the simulator's logical vocabulary: interfaces are named by topology segment rather than by the kernel name the run happened to allocate.

type LinkEvidence

type LinkEvidence struct {
	Node    string `json:"node"`
	Segment string `json:"segment"`
	Address string `json:"address"`
	IPv4    string `json:"ipv4,omitempty"`
	IPv6    string `json:"ipv6,omitempty"`
	Up      bool   `json:"up"`
	// MTU is what the kernel reports for this interface, which is the reading a
	// path-MTU black hole is made of: one hop carrying less than the endpoints
	// still offer. Zero means the link was read and reported none, so a fault
	// that narrows nothing cannot confirm itself from a missing number.
	MTU int `json:"mtu,omitempty"`
}

LinkEvidence describes one actual namespace interface using its logical segment name. Kernel implementation names are intentionally absent.

type NetdocIdentity added in v1.11.3

type NetdocIdentity struct {
	Path    string `json:"path"`
	Version string `json:"version"`
}

NetdocIdentity is which Network Doctor executable produced a result: the absolute path the run launched, and the line that same executable printed for -version. It is what makes a saved result reproducible: `netdoc` on the next machine, or in the next month, is not necessarily this build.

The version is recorded as the binary reported it. A local build says `dev`, and that is the honest answer; nothing here infers a version from the checkout, the filename, or the simulator's own build.

type NetworkCondition added in v1.11.3

type NetworkCondition string

A hunt false negative means exactly one thing: the simulator independently established a network condition whose diagnostic meaning Network Doctor failed to recognize. It deliberately does not mean "a mutation expected probe X to fail and probe X did not fail": a mutation is intent rather than truth, and a probe id is an implementation detail of how the diagnosis is assembled rather than the thing a user is told.

NetworkCondition is the vocabulary in between: one domain-level fact about the network. Every condition is established from simulator-side observation alone and recognized from one diagnosis alone. Nothing in this file reads the mutation manifest, and no diagnosis ever feeds back into truth.

const (
	ConditionIPv4InternetUnreachable NetworkCondition = "ipv4_internet_unreachable"
	ConditionIPv6InternetUnreachable NetworkCondition = "ipv6_internet_unreachable"
	ConditionTLSCertificateExpired   NetworkCondition = "tls_certificate_expired"
	ConditionTLSHostnameMismatch     NetworkCondition = "tls_hostname_mismatch"
	ConditionProxyDestinationRefused NetworkCondition = "proxy_destination_refused"
	ConditionQUICUDP443Blocked       NetworkCondition = "quic_udp_443_blocked"
	ConditionNoDefaultRoute          NetworkCondition = "no_default_route"
)

type Node

type Node struct {
	Name string `yaml:"name"`
	// Role client marks the namespace netdoc runs in; every other node is
	// scenery. Exactly one client per scenario.
	Role    string `yaml:"role"`
	Address string `yaml:"address"`
	// Interfaces is the explicit multi-segment form. Address is retained only
	// for legacy single-segment scenario compatibility.
	Interfaces []Interface `yaml:"interfaces"`
	// Aliases are extra addresses put on the node's loopback, which is how the
	// simulated internet claims diagnostic's production probe endpoints without
	// anything leaving the namespace.
	Aliases  []string  `yaml:"aliases"`
	Gateway  string    `yaml:"gateway"`
	Resolver string    `yaml:"resolver"`
	Services []Service `yaml:"services"`
}

Node is one network namespace on the segment.

type NodeInfo

type NodeInfo struct {
	Name       string          `json:"name"`
	Role       string          `json:"role"`
	Address    string          `json:"address"`
	Aliases    []string        `json:"aliases,omitempty"`
	Interface  string          `json:"interface"`
	Gateway    string          `json:"gateway,omitempty"`
	Resolver   string          `json:"resolver,omitempty"`
	Services   []string        `json:"services,omitempty"`
	PID        int             `json:"pid"`
	Interfaces []InterfaceInfo `json:"interfaces,omitempty"`
	Routes     []RouteInfo     `json:"routes,omitempty"`
}

NodeInfo is a namespace the simulation created.

type NumberRange

type NumberRange struct {
	Min float64 `yaml:"min"`
	Max float64 `yaml:"max"`
}

type ObservedTruth

type ObservedTruth struct {
	DNS            string   `json:"dns"`
	IPv4           string   `json:"ipv4"`
	IPv6           string   `json:"ipv6"`
	Gateway        string   `json:"gateway"`
	Proxy          string   `json:"proxy"`
	TLS            string   `json:"tls"`
	TCP            string   `json:"tcp"`
	Link           string   `json:"link"`
	Packet         string   `json:"packet"`
	Route          string   `json:"route"`
	ObservedFaults []string `json:"observed_faults"`
}

ObservedTruth is deliberately coarse. It records only simulator evidence, not conclusions inferred from the mutation request or copied from netdoc. IPv4 and IPv6 describe point-in-time reachability when final evidence was collected, after the diagnostic run and fault scheduler had stopped.

type Options

type Options struct {
	// Netdoc is the netdoc binary the tests execute. Required.
	Netdoc string
	// ProbeTimeout is passed to netdoc as -timeout, and is what the report uses
	// to tell a probe that answered from one that ran out of time.
	ProbeTimeout time.Duration
	// Repeat runs each test this many times to catch a diagnosis that is not
	// reproducible. Values below 1 mean once.
	Repeat int
	// Keep leaves the environment running after the report is written.
	Keep bool
	// Hold, when set, is called once the topology is built, every fault is
	// applied and the timeline's opening state has landed, and before the first
	// netdoc process starts. Challenge Mode uses it to hand the finished network
	// to a person, so both they and netdoc face the same state. A non-nil error
	// abandons the run before any test, and cleanup still happens.
	Hold func(context.Context, Env) error
	// SetupTimeout, TestTimeout and CleanupTimeout bound the three phases. Each
	// falls back to a sane default when zero.
	SetupTimeout   time.Duration
	TestTimeout    time.Duration
	CleanupTimeout time.Duration
	// Log receives a line per privileged command as it runs. Nil is quiet.
	Log io.Writer
}

Options tune one simulation run.

type PacketConditionEvidence

type PacketConditionEvidence struct {
	Node           string        `json:"node"`
	Segment        string        `json:"segment"`
	Latency        time.Duration `json:"latency_ms,omitempty"`
	Jitter         time.Duration `json:"jitter_ms,omitempty"`
	LossPercent    float64       `json:"loss_percent,omitempty"`
	Seed           uint32        `json:"seed,omitempty"`
	Active         bool          `json:"active"`
	DroppedPackets uint64        `json:"dropped_packets"`
	ObservedMinRTT time.Duration `json:"observed_min_rtt_ms,omitempty"`
	ObservedMaxRTT time.Duration `json:"observed_max_rtt_ms,omitempty"`
	RTTSamples     int           `json:"rtt_samples"`
}

type PacketDropEvidence added in v1.11.3

type PacketDropEvidence struct {
	Node      string `json:"node"`
	Family    string `json:"family,omitempty"`
	Protocol  string `json:"protocol,omitempty"`
	Port      int    `json:"port,omitempty"`
	To        string `json:"to,omitempty"`
	Direction string `json:"direction"`
	Packets   uint64 `json:"packets"`
}

PacketDropEvidence is the kernel's own count of the packets one drop fault's rule matched, read back from that rule's nftables counter once the run ended. A rule that was installed but never matched anything reports zero: a fault that was injected and a fault that took effect are different claims, and only the counter can tell them apart.

type ProbeFingerprint

type ProbeFingerprint struct {
	Test   string `json:"test"`
	ID     string `json:"id"`
	Status string `json:"status"`
	Cause  string `json:"cause,omitempty"`
	IPv4   string `json:"ipv4,omitempty"`
	IPv6   string `json:"ipv6,omitempty"`
}

type Report

type Report struct {
	Scenario    string        `json:"scenario"`
	Description string        `json:"description,omitempty"`
	ID          string        `json:"id"`
	Backend     string        `json:"backend"`
	StartedAt   time.Time     `json:"started_at"`
	Duration    time.Duration `json:"duration_ms"`
	Result      string        `json:"result"`
	// Error is set when setup, not diagnosis, is what failed.
	Error string `json:"error,omitempty"`

	Topology []NodeInfo  `json:"topology"`
	Faults   []FaultInfo `json:"faults"`
	// Timeline is what the fault scheduler did, relative to T0, the instant
	// just before the first netdoc process started. TimelineID identifies the
	// requested timeline and ignores how long the OS took to apply it.
	Timeline    []FaultEventEvidence `json:"fault_timeline"`
	TimelineID  string               `json:"fault_timeline_id,omitempty"`
	Tests       []TestOutcome        `json:"tests"`
	Evidence    Evidence             `json:"evidence"`
	Cleanup     CleanupInfo          `json:"cleanup"`
	Suggestions []Suggestion         `json:"suggestions"`
}

Report is one simulation, start to finish. It is the machine-readable artifact: `netdoc-sim run --json` prints exactly this.

func Run

func Run(ctx context.Context, s *Scenario, b Backend, opts Options) (rep *Report)

Run executes one scenario end to end and always returns a report: setup failures, cancellation and panics are reported, not returned as bare errors, because a simulation that fell over is itself a result worth printing.

Cleanup runs on every exit path. It gets a context detached from the caller's so a cancelled or timed-out run still releases its namespaces.

func (*Report) WriteJSON

func (r *Report) WriteJSON(w io.Writer) error

WriteJSON prints the machine-readable report.

func (*Report) WriteText

func (r *Report) WriteText(w io.Writer)

WriteText prints the human-readable report. Every string that came from a subprocess goes through textsafe first: netdoc's detail lines carry remote text, and this one lands on a terminal.

type Reproduction

type Reproduction struct {
	Scenario  string `json:"scenario"`
	Seed      int64  `json:"seed"`
	Iteration int    `json:"iteration"`
}

type Route

type Route struct {
	Node        string `yaml:"node"`
	Destination string `yaml:"destination"`
	Via         string `yaml:"via"`
	Metric      int    `yaml:"metric"`
	Default     bool   `yaml:"-"`
	Family      string `yaml:"-"`
}

Route is a validated unicast route. Destination is either "default" or a canonical prefix; free-form iproute expressions are deliberately impossible.

type RouteEvidence

type RouteEvidence struct {
	Node             string `json:"node"`
	Destination      string `json:"destination"`
	Via              string `json:"via,omitempty"`
	Segment          string `json:"segment"`
	Metric           int    `json:"metric"`
	Family           string `json:"family,omitempty"`
	Selected         bool   `json:"selected"`
	Source           string `json:"source,omitempty"`
	GatewayReachable *bool  `json:"gateway_reachable,omitempty"`
}

RouteEvidence combines the validated route with the kernel's selected path. GatewayReachable is omitted when no neighbor observation was available.

type RouteInfo

type RouteInfo struct {
	Destination string `json:"destination"`
	Via         string `json:"via"`
	Segment     string `json:"segment"`
	Metric      int    `json:"metric"`
	Family      string `json:"family,omitempty"`
}

type RouteTableEvidence added in v1.11.3

type RouteTableEvidence struct {
	Node   string        `json:"node"`
	Family string        `json:"family"`
	Routes []KernelRoute `json:"routes"`
}

RouteTableEvidence is the routing table one node's kernel actually held when the run ended, read back with `ip route show` from inside that node's own namespace. RouteEvidence answers "where does this destination go"; this answers the different question of "what routes exist at all", which is the only thing that can establish an absence: a route that was deleted, or one that was never installed, leaves no trace in a per-destination lookup beyond the lookup failing, and a failed lookup is not the same claim as a missing route.

A record exists for every family the node has an address in, so an empty Routes list is the positive statement "this table was read and held nothing", not "nobody looked". Families the node has no address in get no record.

type RouterEvidence

type RouterEvidence struct {
	Node           string `json:"node"`
	IPv4Forwarding bool   `json:"ipv4_forwarding"`
	IPv6Forwarding bool   `json:"ipv6_forwarding"`
}

type SOCKSEvidence

type SOCKSEvidence struct {
	Node        string `json:"node"`
	Service     string `json:"service,omitempty"`
	Event       string `json:"event"`
	AddressType string `json:"address_type,omitempty"`
	Destination string `json:"destination,omitempty"`
	Port        int    `json:"port,omitempty"`
	Result      string `json:"result"`
	Count       int    `json:"count"`
}

SOCKSEvidence aggregates protocol events observed by a SOCKS service. A greeting proves proxy reachability even when local DNS fails before the client can send a CONNECT request.

type Scenario

type Scenario struct {
	Name        string        `yaml:"name"`
	Description string        `yaml:"description"`
	Topology    Topology      `yaml:"topology"`
	Faults      []Fault       `yaml:"faults"`
	Tests       []Test        `yaml:"tests"`
	Expect      Expect        `yaml:"expect"`
	Campaign    *CampaignSpec `yaml:"campaign,omitempty"`
}

Scenario is one simulation: a topology to build, faults to inject, netdoc runs to make, and the diagnosis those runs should produce.

func LibraryScenario

func LibraryScenario(name string) (*Scenario, error)

LibraryScenario loads a built-in scenario by name.

func Load

func Load(ref string) (*Scenario, error)

Load resolves a scenario reference: a built-in name, or a path to a YAML file. A reference containing a path separator or a .yaml suffix is always treated as a file, so a local scenario can never be shadowed by a built-in.

func LoadScenario

func LoadScenario(path string) (*Scenario, error)

LoadScenario reads and validates a scenario file.

func ParseScenario

func ParseScenario(r io.Reader) (*Scenario, error)

ParseScenario decodes YAML and validates it. Unknown fields are an error, so a typo'd key fails loudly instead of being silently ignored.

func (*Scenario) Client

func (s *Scenario) Client() *Node

Client returns the node netdoc runs in. Validate guarantees there is one.

func (*Scenario) Validate

func (s *Scenario) Validate() error

Validate reports the first thing wrong with the scenario. It runs before any namespace exists, so a bad scenario costs nothing.

type ScheduledEvent

type ScheduledEvent struct {
	// At is the offset from T0, as a Go duration ("700ms").
	At string `yaml:"at"`
	// Latency, Jitter and LossPercent describe a scheduled_netem state. All
	// three absent means an unimpaired link.
	Latency     string  `yaml:"latency"`
	Jitter      string  `yaml:"jitter"`
	LossPercent float64 `yaml:"loss_percent"`
	// Outcome and Delay describe a scheduled_dns state. Delay is required by,
	// and only by, the delay outcome.
	Outcome string `yaml:"outcome"`
	Delay   string `yaml:"delay"`
	// State describes a scheduled_link event: up or down.
	State string `yaml:"state"`
}

ScheduledEvent is one timed change written in a scenario file. Every field is a validated value: no command, device name, qdisc handle or path can be spelled here.

type Segment

type Segment struct {
	Name string `yaml:"name"`
	// Subnet is the backward-compatible IPv4 spelling used by the original
	// routed scenarios. IPv4 and IPv6 let one logical L2 segment carry both
	// families without manufacturing a second interface.
	Subnet string `yaml:"subnet"`
	IPv4   string `yaml:"ipv4"`
	IPv6   string `yaml:"ipv6"`
}

Segment is one simulator-owned Linux bridge.

type Service

type Service struct {
	// Name is optional for existing scenarios, but required when another
	// scenario object needs to identify the service. Non-empty names are unique
	// across the topology.
	Name string `yaml:"name"`
	Type string `yaml:"type"`
	Port int    `yaml:"port"`
	// Banner makes a TCP fixture write one bounded, newline-terminated protocol
	// greeting before it drains the client connection.
	Banner string `yaml:"banner"`
	// Zone maps a name to an address for ServiceDNS. A name that is absent
	// answers NXDOMAIN, which is how "DNS returns NXDOMAIN" is expressed.
	Zone map[string]string `yaml:"zone"`
	// Records is the ordered multi-record form. Zone remains the compatible
	// single-record shorthand; both forms describe static address records only.
	Records []DNSRecord `yaml:"records"`
	// Body and Status shape the ServiceHTTP reply on every path but the
	// connectivity check, which answers 204 so netdoc's captive-portal check
	// passes unless Portal says otherwise.
	Status int    `yaml:"status"`
	Body   string `yaml:"body"`
	// DateOffset moves the HTTP Date header relative to the server's wall clock.
	// It uses Go duration syntax and is empty when the ordinary net/http header
	// should be left untouched.
	DateOffset string `yaml:"date_offset"`
	// Portal makes ServiceHTTP intercept the connectivity check the way a
	// captive portal does: /generate_204 redirects to a fixed sign-in page
	// instead of answering 204. Intent only, as every other fixture mode is:
	// the sign-in URL is the simulator's, since a scenario-supplied one would
	// be a raw URL in a file that is otherwise not allowed to carry any.
	Portal bool `yaml:"portal"`
	// Certificate describes simulator-generated TLS identity. Scenario files
	// select intent only; they cannot provide keys, PEM, paths, or algorithms.
	Certificate *TLSCertificate `yaml:"certificate"`
	// DNSFault is a bounded, precomputed response schedule. It is consumed per
	// queried name and query type, never generated by a service goroutine.
	DNSFault *DNSFault `yaml:"dns_fault"`
	// DoHResponse is the encrypted-DNS fixture's response mode. Empty serves a
	// valid DNS message; invalid serves deterministic protocol-invalid bytes on
	// DoH only so DoT remains an independent control.
	DoHResponse string `yaml:"doh_response"`
}

Service is a test server the node runs. Ports are bound inside the node's namespace, so two nodes may both serve :53 or :443.

type ServiceReplyEvidence added in v1.11.3

type ServiceReplyEvidence struct {
	Node    string `json:"node"`
	Service string `json:"service,omitempty"`
	Type    string `json:"type"`
	Port    int    `json:"port"`
	Status  int    `json:"status,omitempty"`
	Result  string `json:"result"`
	Count   int    `json:"count"`
}

ServiceReplyEvidence counts the replies a controlled service actually sent, in the shape it sent them. It is the companion to ServiceStateEvidence and deliberately not the same record: a service that came up in a faulty mode has a state, but until a client reaches it and it answers, nothing was done to anyone. Only a reply proves the fault reached the wire.

type ServiceStateEvidence added in v1.11.2

type ServiceStateEvidence struct {
	Node    string `json:"node"`
	Service string `json:"service,omitempty"`
	Type    string `json:"type"`
	Port    int    `json:"port"`
	Mode    string `json:"mode,omitempty"`
	Status  int    `json:"status,omitempty"`
}

ServiceStateEvidence records the mode of a successfully started controlled service. It is emitted by the node holder, not copied into the report from a hunt manifest or diagnosis.

type StarterPack added in v1.11.3

type StarterPack struct {
	ID          string   `json:"id"`
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Challenges  []string `json:"challenges"`
}

StarterPack is a pack as it is published: a stable machine id, a name and a sentence for a person, and the challenge ids in the order they are meant to be worked through. The ids are the whole content, and anybody can play one directly without going through a pack at all.

func StarterPackByID added in v1.11.3

func StarterPackByID(raw string) (StarterPack, bool)

StarterPackByID resolves a pack a person named.

func StarterPacks added in v1.11.3

func StarterPacks() []StarterPack

StarterPacks lists the curated packs, in the order they are offered.

type State

type State struct {
	ID       string    `json:"id"`
	Scenario string    `json:"scenario"`
	PID      int       `json:"pid"`
	Started  time.Time `json:"started"`
	// Stamp identifies the process behind PID across pid reuse. Without it,
	// releasing a simulation whose director has since exited would signal
	// whichever unrelated process inherited the number.
	Stamp     string     `json:"stamp"`
	Workspace string     `json:"workspace"`
	Nodes     []NodeInfo `json:"nodes"`
}

State is the record a kept simulation leaves behind so another process can find, inspect and release it. A run that is not kept writes none: its namespaces die with its process tree, so there is nothing to record.

func ListStates

func ListStates() ([]*State, error)

ListStates returns every recorded simulation, newest first. Records whose process is gone are returned too; Alive tells them apart, and Release is how their leftovers get swept up.

func LoadState

func LoadState(id string) (*State, error)

LoadState reads one simulation's record. The id the caller asked for is the only one that ever reaches the filesystem, and the decoded record has to agree with it: releasing acts destructively on paths derived from the id, so a record is not allowed to name a different simulation than the file it was found in.

func NewState

func NewState(id, scenario, workspace string, started time.Time, nodes []NodeInfo) *State

NewState records the calling process as the holder of a kept simulation.

func (*State) Alive

func (s *State) Alive() bool

Alive reports whether the process holding this simulation's namespaces is still running. Two things have to hold, because killing an unrelated process would be the worst bug this package could have. The stamp catches a recycled pid, and the executable check catches the case the stamp cannot: a stamp is readable out of /proc by anything that can also doctor the record, so on its own it proves nothing about which program is behind the number.

func (*State) Release

func (s *State) Release() error

Release ends a kept simulation: the director is asked to stop, which takes its namespaces and every holder with it, then the leftovers on disk go. Idempotent, so releasing an already-dead simulation just sweeps its files.

A record that does not survive validation is not swept at all. Nothing is signalled unless the pid is provably still this simulation's director; a pid that is gone, or recycled by some other program, is simply left alone while the reconstructed leftovers go.

func (*State) Save

func (s *State) Save() error

Save writes the record for a kept simulation. The record is this process's claim on the id, so it is created exclusively: an id that already has a record is a collision to report, never a file to truncate, and O_EXCL also refuses the symlink somebody may have left where the record belongs, since the kernel does not follow the final component when it is set.

type Suggestion

type Suggestion struct {
	Code     string `json:"code"`
	Test     string `json:"test,omitempty"`
	Probe    string `json:"probe,omitempty"`
	Cause    string `json:"cause,omitempty"`
	Message  string `json:"message"`
	Evidence string `json:"evidence,omitempty"`
}

Suggestion is one deterministic, evidence-backed improvement for netdoc.

type TCPResetEvidence

type TCPResetEvidence struct {
	Node    string `json:"node"`
	Service string `json:"service,omitempty"`
	Event   string `json:"event"`
	Result  string `json:"result"`
	Count   int    `json:"count"`
}

type TLSCertificate

type TLSCertificate struct {
	Mode     string   `yaml:"mode"`
	DNSNames []string `yaml:"dns_names"`
}

TLSCertificate is the narrow certificate intent accepted by a TLS service.

type TLSEvidence

type TLSEvidence struct {
	Node                 string    `json:"node"`
	Service              string    `json:"service"`
	CertificateMode      string    `json:"certificate_mode"`
	RequestedServer      string    `json:"requested_server,omitempty"`
	CertificateDNS       []string  `json:"certificate_dns"`
	NotBefore            time.Time `json:"not_before"`
	NotAfter             time.Time `json:"not_after"`
	CertificatePresented bool      `json:"certificate_presented"`
	Result               string    `json:"result"`
	Count                int       `json:"count"`
}

TLSEvidence aggregates handshakes observed by a simulator TLS service. It contains certificate metadata only; private keys never enter the recorder.

type Test

type Test struct {
	Name          string     `yaml:"name"`
	Type          string     `yaml:"type"`
	Node          string     `yaml:"node"`
	Target        string     `yaml:"target"`
	SourceSegment string     `yaml:"source_segment"`
	Proxy         *TestProxy `yaml:"proxy"`
	Trust         *TestTrust `yaml:"trust"`
	Expect        *Expect    `yaml:"expect"`
}

Test is one netdoc run inside a node. An empty Target runs the generic (no-target) checks, exactly as `netdoc` with no argument does.

type TestOutcome

type TestOutcome struct {
	Name          string        `json:"name"`
	Node          string        `json:"node"`
	Target        string        `json:"target,omitempty"`
	Proxy         string        `json:"proxy,omitempty"`
	Trust         string        `json:"trust,omitempty"`
	SourceSegment string        `json:"source_segment,omitempty"`
	Command       []string      `json:"command"`
	Duration      time.Duration `json:"duration_ms"`
	// StartOffset and EndOffset place this netdoc process on the fault
	// timeline, relative to T0.
	StartOffset time.Duration `json:"start_offset_ms"`
	EndOffset   time.Duration `json:"end_offset_ms"`
	ExitCode    int           `json:"exit_code"`
	// ProcessOutcome distinguishes a whole-netdoc deadline or signal from a
	// probe row that used its own timeout budget.
	ProcessOutcome string `json:"process_outcome"`
	Signal         string `json:"signal,omitempty"`
	// Error is set when netdoc could not be run or produced no report at all.
	Error     string     `json:"error,omitempty"`
	Stderr    string     `json:"stderr,omitempty"`
	Diagnosis *Diagnosis `json:"diagnosis,omitempty"`

	ExpectedVerdict string            `json:"expected_verdict,omitempty"`
	ExpectedSummary string            `json:"expected_summary,omitempty"`
	ActualVerdict   string            `json:"actual_verdict,omitempty"`
	Checks          []CheckComparison `json:"checks"`
	// TimedOut names probes whose failure was the probe deadline expiring
	// rather than an answer, a diagnosis that cost the full budget.
	TimedOut []string `json:"timed_out,omitempty"`
	// RepeatVerdicts holds the verdict of every repeat run, present only when
	// --repeat asked for more than one.
	RepeatVerdicts []string `json:"repeat_verdicts,omitempty"`

	FalseNegatives int `json:"false_negatives"`
	FalsePositives int `json:"false_positives"`
	Matched        int `json:"matched"`
}

TestOutcome is one netdoc run and how its diagnosis lined up.

type TestProxy

type TestProxy struct {
	Scheme string `yaml:"scheme"`
	Node   string `yaml:"node"`
	Port   int    `yaml:"port"`
	// contains filtered or unexported fields
}

TestProxy selects one SOCKS service and the public URL scheme netdoc should receive. The address is derived from the validated node; scenarios cannot supply a raw proxy URL or environment variable.

type TestTrust

type TestTrust struct {
	Service string `yaml:"service"`
}

TestTrust selects the public root generated by one validated TLS service. The runner turns it into SSL_CERT_FILE; scenarios cannot supply environment names, paths, or certificate bytes.

type TimedEvent

type TimedEvent struct {
	Offset      time.Duration `json:"offset_ms"`
	Type        string        `json:"type"`
	Node        string        `json:"node,omitempty"`
	Segment     string        `json:"segment,omitempty"`
	Service     string        `json:"service,omitempty"`
	Latency     time.Duration `json:"latency_ms,omitempty"`
	Jitter      time.Duration `json:"jitter_ms,omitempty"`
	LossPercent float64       `json:"loss_percent,omitempty"`
	NetemSeed   uint32        `json:"netem_seed,omitempty"`
	Outcome     string        `json:"outcome,omitempty"`
	Delay       time.Duration `json:"delay_ms,omitempty"`
	State       string        `json:"state,omitempty"`
}

TimedEvent is one fully resolved scheduled change. The campaign generator and the scenario loader both produce these before T0; nothing about them is decided while the simulation runs.

type Topology

type Topology struct {
	Subnet   string    `yaml:"subnet"`
	Segments []Segment `yaml:"segments"`
	Nodes    []Node    `yaml:"nodes"`
	Routes   []Route   `yaml:"routes"`
}

Topology describes L2 segments, node interfaces, and routes. Subnet remains the backward-compatible shorthand used by the original single-segment scenarios; validation normalizes it into Segments, Interfaces, and Routes.

type TriageBaseline added in v1.10.8

type TriageBaseline struct {
	Scenario string `json:"scenario"`
	Seed     int64  `json:"seed"`
}

TriageBaseline is a known-good scenario and its fixed regression seed. Automated exploration may override it, and historical findings reproduce from the numeric seed recorded in their hunt report.

func TriageBaselineFor added in v1.10.8

func TriageBaselineFor(scenario string) (TriageBaseline, bool)

TriageBaselineFor returns the fixed regression seed for a baseline scenario.

func TriageBaselines added in v1.10.8

func TriageBaselines() []TriageBaseline

TriageBaselines returns the baseline scenarios and fixed regression seeds.

type TriageFinding added in v1.10.8

type TriageFinding struct {
	Fingerprint      string               `json:"fingerprint"`
	Scenario         string               `json:"scenario"`
	Seed             int64                `json:"seed"`
	Case             int                  `json:"case"`
	CaseSeed         int64                `json:"case_seed"`
	MaxFaults        int                  `json:"max_faults"`
	CaseFingerprint  string               `json:"case_fingerprint"`
	GeneratorVersion string               `json:"generator_version"`
	Lane             HuntLane             `json:"lane,omitempty"`
	Finding          HuntFinding          `json:"finding"`
	Reproducible     bool                 `json:"reproducible"`
	Truth            ObservedTruth        `json:"simulator_truth"`
	Diagnosis        DiagnosisFingerprint `json:"diagnosis_fingerprint"`
	Mutations        []GeneratedMutation  `json:"mutations"`
	Issue            TriageIssue          `json:"issue"`
}

func NewTriageFinding added in v1.10.8

func NewTriageFinding(finding HuntFinding) TriageFinding

NewTriageFinding derives the stable issue identity for one hunt finding. Scenario, seed, case and case fingerprint pin the generated network; the hunt fingerprint pins what disagreed. The same bug in the same case therefore keeps one issue.

func (*TriageFinding) IssueBody added in v1.10.8

func (f *TriageFinding) IssueBody(revision, runContext string) string

IssueBody renders the report a human needs to start debugging without re-running anything first.

func (*TriageFinding) IssueTitle added in v1.10.8

func (f *TriageFinding) IssueTitle() string

IssueTitle carries the fingerprint as a bare hex word, which is what the duplicate search matches on: GitHub tokenizes it whole, and no other title text can collide with it.

func (*TriageFinding) ReproduceCommand added in v1.10.8

func (f *TriageFinding) ReproduceCommand() string

ReproduceCommand is the copy/pasteable single-case reproduction, built from this finding's own flattened coordinates through the one place that decides the command's shape, so the hunt report and a filed issue cannot drift into printing two different commands for one case.

type TriageIssue added in v1.10.8

type TriageIssue struct {
	Status string `json:"status"`
	URL    string `json:"url,omitempty"`
}

type TriageReport added in v1.10.8

type TriageReport struct {
	Revision  string                 `json:"revision"`
	Lane      HuntLane               `json:"lane,omitempty"`
	Context   string                 `json:"context,omitempty"`
	Baselines []TriageScenarioResult `json:"baselines"`
	Findings  []TriageFinding        `json:"findings"`
	Result    string                 `json:"result"`
	Error     string                 `json:"error,omitempty"`
}

func (*TriageReport) WriteJSON added in v1.10.8

func (r *TriageReport) WriteJSON(w io.Writer) error

func (*TriageReport) WriteText added in v1.10.8

func (r *TriageReport) WriteText(w io.Writer)

type TriageScenarioResult added in v1.10.8

type TriageScenarioResult struct {
	Scenario   string `json:"scenario"`
	Seed       int64  `json:"seed"`
	Cases      int    `json:"executed_cases"`
	Candidates int    `json:"candidate_findings"`
	Filtered   int    `json:"below_severity_floor"`
	HuntResult string `json:"hunt_result"`
}

Jump to

Keyboard shortcuts

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