From efce41f458a4453b0cf3ca26372267620136968e Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 16 May 2026 10:40:41 +0300 Subject: [PATCH] Prefer QUIC fabric endpoints --- .../rap-node-agent/cmd/rap-node-agent/main.go | 19 ++++++++- .../cmd/rap-node-agent/main_test.go | 7 +++- .../mesh/endpoint_candidate_scoring.go | 3 ++ .../mesh/endpoint_candidate_scoring_test.go | 39 +++++++++++++++++++ .../DISTRIBUTED_FABRIC_NODE_PROTOCOL_PLAN.md | 3 ++ 5 files changed, 69 insertions(+), 2 deletions(-) diff --git a/agents/rap-node-agent/cmd/rap-node-agent/main.go b/agents/rap-node-agent/cmd/rap-node-agent/main.go index 0e3f93b..5022220 100644 --- a/agents/rap-node-agent/cmd/rap-node-agent/main.go +++ b/agents/rap-node-agent/cmd/rap-node-agent/main.go @@ -2781,7 +2781,10 @@ func meshOutboundSessionReportFromState(cfg config.Config, meshState *syntheticM } func meshEndpointReport(cfg config.Config, identity state.Identity, meshState *syntheticMeshState, observedAt time.Time, candidates []mesh.PeerEndpointCandidate) map[string]any { - transport := cfg.MeshAdvertiseTransport + transport := strings.TrimSpace(candidates[0].Transport) + if transport == "" { + transport = strings.TrimSpace(cfg.MeshAdvertiseTransport) + } if transport == "" { transport = "direct_tcp_tls" } @@ -3842,6 +3845,20 @@ func advertisedEndpointCandidates(cfg config.Config, identity state.Identity, me Priority: 10, }) } + if cfg.MeshQUICFabricEnabled && meshState != nil && strings.TrimSpace(meshState.QUICFabricListenAddr) != "" { + candidates = append(candidates, mesh.PeerEndpointCandidate{ + EndpointID: identity.NodeID + "-quic-fabric", + NodeID: identity.NodeID, + Transport: "direct_quic", + Address: "quic://" + meshState.QUICFabricListenAddr, + Reachability: reachabilityFromConnectivityMode(cfg.MeshConnectivityMode), + NATType: cfg.MeshNATType, + ConnectivityMode: cfg.MeshConnectivityMode, + Region: cfg.MeshRegion, + Priority: 5, + PolicyTags: []string{"fast-path"}, + }) + } candidates = append(candidates, interfaceEndpointCandidates(cfg, identity, meshState, observedAt)...) for i := range candidates { if candidates[i].EndpointID == "" { diff --git a/agents/rap-node-agent/cmd/rap-node-agent/main_test.go b/agents/rap-node-agent/cmd/rap-node-agent/main_test.go index 1d27bfc..0ad6ef3 100644 --- a/agents/rap-node-agent/cmd/rap-node-agent/main_test.go +++ b/agents/rap-node-agent/cmd/rap-node-agent/main_test.go @@ -720,12 +720,17 @@ func TestHeartbeatPayloadIncludesMeshEndpointReport(t *testing.T) { if !ok { t.Fatalf("mesh endpoint report missing: %+v", payload.Metadata) } - if report["peer_endpoint"] != "https://node-a.example.test:443" || + if report["peer_endpoint"] != "quic://127.0.0.1:19443" || + report["transport"] != "direct_quic" || report["connectivity_mode"] != "outbound_only" || report["nat_type"] != "symmetric" || report["region"] != "eu" { t.Fatalf("unexpected endpoint report: %+v", report) } + candidates, ok := report["endpoint_candidates"].([]mesh.PeerEndpointCandidate) + if !ok || len(candidates) < 2 || candidates[0].Transport != "direct_quic" || candidates[1].Transport != "wss" { + t.Fatalf("unexpected endpoint candidates: %+v", report["endpoint_candidates"]) + } if payload.Capabilities["mesh_dynamic_endpoint_reporting"] != true { t.Fatalf("dynamic endpoint capability missing: %+v", payload.Capabilities) } diff --git a/agents/rap-node-agent/internal/mesh/endpoint_candidate_scoring.go b/agents/rap-node-agent/internal/mesh/endpoint_candidate_scoring.go index a25d930..3da1cff 100644 --- a/agents/rap-node-agent/internal/mesh/endpoint_candidate_scoring.go +++ b/agents/rap-node-agent/internal/mesh/endpoint_candidate_scoring.go @@ -59,6 +59,9 @@ func scorePeerEndpointCandidate(candidate PeerEndpointCandidate, opts EndpointCa reasons := []string{"base"} switch candidate.Transport { + case "quic", "direct_quic", "udp_quic", "quic_udp": + score += 45 + reasons = append(reasons, "transport:quic") case "direct_tcp_tls", "direct_http", "direct_https": score += 35 reasons = append(reasons, "transport:direct") diff --git a/agents/rap-node-agent/internal/mesh/endpoint_candidate_scoring_test.go b/agents/rap-node-agent/internal/mesh/endpoint_candidate_scoring_test.go index 4ec8d1c..ec2fbcc 100644 --- a/agents/rap-node-agent/internal/mesh/endpoint_candidate_scoring_test.go +++ b/agents/rap-node-agent/internal/mesh/endpoint_candidate_scoring_test.go @@ -99,6 +99,45 @@ func TestRankPeerEndpointCandidatesUsesDeterministicTieBreak(t *testing.T) { } } +func TestRankPeerEndpointCandidatesPrefersQUICFastPath(t *testing.T) { + now := time.Date(2026, 5, 16, 12, 0, 0, 0, time.UTC) + candidates := []PeerEndpointCandidate{ + { + EndpointID: "node-b-wss", + NodeID: "node-b", + Transport: "wss", + Address: "wss://node-b.example.test", + Reachability: "public", + NATType: "none", + ConnectivityMode: "direct", + Priority: 10, + LastVerifiedAt: &now, + }, + { + EndpointID: "node-b-quic", + NodeID: "node-b", + Transport: "direct_quic", + Address: "quic://node-b.example.test:19443", + Reachability: "public", + NATType: "none", + ConnectivityMode: "direct", + Priority: 10, + LastVerifiedAt: &now, + }, + } + ranked := RankPeerEndpointCandidates(candidates, EndpointCandidateScoreOptions{ + ChannelClass: SyntheticChannelFabricControl, + Now: now, + MaxVerificationAge: time.Minute, + }) + if ranked[0].Candidate.EndpointID != "node-b-quic" { + t.Fatalf("top endpoint = %q, want quic: %+v", ranked[0].Candidate.EndpointID, ranked) + } + if !containsReason(ranked[0].Reasons, "transport:quic") { + t.Fatalf("quic reason missing: %+v", ranked[0].Reasons) + } +} + func TestRankPeerEndpointCandidatesPrefersCorporatePrivateEndpoint(t *testing.T) { now := time.Date(2026, 4, 28, 12, 0, 0, 0, time.UTC) candidates := []PeerEndpointCandidate{ diff --git a/docs/architecture/DISTRIBUTED_FABRIC_NODE_PROTOCOL_PLAN.md b/docs/architecture/DISTRIBUTED_FABRIC_NODE_PROTOCOL_PLAN.md index 38f23ba..f9cde82 100644 --- a/docs/architecture/DISTRIBUTED_FABRIC_NODE_PROTOCOL_PLAN.md +++ b/docs/architecture/DISTRIBUTED_FABRIC_NODE_PROTOCOL_PLAN.md @@ -307,6 +307,9 @@ in heartbeat metadata, and pass the setting through host-agent install/update profiles. `mesh-live-smoke` verifies the QUIC carrier by starting a temporary QUIC fabric server and requiring a `PING`/`PONG` round trip over `QUICFabricTransport`. +Nodes now advertise enabled QUIC fabric listeners as `direct_quic` fast-path +endpoint candidates, and endpoint ranking prefers QUIC over WebSocket/HTTPS +compatibility candidates for fabric sessions. Deliverables: