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
- Variables
- func AuthoredChallengeSlugs() []string
- func ChallengeAnswerNames() []string
- func DailyDate(at time.Time) string
- func DeriveHuntCaseSeed(seed int64, base string, caseNumber int) int64
- func DeriveIterationSeed(seed int64, scenario string, iteration int) int64
- func HuntBaseNames() []string
- func HuntGeneratorVersions() []string
- func HuntLaneNames() []string
- func LaunchDirector(ctx context.Context, self string, argv []string, stdin io.Reader, ...) (int, error)
- func LibraryNames() []string
- func NewID() string
- func NormalizeChallengeID(raw string) (string, error)
- func ParseDailyDate(raw string) (string, error)
- func RandomChallengeID() (string, error)
- func RandomSeed() (int64, error)
- func RunNode(ctx context.Context, cfgPath string, stdin io.Reader, stdout, stderr io.Writer) (err error)
- func SeverityAtLeast(severity, floor HuntSeverity) bool
- func StarterPackNames() []string
- func StateDir() string
- func ValidateMergedHuntResult(result *HuntResult) error
- func WriteAnswerMenu(w io.Writer)
- type AuthoredChallenge
- type Backend
- type CampaignDNS
- type CampaignDNSDelay
- type CampaignNetem
- type CampaignOptions
- type CampaignResult
- type CampaignSpec
- type CampaignTimeline
- type Capabilities
- type Challenge
- type ChallengeAnswer
- type ChallengeAnswerInfo
- type ChallengeContestant
- type ChallengeOptions
- type ChallengeResult
- type ChallengeSession
- type ChallengeSubmission
- type ChallengeTiming
- type ChallengeTruth
- type CheckComparison
- type CleanupInfo
- type ControlledTargetEvidence
- type DNSEvidence
- type DNSFault
- type DNSQueryEvidence
- type DNSRecord
- type Diagnosis
- type DiagnosisAttempt
- type DiagnosisCheck
- type DiagnosisFamilies
- type DiagnosisFinding
- type DiagnosisFingerprint
- type DurationRange
- type Env
- type Evidence
- type ExecResult
- type Expect
- type ExpectedCheck
- type FamilyReachabilityEvidence
- type Fault
- type FaultEvent
- type FaultEventEvidence
- type FaultInfo
- type FingerprintCount
- type GeneratedCase
- type GeneratedCaseManifest
- type GeneratedMutation
- type HuntCaseFinding
- type HuntCaseResult
- type HuntFinding
- type HuntLane
- type HuntOptions
- type HuntReproduction
- type HuntResult
- type HuntSeverity
- type HuntShard
- type HuntSuggestion
- type Interface
- type InterfaceInfo
- type IterationResult
- type KernelRoute
- type LinkEvidence
- type NetdocIdentity
- type NetworkCondition
- type Node
- type NodeInfo
- type NumberRange
- type ObservedTruth
- type Options
- type PacketConditionEvidence
- type PacketDropEvidence
- type ProbeFingerprint
- type Report
- type Reproduction
- type Route
- type RouteEvidence
- type RouteInfo
- type RouteTableEvidence
- type RouterEvidence
- type SOCKSEvidence
- type Scenario
- type ScheduledEvent
- type Segment
- type Service
- type ServiceReplyEvidence
- type ServiceStateEvidence
- type StarterPack
- type State
- type Suggestion
- type TCPResetEvidence
- type TLSCertificate
- type TLSEvidence
- type Test
- type TestOutcome
- type TestProxy
- type TestTrust
- type TimedEvent
- type Topology
- type TriageBaseline
- type TriageFinding
- type TriageIssue
- type TriageReport
- type TriageScenarioResult
Constants ¶
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.
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.
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.
const ( ChallengeHumanWins = "human_wins" ChallengeNetdocWins = "network_doctor_wins" ChallengeDraw = "draw" ChallengeNobodyWins = "nobody_wins" ChallengeNoResult = "no_result" )
Matchups.
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.
const ( ProcessExited = "exited" ProcessTimedOut = "timed_out" ProcessCancelled = "cancelled" ProcessSignaled = "signaled" ProcessExecError = "exec_error" )
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.
const ( FamilyStateReachable = "reachable" FamilyStateUnreachable = "unreachable" 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.
const ( HuntResultClean = "clean" HuntResultFindings = "findings" HuntResultError = "error" HuntResultCancelled = "cancelled" HuntMaxShards = 500 )
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.
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 )
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.
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.
const ( TLSCertificateValid = "valid" TLSCertificateExpired = "expired" TLSCertificateNotYetValid = "not_yet_valid" TLSCertificateHostnameMismatch = "hostname_mismatch" DoHResponseInvalid = "invalid" )
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.
const ( DirectionOutbound = "outbound" DirectionInbound = "inbound" )
FaultDrop directions.
const ( DNSOutcomeAnswer = "answer" DNSOutcomeSERVFAIL = "servfail" DNSOutcomeREFUSED = "refused" DNSOutcomeTruncated = "truncated" DNSOutcomeWrongAnswer = "wrong_answer" )
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.
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.
const ( LinkStateUp = "up" LinkStateDown = "down" )
Link states a scheduled_link event may ask for.
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.
const ( TriageResultClean = "clean" TriageResultFindings = "findings" TriageResultError = "error" IssueStatusNotFiled = "not_filed" IssueStatusExisting = "existing" IssueStatusCreated = "created" )
const (
// AuthoredIDVersion is the id version authored challenges resolve through.
AuthoredIDVersion = "A1"
)
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" )
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.
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 ¶
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.
var ChallengeDifficulties = []string{DifficultyEasy, DifficultyMedium, DifficultyHard}
ChallengeDifficulties is ordered API.
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.
var HuntSeverities = []HuntSeverity{ SeverityCritical, SeverityHigh, SeverityMedium, SeverityLow, SeverityInfo, }
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
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 ¶
DeriveHuntCaseSeed makes case N independent of every earlier PRNG stream.
func DeriveIterationSeed ¶
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
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
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
RandomChallengeID draws a fresh id, at the current version.
func RandomSeed ¶
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
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.
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 ¶
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) 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
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
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
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
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
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
Replay is the command that reproduces this exact challenge.
func (*Challenge) WriteBriefing ¶ added in v1.11.3
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 ¶
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 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 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 HuntOptions ¶
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) 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
HuntShard selects global case numbers using zero-based modulo partitioning.
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
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 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 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 ¶
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.
type Reproduction ¶
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 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 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 ¶
LibraryScenario loads a built-in scenario by name.
func Load ¶
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 ¶
LoadScenario reads and validates a scenario file.
func ParseScenario ¶
ParseScenario decodes YAML and validates it. Unknown fields are an error, so a typo'd key fails loudly instead of being silently ignored.
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 ¶
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 ¶
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 (*State) Alive ¶
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 ¶
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 ¶
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 TLSCertificate ¶
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
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 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)
Source Files
¶
- analysis.go
- backend.go
- campaign.go
- challenge.go
- challenge_authored.go
- challenge_daily.go
- challenge_hostname.go
- challenge_report.go
- challenge_run.go
- challenge_share.go
- challenge_starters.go
- challenge_truth.go
- compare.go
- encrypted_dns_service.go
- evidence.go
- httpconnect.go
- hunt.go
- hunt_analyze.go
- hunt_generate.go
- hunt_merge.go
- hunt_oracle.go
- hunt_report.go
- library.go
- netns_linux.go
- node_linux.go
- quic_service.go
- report.go
- route_evidence_linux.go
- runner.go
- scenario.go
- services.go
- services_errno_other.go
- socks5.go
- state.go
- state_linux.go
- timeline.go
- tls_service.go
- topology.go
- triage.go