Skip to content

More connectivity matrix fixes - #1933

Merged
Frostman merged 11 commits into
masterfrom
ema/third-matrix-round
Aug 10, 2026
Merged

More connectivity matrix fixes#1933
Frostman merged 11 commits into
masterfrom
ema/third-matrix-round

Conversation

@edipascale

Copy link
Copy Markdown
Contributor

No description provided.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

The PR centralizes connectivity expectation ownership, updates TCP and UDP probe handling, expands ACL coverage, improves reachability diagnostics, and adjusts ESLAG endpoint and multihomed leaf selection.

Connectivity matrix validation and probing

Layer / File(s) Summary
Expectation ownership and validation
pkg/hhfab/matrix.go, pkg/hhfab/matrix_test.go
entryOwner classifies expectations as unknown or unreadable. Validation and probe-enable checks cover server, external, NAT, port-forward, and filtered entries.
Shared probe selection and dispatch
pkg/hhfab/matrix.go
Probe phases share ownership and server-pair filtering. Listener cleanup matches exact ports. Disabled required probes fail early.
Reachability-aware TCP and UDP checks
pkg/hhfab/testing.go, pkg/hhfab/rt_nat_tests.go
TCP and UDP checks preserve reachability details, apply nested deadlines, retry unparseable UDP results, and classify inconclusive loss.
Probe command and diagnostic coverage
pkg/hhfab/testing_test.go
Tests cover BGP commands, UDP timing, and reachability explanation formatting.

ACL protocol and NAT coverage

Layer / File(s) Summary
ACL test cases and documentation
pkg/hhfab/rt_acl_tests.go, pkg/hhfab/rt_acl_tests.md
The ACL suite and coverage documentation cover numeric ICMP matching, pre-NAT destination evaluation, flow-scope NAT restrictions, and external-peering limitations.

ESLAG and multihoming support

Layer / File(s) Summary
ESLAG and L3VNI endpoint handling
pkg/hhfab/endpoints.go, pkg/hhfab/endpoints_test.go
Endpoint collection tracks ESLAG and VPC mode metadata. Missing-IP handling skips only ESLAG/L3VNI-only servers.
ESLAG fallback and candidate ordering
pkg/hhfab/rt_eslag_fallback.go, pkg/hhfab/vlabbuilder.go, cmd/hhfab/main.go
ESLAG fallback refreshes the matrix endpoint after bond restoration. Candidate construction prioritizes orphan leaves and appends ESLAG leaves when required. The CLI description permits any two different leaves.

Possibly related PRs

Suggested reviewers: copilot, frostman, pau-hedgehog

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided, so the changeset has no author-provided summary to assess. Add a concise description that summarizes the connectivity matrix, ACL, ESLAG, and diagnostic changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies connectivity matrix fixes, which are the primary focus of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR continues tightening the connectivity-matrix-driven test harness by making matrix validation stricter (rejecting expectations that no probe phase can assert), improving probe error attribution, and expanding ACL test coverage to cover additional dataplane behaviors.

Changes:

  • Adjust multihomed server leaf candidate selection to prefer orphan leaves and only “top up” with ESLAG leaves when needed.
  • Refactor connectivity matrix validation/probing by introducing a single “entry owner” (phase) classifier, adding explicit validation for unprobeable expectations, and improving TCP/UDP probe error context via Reachability.
  • Extend gateway ACL test suite/docs with numeric-protocol ICMP coverage and pre-NAT destination matching coverage; add unit tests for getServerHostBGPCmd.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pkg/hhfab/vlabbuilder.go Ensures orphan leaves are preferred for multihomed server placement, with ESLAG leaves appended only when necessary.
pkg/hhfab/testing.go Updates TCP/UDP port probes to accept Reachability and improves IperfError context (Why).
pkg/hhfab/testing_test.go Adds unit tests for host BGP command generation from server connections.
pkg/hhfab/rt_acl_tests.md Documents new ACL test cases/coverage and clarifies limitations.
pkg/hhfab/rt_acl_tests.go Adds new ACL test cases (numeric ICMP protocol; pre-NAT destination match) and reuses the persistent iperf3 port constant.
pkg/hhfab/matrix.go Adds phase ownership classification, validates unprobeable entries, anchors listener teardown pkill, and aligns phases with entryOwner.
pkg/hhfab/endpoints_test.go Updates validation tests and adds coverage for “no phase can read this entry” failures.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/hhfab/matrix.go
Copilot AI review requested due to automatic review settings August 3, 2026 16:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@edipascale edipascale added ci:+release Enable VLAB release tests ci:+hlab Enable hybrid VLAB tests labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Release Tests

 12 files  ± 0   48 suites  ±0   4h 53m 17s ⏱️ - 21m 11s
 57 tests + 2   55 ✅ + 2    2 💤 ±0  0 ❌ ±0 
684 runs  +24  311 ✅ +20  373 💤 +4  0 ❌ ±0 

Results for commit 48c69e9. ± Comparison against base commit 1d27ea7.

♻️ This comment has been updated with latest results.

Base automatically changed from ema/test-connectivity-review to master August 4, 2026 05:50
Copilot AI review requested due to automatic review settings August 4, 2026 08:13
@edipascale
edipascale force-pushed the ema/third-matrix-round branch from 8023e3d to ca75c69 Compare August 4, 2026 08:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment thread pkg/hhfab/rt_eslag_fallback.go
@edipascale edipascale self-assigned this Aug 4, 2026
@edipascale
edipascale marked this pull request as ready for review August 4, 2026 13:44
@edipascale
edipascale requested review from a team as code owners August 4, 2026 13:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
pkg/hhfab/endpoints_test.go (1)

319-326: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the required unprobed classification.

The endpoint label also appears in the unknown-entry error. A regression that returns could not be evaluated instead of no probe phase can read still passes this case. Store the result and assert the required classification.

Proposed fix
-		require.ErrorContains(t, m.Validate(), "server-1(vpc-1/default) → server-2(vpc-2/default)")
+		err = m.Validate()
+		require.ErrorContains(t, err, "no probe phase can read")
+		require.ErrorContains(t, err, "server-1(vpc-1/default) → server-2(vpc-2/default)")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/hhfab/endpoints_test.go` around lines 319 - 326, Update the final
m.Validate assertion in the TranslatedAddress case to store the returned error
and assert that it contains both the endpoint label and the required “no probe
phase can read” unprobed classification, rather than relying only on the
endpoint text.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/hhfab/matrix.go`:
- Around line 319-321: Add an ownership check to the condition that applies the
external Allow shortcut. The existing condition checks e.Pair.Destination,
e.Pair.Destination.External, and extAllowBySource[e.Pair.Source], but it should
also verify that the entry owner equals matrixPhaseCurl before continuing. This
ensures the shortcut only suppresses validation for curl-owned entries with
VerdictUnknown, not for unrelated external DNAT entries that happen to share a
source with an unscoped external Allow.
- Around line 373-376: Update Validate() to reject entries that simultaneously
define ProtoPort and a nonzero NAT.DestinationPort, including the inherited NAT
overlay case; return the existing validation error format for invalid
combinations. Preserve entryOwner() precedence only for valid, non-overlapping
entries.

---

Nitpick comments:
In `@pkg/hhfab/endpoints_test.go`:
- Around line 319-326: Update the final m.Validate assertion in the
TranslatedAddress case to store the returned error and assert that it contains
both the endpoint label and the required “no probe phase can read” unprobed
classification, rather than relying only on the endpoint text.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b7b6827a-1c23-4575-b4fe-33ebe46b8b66

📥 Commits

Reviewing files that changed from the base of the PR and between 01803ea and ca75c69.

📒 Files selected for processing (8)
  • pkg/hhfab/endpoints_test.go
  • pkg/hhfab/matrix.go
  • pkg/hhfab/rt_acl_tests.go
  • pkg/hhfab/rt_acl_tests.md
  • pkg/hhfab/rt_eslag_fallback.go
  • pkg/hhfab/testing.go
  • pkg/hhfab/testing_test.go
  • pkg/hhfab/vlabbuilder.go

Comment thread pkg/hhfab/matrix.go Outdated
Comment thread pkg/hhfab/matrix.go
@edipascale
edipascale marked this pull request as draft August 4, 2026 14:24
Copilot AI review requested due to automatic review settings August 4, 2026 15:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (1)

pkg/hhfab/matrix.go:410

  • hasEntriesOwnedBy only scans explicitly added expectations in m.entries, but the probe phases also act on implicit default expectations returned by Lookup (e.g., default-deny server→server and server→external pairs). As a result, TestConnectivityWithMatrix can allow pings/iperfs/curls to be disabled even though those phases would still have work (and would otherwise pass those implicit expectations unasserted).
func (m *ConnectivityMatrix) hasEntriesOwnedBy(p matrixPhase) bool {
	for _, byPP := range m.entries {
		for _, e := range byPP {
			if m.entryOwner(e) == p {
				return true

@edipascale
edipascale marked this pull request as ready for review August 4, 2026 16:49
Copilot AI review requested due to automatic review settings August 6, 2026 09:31
@edipascale
edipascale force-pushed the ema/third-matrix-round branch from 6628df4 to b9c51d5 Compare August 6, 2026 09:31
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings August 6, 2026 10:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/hhfab/endpoints.go`:
- Around line 201-202: Update the HostBGP attachment deduplication flow around
the attachment collection and metadata construction so duplicate attachments
preserve metadata from every raw VPCAttachment: merge l3vni with logical AND
(and retain the ESLAG exemption only when appropriate) before applying the no-IP
suppression at the line-250 path. Ensure the resulting behavior is independent
of VPCAttachmentList.Items order, and add regression coverage for both
attachment orders.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7733d4ca-a1a5-41e8-b774-869a6a44266c

📥 Commits

Reviewing files that changed from the base of the PR and between b9c51d5 and e6df485.

📒 Files selected for processing (2)
  • pkg/hhfab/endpoints.go
  • pkg/hhfab/endpoints_test.go

Comment thread pkg/hhfab/endpoints.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (3)

pkg/hhfab/matrix.go:537

  • probesServerPair() claims to match what the server-to-server phases will probe, but it doesn’t exclude self-pairs or same-node pairs. Those are skipped by entryOwner(), so this helper can return true for pairs that will never be probed (and may lead to extra iteration / future inconsistencies).
// probesServerPair reports whether the server-to-server phases will probe this
// pair, so listener setup covers exactly what gets probed.
func (d *matrixTestDeps) probesServerPair(src, dst *Endpoint) bool {
	return src.Server != nil && dst.Server != nil &&
		d.inSources(src.Server.Name) && d.inDestinations(dst.Server.Name)

pkg/hhfab/matrix.go:855

  • The “phase has no enabled probe” guards use matrix.hasEntriesOwnedBy(...), which doesn’t consider opts.Sources/opts.Destinations. This can incorrectly fail runs where the caller filters out all server-to-server (or curl) entries, even though nothing in that phase would actually be probed.
	// checkPing/checkIPerf/checkCurl no-op when their count is off, so a phase
	// left with no enabled probe would pass its entries unasserted
	if opts.PingsCount <= 0 && opts.IPerfsSeconds <= 0 && matrix.hasEntriesOwnedBy(matrixPhaseServerServer) {
		return fmt.Errorf("matrix has server-to-server entries but both pings and iperfs are disabled") //nolint:goerr113
	}

pkg/hhfab/endpoints.go:110

  • The new naming is misleading: serverAttachment.l3vni is set via vpc.Spec.Mode != vpcapi.VPCModeL2VNI, which includes both l3vni and l3flat modes, and eslagL3VNIOnly() similarly tests “non-L2VNI” rather than L3VNI specifically. Consider renaming to reflect the actual semantics (e.g., l3Mode / eslagNonL2VNIOnly) to avoid confusion as more VPC modes exist.
	eslag      bool
	l3vni      bool
	attachName string // for diagnostics
}

// eslagL3VNIOnly reports whether every one of the server's attachments is an
// ESLAG connection into a non-L2VNI VPC. Those servers never run hhnet, so
// discovering no addresses on them is expected rather than a failure.
func eslagL3VNIOnly(atts []serverAttachment) bool {
	if len(atts) == 0 {
		return false
	}

	return !slices.ContainsFunc(atts, func(a serverAttachment) bool {
		return !a.eslag || !a.l3vni
	})
}

pau-hedgehog
pau-hedgehog previously approved these changes Aug 7, 2026

@pau-hedgehog pau-hedgehog left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, this closes almost everything from the last round plus the two issues that came out of reviewing it.

One defect worth fixing: the new disabled-probe guard counts stored entries while the server-to-server phase probes synthesized ones, so it can pass while asserting nothing. One question on the UDP connect budget depending on the expectation. The rest are comments and one-liners.

I ran two mutations on env-1 to check the ACL suite fails when it should: breaking the ACL config fails Default Deny through all three probe types both ways, and flipping one tcp verdict fails exactly one probe in one direction. Worth knowing it has teeth, and maybe worth making permanent.

The red v-mesh-gw job looks like the #1929/#1937/#1924 single-packet-loss family: one lost ping on an expected-allow switch-peering pair, nothing to do with the ACL or matrix paths. Worth rebasing onto master before merge so CI re-runs and we get a clean board rather than merging with a red check.

Nits, batched here since several sit on lines this PR doesn't touch:

  • overlayExternalNoEgress still writes a bare {Pair, Verdict: VerdictDeny} with no Reason, Peering or Detail, so a curl failure on one of those pairs prints "because: no reason recorded" in the branch whose parent added the because clause. One Detail line.
  • The --multihomed-servers usage string still says "2 connections to 2 different orphan leaves" while 94fe6f2 makes ESLAG leaves the documented fallback. The struct comment was fixed in #1910, this one was missed.
  • TestValidate is still in endpoints_test.go and this PR adds 40 more lines to it. Cheaper to move to matrix_test.go now than later.
  • checkUDPPort's dead band: "UDP traffic was not blocked" is a real improvement over claiming delivery, but it still doesn't say the result was inconclusive or print the two thresholds.
  • The UDP retry doubles the pathological worst case to 2 x outer, ~96s for a deny probe at release defaults, serialized behind the weight-1 semaphore. The expected case is far better and the retry is the right call; just noting the tail.

Comment thread pkg/hhfab/matrix.go Outdated
Comment thread pkg/hhfab/testing.go
Comment thread pkg/hhfab/matrix.go Outdated
Comment thread pkg/hhfab/matrix.go
Copilot AI review requested due to automatic review settings August 7, 2026 12:13
@edipascale
edipascale force-pushed the ema/third-matrix-round branch from e6df485 to 78e59df Compare August 7, 2026 12:13
@edipascale

edipascale commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

@pau-hedgehog I rebased on master and pushed two more commits on top of the old ones; the first addresses your comments (except for the UDP connection timeout, until we decide what to do), the second improves the because clause for ACLs. I want to avoid a fourth round of matrix fixes if we can, so let's try and fix any remaining issue here

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Suppressed comments (2)

pkg/hhfab/matrix.go:351

  • The doc comment on entryOwner is misleading: Validate calls entryOwner before it reports VerdictUnknown entries, and entryOwner itself has VerdictUnknown-specific logic (e.g. external Curl allow short-circuit). The comment currently states Validate has already rejected VerdictUnknown, which isn’t true and may confuse future changes (e.g. someone removing the VerdictUnknown branch as “dead”).
// entryOwner assumes Validate has already rejected VerdictUnknown: the phases
// carry no verdict guard of their own, and reachabilityFromExpectation maps
// Unknown to Reachable:false, so a phase reached without Validate would assert
// deny on an entry explicitly marked unevaluable.

pkg/hhfab/endpoints.go:110

  • Function/field naming doesn’t match the behavior described in the comment: eslagL3VNIOnly returns true for any attachment where VPC mode is “non-L2VNI” (endpoints.go:202 sets l3vni as Mode != VPCModeL2VNI), which includes L3Flat as well as L3VNI (see other code paths checking VPCModeL3VNI || VPCModeL3Flat). Consider renaming the boolean (e.g. l3Mode) and the helper (e.g. eslagL3ModeOnly) to avoid implying it only applies to L3VNI.
// eslagL3VNIOnly reports whether every one of the server's attachments is an
// ESLAG connection into a non-L2VNI VPC. Those servers never run hhnet, so
// discovering no addresses on them is expected rather than a failure.
func eslagL3VNIOnly(atts []serverAttachment) bool {
	if len(atts) == 0 {
		return false
	}

	return !slices.ContainsFunc(atts, func(a serverAttachment) bool {
		return !a.eslag || !a.l3vni
	})
}

@pau-hedgehog pau-hedgehog left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebase looks good, and thanks for turning these around so fast.

checkProbesEnabled closes the guard properly: it walks the same pairs the phases do, including the Deny that Lookup synthesizes where populate stored nothing, and honouring the source/destination filters covers the ownership-is-not-probed gap as well. TestCheckProbesEnabled asserting the guard rather than the walk is the right shape.

Also reviewed 78e59df on top. expectationWhy keeps the allow path byte-identical and only changes the deny phrasing, TestExpectationWhy covers every branch of it, and each current caller of setVPCToVPCProtoVerdict is an ACL context so labelling a proto-scoped deny "ACL" holds for all of them. "ACL on gateway-peering ..." is a real improvement on a failure line, that phrasing bothered me in the mutation runs too.

UDP connect budget settled in-thread: keeping 5s, revisit if we ever see a deny probe time out on a path we expect to be open.

Nothing else from me.

edipascale and others added 11 commits August 10, 2026 09:18
Three places decided which phase reads an entry: runMatrixServerServerPhase
skipped a pair carrying proto-port entries or a DNAT'd port,
runMatrixPortForwardPhase claimed an entry with a valid NAT (ip, port), and
defaultEntryShadowed re-derived the first of those for Validate. They agreed,
but any edit to one gate could silently desynchronize Validate from the phase
it certifies — which is how the shadowed-entry bug arose in the first place.

Replace all of it with entryOwner(), consulted by the four phases and by
Validate. Two gaps close as a consequence:

- a proto-scoped entry towards an external is read by nothing.
  runMatrixProtoPortPhase probes server destinations only, and the external
  oracle is one untargeted curl per source server. Validate now rejects such
  an entry instead of letting it report green while asserting nothing, which
  is what an ACL case on an external peering would have done.
- an entry with a translated port but no valid destination IP was claimed by
  no phase, so unless its verdict was Unknown it went untested. It is now
  unprobeable by construction and Validate says so, before any SSH runs.

The proto-port phase and its listener setup also go through entryOwner, so a
multi-attached server no longer gets an on-demand iperf3 listener started and
torn down on a same-node pair that no probe visits.

Separately, on probe reporting:

- checkTCPPort and checkUDPPort took a bool, so neither could set
  IperfError.Why and the ACL cases were the only failures in the suite
  without a "because: ..." suffix. They now take a Reachability like
  checkPing and checkCurl do, which also drops the workaround in
  runMatrixIperfPortForward that patched Why after the call.
- UDP loss between the allow and deny thresholds is neither delivered nor
  blocked, so an expected deny fails there — correctly, but the message
  claimed datagrams were delivered, pointing at a dataplane bug when the
  path is merely lossy.
- an icmp expectation is unassertable when pings are disabled, since
  checkPing no-ops at count 0. Report it rather than passing silently.
- anchor the listener teardown's pkill pattern so port 5301 cannot match a
  listener on 53010.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Emanuele Di Pascale <emanuele@githedgehog.com>
MultiHomedServers is documented as taking two connections to two different
leaves, "preferably orphans", and ESLAG leaves are only added to the candidate
list when there are fewer than two orphans. But they were appended before the
orphan loop ran, so with exactly one orphan the first multihomed server took
two ESLAG leaves and the orphan went unused.

Collect them separately and append after the orphan loop, so the round-robin
reaches an ESLAG leaf only once the orphans are exhausted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Emanuele Di Pascale <emanuele@githedgehog.com>
Two dimensions the ACL suite asserted nothing about.

Every ICMP expectation so far rested on ICMP falling through to the default
action, since it matches neither a tcp nor a udp rule. The dataplane has no
icmp keyword yet, but a numeric protocol is accepted, so a rule with
proto "1" is the first case asserting that a rule can match ICMP at all.

ACL rules are evaluated before NAT, so match.dst is compared against the
address the initiator dialed — the peer's advertised expose "as" pool — not
the destination's native IP. No case exercised that: nine configure no NAT and
the masquerade one has no match block. Pair bidirectional static NAT with
rules matching the peer's pool; an implementation matching post-NAT would deny
everything, so the all-allow result is the assertion.

Also documents in rt_acl_tests.md that ACLs on external peerings cannot be
asserted with the probes available, and that the flow-scope-needs-NAT
restriction is temporary per docs#338 — once flow works without NAT it becomes
the CRD default, and every case here uses explicit packet scope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Emanuele Di Pascale <emanuele@githedgehog.com>
The helper is pure and had no test, and its multi-VPC loop has no caller until
the multi-VPC hostBGP work lands, so nothing pinned the emitted string when it
moved from a literal "vpc" token to the VPC name. Cover the single-param form,
the ServerOffset walk, the two-param concatenation, and the error paths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Emanuele Di Pascale <emanuele@githedgehog.com>
runMatrixProtoPortPhase honors the Sources/Destinations filters, but
startMatrixProtoPortListeners only skipped non-server sources, so it computed
its listener set over a wider pair set than the phase probes. With either
filter set, that starts and tears down an on-demand iperf3 listener on
destinations no probe visits.

Dormant today — every matrix caller passes tcOpts, which never sets either
field, and the CLI flags feed the legacy path — but the fields exist for this
one to use.

State the pair condition once on matrixTestDeps and have both loops use it, so
the listener set cannot drift from the probe set again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Emanuele Di Pascale <emanuele@githedgehog.com>
similarly to what happens in the static DHCP test, we are
getting a new lease via hhnet bond on the eslag server,
which might return a different IP. Update the matrix to make
sure we have the correct information

Signed-off-by: Emanuele Di Pascale <emanuele@githedgehog.com>
Validate was re-deriving what the curl phase can assert, and stated it
for every external destination rather than for curl-owned entries only.
An Unknown external port-forward entry therefore escaped Validate and
was then skipped by its own phase, asserting nothing.

Fold all three judgments into entryOwner so Validate becomes a plain
three-way split on the owner, and reject two more entries no probe can
read: ProtoPort combined with a translated port, which the proto-port
probes would aim past, and an external Allow behind a NAT with no
source pool, which the curl oracle discards while the entry claims
reachability. Neither is produced today, but setVPCToVPCProtoVerdict
inherits NAT from the pair's default entry, so the first is one overlay
away.

Since Validate aborts the run, no Unknown entry can reach a phase any
more; drop the port-forward phase's now-dead verdict guard.

Also reject runs where a phase has entries but all of its probes are
knob-disabled no-ops: only checkUDPPort defaults its duration, so
--pings=0 --iperfs=0 left every stated server-to-server expectation
unasserted while Validate reported the oracle sound.

Signed-off-by: Emanuele Di Pascale <emanuele@githedgehog.com>
A denied path neither completes nor refuses the iperf3 -u control
connect, so the probe ran until its own `timeout` backstop killed it,
leaving 5s for the report to come back over SSH. Overrun that and the
unparseable report was returned as a hard error before the deny
expectation was ever consulted, failing a correctly denied path.

Bound the connect with iperf3's --connect-timeout and size the
deadlines off it: 5s for deny probes (an open path handshakes in
milliseconds), 15s for allow probes, `timeout -k 5` as a pure
backstop, and 30s of SSH headroom. Retry once when no report comes
back, since that is a stall on the SSH/docker path rather than a
verdict on the path under test.

Deny probes drop from ~28.6s to ~5.5s at release defaults.

Fix #1944

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Emanuele Di Pascale <emanuele@githedgehog.com>
CollectServerEndpoints skipped any server whose address discovery came
back empty without recording a drop, so the matrix lost that server and
all of its attachments while Validate still called the oracle sound.
The comment named the exempt case, ESLAG servers in L3VNI mode which
never run hhnet, but the condition tested only that no addresses were
found: a server whose DHCP or hhnet failed took the same silent path.
That is the wrong way round, since a server with one address matching
nothing does stop the run.

Carry the two facts the comment describes on each candidate attachment,
taken from the attachment's own connection and VPC, and exempt only a
server whose every attachment is ESLAG into a non-L2VNI VPC. Any other
server with no addresses now falls through to the existing per-
attachment loop and is recorded as dropped, which Validate refuses to
run against.

Fix #1945

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Emanuele Di Pascale <emanuele@githedgehog.com>
hasEntriesOwnedBy iterated m.entries, but no phase works from stored
entries: they walk endpoint pairs and call Lookup, which synthesizes a
Deny where nothing is stored. populate only stores reachable or
unevaluable pairs, so an all-deny topology had zero stored
server-to-server entries while the phase still probed every pair. With
--pings 0 --iperfs 0 the guard passed, checkPing returned nil and every
deny expectation passed unasserted, which is what the guard was for.

Replace it with checkProbesEnabled, which walks the same pairs the phases
do and honors the --source/--destination filters they also apply. It owns
all three thresholds, so the icmp check moves out of
runMatrixProtoPortPhase and into the same pre-flight pass: it is
answerable from the matrix alone, and it no longer fires after setup and
listener start, once per entry. TestCheckProbesEnabled asserts the guard
rather than the walk, so reintroducing the stored-entry scan fails it.

Also from review:

- state on matrixPhase that ownership is not the same as being probed,
  since Validate does not model the source/destination filters, and on
  entryOwner that the phases depend on Validate having rejected
  VerdictUnknown first
- give deny expectations a real explanation instead of "no reason
  recorded"; ReachabilityReason only ever explains an allow, so the
  absence of an allowing peering is itself the reason
- distinguish the UDP dead band from genuine delivery and name both
  thresholds
- fix the --multihomed-servers usage string, which still promised orphan
  leaves after ESLAG leaves became the documented fallback
- move TestValidate to matrix_test.go

Signed-off-by: Emanuele Di Pascale <emanuele@githedgehog.com>
An ACL deny printed "because: gateway-peering <name>", which reads as if
the peering explained the block when it is the thing being blocked. The
reason alone cannot say more: ReachabilityReason only ever names the
peering that would allow a pair, so a deny has to be phrased against it.

Reuse Detail, which already carries free-form context for Reason, to say
what withholds the pair, and render it as "<detail> on <reason>
<peering>". A proto-scoped deny is exactly an ACL decision — that is what
makes a verdict protocol/port-dependent, as setVPCToVPCProtoVerdict's own
doc notes — so it records "ACL" and the clause becomes "because: ACL on
gateway-peering <name>".

Denies that carry a peering but no detail (overrideVPCToVPCVerdict) now
read "<reason> <peering> does not allow it" rather than borrowing the
allow phrasing, and a pair no peering covers still reports "no peering
allows it".

Signed-off-by: Emanuele Di Pascale <emanuele@githedgehog.com>
@edipascale
edipascale force-pushed the ema/third-matrix-round branch from 78e59df to 48c69e9 Compare August 10, 2026 07:19
@Frostman
Frostman merged commit 1ec4d33 into master Aug 10, 2026
50 of 52 checks passed
@Frostman
Frostman deleted the ema/third-matrix-round branch August 10, 2026 16:51
@coderabbitai coderabbitai Bot mentioned this pull request Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:+hlab Enable hybrid VLAB tests ci:+release Enable VLAB release tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants