This commit is contained in:
2026-05-14 23:30:34 +03:00
parent 26cb65e936
commit 04c46042d9
239 changed files with 34102 additions and 438 deletions
+25 -16
View File
@@ -2144,22 +2144,23 @@ type SetFabricEgressPoolNodeInput struct {
}
type IssueFabricServiceChannelLeaseInput struct {
ActorUserID string
ClusterID string
OrganizationID string
UserID string
ResourceID string
ServiceClass string
EntryNodeIDs []string
ExitNodeIDs []string
PreferredEntryNodeID string
PreferredExitNodeID string
RequiredRoles []string
AllowedChannels []string
QoS json.RawMessage
Failover json.RawMessage
Metadata json.RawMessage
TTL time.Duration
ActorUserID string
ClusterID string
OrganizationID string
UserID string
ResourceID string
ServiceClass string
EntryNodeIDs []string
ExitNodeIDs []string
PreferredEntryNodeID string
PreferredExitNodeID string
RequiredRoles []string
AllowedChannels []string
QoS json.RawMessage
Failover json.RawMessage
Metadata json.RawMessage
TTL time.Duration
BackendFallbackAllowed *bool
}
type UpdateFabricServiceChannelPoolPolicyInput struct {
@@ -2531,6 +2532,14 @@ type RenewNodeVPNAssignmentLeaseInput struct {
TTL time.Duration
}
type AcquireNodeVPNAssignmentLeaseInput struct {
ClusterID string
VPNConnectionID string
OwnerNodeID string
TTL time.Duration
Metadata json.RawMessage
}
type ReleaseVPNConnectionLeaseInput struct {
ActorUserID string
ClusterID string
@@ -147,6 +147,7 @@ func (m *Module) RegisterRoutes(router chi.Router) {
r.Post("/{clusterID}/vpn-connections/{vpnConnectionID}/leases/{leaseID}/release", m.releaseVPNConnectionLease)
r.Post("/{clusterID}/vpn-connections/{vpnConnectionID}/leases/{leaseID}/fence", m.fenceVPNConnectionLease)
r.Get("/{clusterID}/nodes/{nodeID}/vpn/assignments", m.listNodeVPNAssignments)
r.Post("/{clusterID}/nodes/{nodeID}/vpn/assignments/{vpnConnectionID}/lease/acquire", m.acquireNodeVPNAssignmentLease)
r.Post("/{clusterID}/nodes/{nodeID}/vpn/assignments/{vpnConnectionID}/lease/{leaseID}/renew", m.renewNodeVPNAssignmentLease)
r.Post("/{clusterID}/nodes/{nodeID}/vpn/assignments/{vpnConnectionID}/status", m.reportNodeVPNAssignmentStatus)
r.Get("/{clusterID}/vpn-connections/{vpnConnectionID}/tunnel/stats", m.getVPNPacketStats)
@@ -2072,6 +2073,35 @@ func (m *Module) listNodeVPNAssignments(w http.ResponseWriter, r *http.Request)
httpx.WriteJSON(w, http.StatusOK, map[string]any{"vpn_assignments": items})
}
func (m *Module) acquireNodeVPNAssignmentLease(w http.ResponseWriter, r *http.Request) {
var payload struct {
TTLSeconds int `json:"ttl_seconds"`
Metadata json.RawMessage `json:"metadata"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
httpx.WriteError(w, http.StatusBadRequest, "invalid vpn node lease acquire payload")
return
}
item, err := m.service.AcquireNodeVPNAssignmentLease(r.Context(), AcquireNodeVPNAssignmentLeaseInput{
ClusterID: chi.URLParam(r, "clusterID"),
VPNConnectionID: chi.URLParam(r, "vpnConnectionID"),
OwnerNodeID: chi.URLParam(r, "nodeID"),
TTL: time.Duration(payload.TTLSeconds) * time.Second,
Metadata: payload.Metadata,
})
if writeServiceError(w, err) {
return
}
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"lease": NodeVPNAssignmentLease{
LeaseID: item.ID,
OwnerNodeID: item.OwnerNodeID,
LeaseGeneration: item.LeaseGeneration,
Status: item.Status,
RenewedAt: item.RenewedAt,
ExpiresAt: item.ExpiresAt,
}})
}
func (m *Module) renewNodeVPNAssignmentLease(w http.ResponseWriter, r *http.Request) {
var payload struct {
TTLSeconds int `json:"ttl_seconds"`
@@ -4758,7 +4758,6 @@ func (s *PostgresStore) vpnEntryEndpointCandidates(ctx context.Context, clusterI
}
func vpnEntryEndpointCandidatesFromHeartbeat(nodeID string, capabilities json.RawMessage, metadata json.RawMessage) []map[string]any {
localGatewayShortcut := heartbeatCapabilityEnabled(capabilities, "vpn_local_gateway_shortcut")
var payload struct {
MeshEndpointReport struct {
PeerEndpoint string `json:"peer_endpoint"`
@@ -4823,9 +4822,6 @@ func vpnEntryEndpointCandidatesFromHeartbeat(nodeID string, capabilities json.Ra
if apiBaseURL := vpnEntryAPIBaseURL(address); apiBaseURL != "" {
item["api_base_url"] = apiBaseURL
}
if localGatewayShortcut {
item["local_gateway_shortcut"] = true
}
out = append(out, item)
}
if len(out) == 0 {
@@ -4847,9 +4843,6 @@ func vpnEntryEndpointCandidatesFromHeartbeat(nodeID string, capabilities json.Ra
if apiBaseURL := vpnEntryAPIBaseURL(address); apiBaseURL != "" {
item["api_base_url"] = apiBaseURL
}
if localGatewayShortcut {
item["local_gateway_shortcut"] = true
}
out = append(out, item)
}
}
@@ -5129,10 +5122,15 @@ func enrichVPNClientFabricRoute(item VPNClientConnection, preferredEntryNodeID,
cfg["vpn_fabric_route"] = map[string]any{
"schema_version": "rap.vpn_fabric_route.v1",
"status": status,
"preferred_data_plane": "fabric_mesh",
"fallback_data_plane": "backend_relay",
"backend_relay_fallback": true,
"selection_mode": "entry_to_fastest_exit",
"preferred_data_plane": "fabric_service_channel",
"fallback_data_plane": "none",
"backend_relay_fallback": false,
"selection_mode": "farm_authoritative_entry_to_exit",
"route_authority": "fabric_farm",
"vpn_builds_routes": false,
"vpn_builds_tunnels": false,
"farm_builds_routes": true,
"farm_builds_tunnels": true,
"entry_pool_node_ids": entryPool,
"exit_pool_node_ids": exitPool,
"selected_entry_node_id": selectedEntry,
@@ -5147,20 +5145,28 @@ func enrichVPNClientFabricRoute(item VPNClientConnection, preferredEntryNodeID,
"tunnel_type": "universal_ip_packet",
"application_protocol_agnostic": true,
"packet_forwarding_channel": "vpn_packet",
"control_plane_packet_relay_mode": "lab_fallback_only",
"control_plane_packet_relay_mode": "fabric_service_channel_only",
"route_authority": "fabric_farm",
"backend_relay_allowed": false,
"requires_fabric_service_channel": true,
"vpn_builds_routes": false,
"vpn_builds_tunnels": false,
"farm_builds_routes": true,
"farm_builds_tunnels": true,
"traffic_contract": map[string]any{
"all_ip_traffic": true,
"protocol_specific_routing": false,
"diagnostics_only_protocol_summaries": true,
},
"route_selection": map[string]any{
"mode": "lowest_latency_healthy_route",
"mode": "farm_authoritative_lowest_latency_healthy_route",
"selected_entry_node_id": selectedEntry,
"selected_exit_node_id": selectedExit,
"route_candidates": routeCandidates,
},
"failover": map[string]any{
"enabled": true,
"owner": "fabric_farm",
"client_topology_hidden": true,
"preserve_vpn_connection_id": true,
"alternate_route_count": alternateVPNRouteCount(routeCandidates, selectedEntry, selectedExit),
@@ -5178,8 +5184,8 @@ func enrichVPNClientFabricRoute(item VPNClientConnection, preferredEntryNodeID,
"drop_policy": "drop_only_when_all_routes_unavailable_or_queue_full",
"bulk_and_realtime": "same_packet_path",
"flow_isolation": "hash_by_ip_protocol_and_ports",
"target_dataplane": "entry_node_to_exit_node_fabric",
"temporary_fallback": "backend_http_packet_relay",
"target_dataplane": "fabric_farm_entry_to_exit_service_channel",
"temporary_fallback": "none",
},
}
out, err := json.Marshal(cfg)
@@ -52,7 +52,7 @@ func TestEnrichVPNClientFabricRoutePrefersPlacementEntryAndActiveExit(t *testing
if !ok {
t.Fatalf("missing vpn_fabric_route in %#v", cfg)
}
if route["preferred_data_plane"] != "fabric_mesh" || route["fallback_data_plane"] != "backend_relay" {
if route["preferred_data_plane"] != "fabric_service_channel" || route["fallback_data_plane"] != "none" || route["backend_relay_fallback"] != false {
t.Fatalf("unexpected data-plane route contract: %#v", route)
}
if route["selected_entry_node_id"] != "entry-2" || route["selected_exit_node_id"] != "exit-active" {
@@ -158,8 +158,8 @@ func TestEnrichVPNClientEntryEndpointCandidatesAddsReportedEntryAPI(t *testing.T
if candidate["node_id"] != "entry-1" || candidate["api_base_url"] != "http://entry.example.test:19131/api/v1" {
t.Fatalf("unexpected endpoint candidate: %#v", candidate)
}
if candidate["local_gateway_shortcut"] != true {
t.Fatalf("local gateway shortcut missing: %#v", candidate)
if _, ok := candidate["local_gateway_shortcut"]; ok {
t.Fatalf("local gateway shortcut must not be advertised in farm-owned VPN mode: %#v", candidate)
}
if candidate["selected_entry"] != true || candidate["source"] != "node_latest_heartbeat.mesh_endpoint_report.endpoint_candidates" {
t.Fatalf("unexpected endpoint metadata: %#v", candidate)
+146 -53
View File
@@ -4015,8 +4015,8 @@ func (s *Service) IssueFabricServiceChannelLease(ctx context.Context, input Issu
if ttl <= 0 {
ttl = time.Minute
}
if ttl > 5*time.Minute {
ttl = 5 * time.Minute
if ttl > 6*time.Hour {
ttl = 6 * time.Hour
}
now := s.now().UTC()
expiresAt := now.Add(ttl)
@@ -4031,6 +4031,9 @@ func (s *Service) IssueFabricServiceChannelLease(ctx context.Context, input Issu
return FabricServiceChannelLease{}, err
}
poolPolicy := fabricServiceChannelPoolPolicyFromCluster(cluster)
if input.BackendFallbackAllowed != nil {
poolPolicy.BackendFallbackAllowed = *input.BackendFallbackAllowed
}
entryNodeIDs := fabricServiceChannelEffectivePool(input.EntryNodeIDs, poolPolicy.EntryPoolNodeIDs)
exitNodeIDs := fabricServiceChannelEffectivePool(input.ExitNodeIDs, poolPolicy.ExitPoolNodeIDs)
if len(entryNodeIDs) == 0 || len(exitNodeIDs) == 0 {
@@ -7303,7 +7306,9 @@ func (s *Service) GetNodeSyntheticMeshConfig(ctx context.Context, input GetNodeS
if feedback, ok := serviceChannelFeedback[route.RouteID]; ok && feedback.Fenced {
replacementDecision := s.serviceChannelRouteReplacementDecision(input, route, intents, serviceChannelFeedback, cfg.ConfigVersion)
routePathDecisions = append(routePathDecisions, replacementDecision)
continue
if replacementDecision.DecisionSource != "service_channel_feedback_no_alternate_keep_primary" {
continue
}
}
reportedPeers, reportedCandidates, err := s.reportedEndpointConfig(ctx, input.ClusterID, input.NodeID, route.Hops, localPerspective)
if err != nil {
@@ -8700,6 +8705,98 @@ func (s *Service) RenewNodeVPNAssignmentLease(ctx context.Context, input RenewNo
return item, nil
}
func (s *Service) AcquireNodeVPNAssignmentLease(ctx context.Context, input AcquireNodeVPNAssignmentLeaseInput) (VPNConnectionLease, error) {
input.ClusterID = strings.TrimSpace(input.ClusterID)
input.VPNConnectionID = strings.TrimSpace(input.VPNConnectionID)
input.OwnerNodeID = strings.TrimSpace(input.OwnerNodeID)
if input.ClusterID == "" || input.VPNConnectionID == "" || input.OwnerNodeID == "" {
return VPNConnectionLease{}, ErrInvalidPayload
}
conn, err := s.store.GetVPNConnection(ctx, input.ClusterID, input.VPNConnectionID)
if errors.Is(err, pgx.ErrNoRows) {
return VPNConnectionLease{}, ErrInvalidVPNConnection
}
if err != nil {
return VPNConnectionLease{}, err
}
if conn.Mode != VPNConnectionModeSingleActive || conn.DesiredState != VPNConnectionDesiredEnabled {
return VPNConnectionLease{}, errors.New("vpn connection must be enabled single_active before lease acquisition")
}
if err := s.ensureVPNLeaseOwnerEligible(ctx, input.ClusterID, input.VPNConnectionID, input.OwnerNodeID); err != nil {
return VPNConnectionLease{}, err
}
assignments, err := s.store.ListNodeVPNAssignments(ctx, input.ClusterID, input.OwnerNodeID)
if err != nil {
return VPNConnectionLease{}, err
}
visibleCandidate := false
for _, assignment := range assignments {
if assignment.VPNConnectionID != input.VPNConnectionID {
continue
}
if assignment.DesiredState != "" && assignment.DesiredState != VPNConnectionDesiredEnabled {
return VPNConnectionLease{}, ErrVPNLeaseOwnerNotAllowed
}
if assignment.AssignmentReason == "active_owner" &&
assignment.ActiveLease != nil &&
assignment.ActiveLease.OwnerNodeID == input.OwnerNodeID {
return VPNConnectionLease{
ID: assignment.ActiveLease.LeaseID,
VPNConnectionID: assignment.VPNConnectionID,
ClusterID: assignment.ClusterID,
OwnerNodeID: assignment.ActiveLease.OwnerNodeID,
LeaseGeneration: assignment.ActiveLease.LeaseGeneration,
Status: assignment.ActiveLease.Status,
RenewedAt: assignment.ActiveLease.RenewedAt,
ExpiresAt: assignment.ActiveLease.ExpiresAt,
}, nil
}
if assignment.AssignmentReason == "eligible_candidate" {
visibleCandidate = true
break
}
}
if !visibleCandidate {
return VPNConnectionLease{}, ErrVPNLeaseOwnerNotAllowed
}
if input.TTL <= 0 {
input.TTL = 2 * time.Minute
}
input.Metadata = defaultJSON(input.Metadata, `{}`)
if !json.Valid(input.Metadata) {
return VPNConnectionLease{}, errors.New("lease metadata must be valid json")
}
token, err := generateFencingToken()
if err != nil {
return VPNConnectionLease{}, err
}
item, err := s.store.AcquireVPNConnectionLease(ctx, AcquireVPNConnectionLeaseInput{
ClusterID: input.ClusterID,
VPNConnectionID: input.VPNConnectionID,
OwnerNodeID: input.OwnerNodeID,
TTL: input.TTL,
Metadata: input.Metadata,
}, s.now().Add(input.TTL), token)
if errors.Is(err, pgx.ErrNoRows) {
return VPNConnectionLease{}, ErrInvalidVPNLease
}
if errors.Is(err, ErrVPNLeaseAlreadyActive) {
return VPNConnectionLease{}, ErrVPNLeaseAlreadyActive
}
if err != nil {
return VPNConnectionLease{}, err
}
_ = s.store.RecordAudit(ctx, ClusterAuditEvent{
ClusterID: &input.ClusterID,
EventType: "vpn_connection.lease_acquired_by_node",
TargetType: "vpn_connection",
TargetID: &input.VPNConnectionID,
Payload: json.RawMessage(`{"node_agent_runtime_requested":true}`),
CreatedAt: s.now(),
})
return item, nil
}
func (s *Service) ReleaseVPNConnectionLease(ctx context.Context, input ReleaseVPNConnectionLeaseInput) (VPNConnectionLease, error) {
if err := s.ensurePlatformAdmin(ctx, input.ActorUserID); err != nil {
return VPNConnectionLease{}, err
@@ -8910,18 +9007,20 @@ func (s *Service) attachVPNFabricServiceChannelLeases(ctx context.Context, profi
if len(exitPool) == 0 {
exitPool = dedupeStrings(append([]string{route.SelectedExitNodeID, connection.ExitNodeID}, connection.AllowedNodeIDs...))
}
backendFallbackAllowed := false
lease, err := s.IssueFabricServiceChannelLease(ctx, IssueFabricServiceChannelLeaseInput{
ClusterID: profile.ClusterID,
OrganizationID: profile.OrganizationID,
UserID: profile.UserID,
ResourceID: connection.ID,
ServiceClass: FabricServiceClassVPNPackets,
EntryNodeIDs: entryPool,
ExitNodeIDs: exitPool,
PreferredEntryNodeID: route.SelectedEntryNodeID,
PreferredExitNodeID: route.SelectedExitNodeID,
AllowedChannels: []string{"vpn_packet", "fabric_control", FabricChannelBulk, FabricChannelControl},
TTL: time.Minute,
ClusterID: profile.ClusterID,
OrganizationID: profile.OrganizationID,
UserID: profile.UserID,
ResourceID: connection.ID,
ServiceClass: FabricServiceClassVPNPackets,
EntryNodeIDs: entryPool,
ExitNodeIDs: exitPool,
PreferredEntryNodeID: route.SelectedEntryNodeID,
PreferredExitNodeID: route.SelectedExitNodeID,
AllowedChannels: []string{"vpn_packet", "fabric_control", FabricChannelBulk, FabricChannelControl},
TTL: 6 * time.Hour,
BackendFallbackAllowed: &backendFallbackAllowed,
})
if err != nil {
profile.Connections[i].ClientConfig = attachVPNFabricServiceChannelError(connection.ClientConfig, err)
@@ -8985,19 +9084,21 @@ func enrichVPNDataplaneSession(profile VPNClientProfile, connection VPNClientCon
status = "ready_for_entry_listener"
}
cfg["vpn_dataplane_session"] = map[string]any{
"schema_version": "rap.vpn_dataplane_session.v1",
"session_id": sessionID,
"status": status,
"issued_at": now,
"expires_at": expiresAt,
"cluster_id": profile.ClusterID,
"organization_id": profile.OrganizationID,
"user_id": profile.UserID,
"vpn_connection_id": connection.ID,
"entry_node_id": route.SelectedEntryNodeID,
"exit_node_id": route.SelectedExitNodeID,
"preferred_transport": "fabric_packet_quic_v1",
"fallback_transport": "backend_http_packet_relay",
"schema_version": "rap.vpn_dataplane_session.v1",
"session_id": sessionID,
"status": status,
"issued_at": now,
"expires_at": expiresAt,
"cluster_id": profile.ClusterID,
"organization_id": profile.OrganizationID,
"user_id": profile.UserID,
"vpn_connection_id": connection.ID,
"entry_node_id": route.SelectedEntryNodeID,
"exit_node_id": route.SelectedExitNodeID,
"preferred_transport": "fabric_service_channel_v1",
"fallback_transport": "none",
"route_authority": "fabric_farm",
"backend_relay_allowed": false,
"packet_contract": map[string]any{
"tunnel_type": "universal_ip_packet",
"application_protocol_agnostic": true,
@@ -9089,10 +9190,12 @@ func vpnConcreteEntryCandidatesFromClientConfig(cfg map[string]any) []map[string
func vpnDataplaneTransportCandidates(route vpnClientFabricRoute, entryCandidates []map[string]any) []map[string]any {
candidates := []map[string]any{
{
"type": "fabric_packet_quic_v1",
"type": "fabric_service_channel_v1",
"status": "contract_ready_listener_pending",
"entry_node_id": route.SelectedEntryNodeID,
"exit_node_id": route.SelectedExitNodeID,
"route_authority": "fabric_farm",
"backend_relay_allowed": false,
"entry_candidates": entryCandidates,
"application_protocols": []string{"ip"},
},
@@ -9100,11 +9203,6 @@ func vpnDataplaneTransportCandidates(route vpnClientFabricRoute, entryCandidates
if direct := vpnDirectHTTPEntryTransportCandidate(route, entryCandidates); direct != nil {
candidates = append(candidates, direct)
}
candidates = append(candidates, map[string]any{
"type": "backend_http_packet_relay",
"status": "active_fallback",
"description": "current safe dataplane until entry listener is available",
})
return candidates
}
@@ -9112,7 +9210,6 @@ func vpnDirectHTTPEntryTransportCandidate(route vpnClientFabricRoute, entryCandi
var selected []map[string]any
hasPublic := false
hasHTTP := false
hasLocalGatewayShortcut := false
for _, candidate := range entryCandidates {
nodeID, _ := candidate["node_id"].(string)
if route.SelectedEntryNodeID != "" && nodeID != route.SelectedEntryNodeID {
@@ -9132,9 +9229,6 @@ func vpnDirectHTTPEntryTransportCandidate(route vpnClientFabricRoute, entryCandi
if strings.EqualFold(reachability, "public") {
hasPublic = true
}
if value, ok := candidate["local_gateway_shortcut"].(bool); ok && value {
hasLocalGatewayShortcut = true
}
selected = append(selected, candidate)
}
if len(selected) == 0 {
@@ -9148,13 +9242,8 @@ func vpnDirectHTTPEntryTransportCandidate(route vpnClientFabricRoute, entryCandi
}
safeClientSwitch := hasPublic
if route.SelectedEntryNodeID != "" && route.SelectedEntryNodeID == route.SelectedExitNodeID {
if hasPublic && hasLocalGatewayShortcut {
status = "available_local_gateway_shortcut"
safeClientSwitch = true
} else {
status = "available_local_gateway_shortcut_pending"
safeClientSwitch = false
}
status = "available_farm_local_route"
safeClientSwitch = hasPublic
}
return map[string]any{
"type": "entry_direct_http_v1",
@@ -9275,9 +9364,13 @@ func vpnFabricRouteIntentPolicy(sourceNodeID, destinationNodeID string, expiresA
"route_version": version,
"policy_version": version,
"peer_directory_version": version,
"backend_relay_fallback": true,
"data_plane_preference": "fabric_mesh",
"route_owner": "vpn_client_profile",
"backend_relay_fallback": false,
"data_plane_preference": "fabric_service_channel",
"route_owner": "fabric_farm",
"vpn_builds_routes": false,
"vpn_builds_tunnels": false,
"farm_builds_routes": true,
"farm_builds_tunnels": true,
"route_refresh_required": true,
"route_refresh_threshold": "24h",
}
@@ -11387,11 +11480,11 @@ func (s *Service) serviceChannelRouteReplacementDecision(input GetNodeSyntheticM
SourceNodeID: fencedRoute.SourceNodeID,
DestinationNodeID: fencedRoute.DestinationNodeID,
OriginalHops: append([]string{}, fencedRoute.Hops...),
EffectiveHops: []string{},
DecisionSource: "service_channel_feedback_no_alternate",
EffectiveHops: append([]string{}, fencedRoute.Hops...),
DecisionSource: "service_channel_feedback_no_alternate_keep_primary",
Generation: generation,
PathScore: 0,
ScoreReasons: []string{"service_channel_fenced_route", "no_unfenced_alternate_route"},
PathScore: serviceChannelReplacementRouteScore(fencedRoute),
ScoreReasons: []string{"service_channel_fenced_route", "no_unfenced_alternate_route", "primary_route_retained_until_rebuild"},
ControlPlaneOnly: true,
ProductionForwarding: false,
ExpiresAt: fencedRoute.ExpiresAt.UTC(),
@@ -11399,10 +11492,10 @@ func (s *Service) serviceChannelRouteReplacementDecision(input GetNodeSyntheticM
applyServiceChannelFeedbackCorrelationToDecision(&decision, routeFeedback)
if serviceChannelFeedbackRequestsRebuild(routeFeedback) {
decision.RebuildRequestID = serviceChannelRebuildRequestID(fencedRoute.RouteID, input.NodeID, generation)
decision.RebuildStatus = "pending_degraded_fallback"
decision.RebuildStatus = "requested"
decision.RebuildReason = "service_channel_feedback_rebuild_requested"
decision.RebuildAttempt = routeFeedback.ConsecutiveFailures
decision.ScoreReasons = append(decision.ScoreReasons, "service_channel_rebuild_requested", "backend_relay_degraded_fallback_until_rebuild")
decision.ScoreReasons = append(decision.ScoreReasons, "service_channel_rebuild_requested")
if routeFeedback.DegradedFallbackRecommended {
decision.ScoreReasons = append(decision.ScoreReasons, "service_channel_degraded_fallback_recommended")
}
@@ -732,7 +732,7 @@ func TestGetVPNClientProfileEnsuresFabricVPNPacketRouteIntents(t *testing.T) {
if !ok {
t.Fatalf("missing vpn_dataplane_session in %#v", cfg)
}
if session["preferred_transport"] != "fabric_packet_quic_v1" || session["fallback_transport"] != "backend_http_packet_relay" {
if session["preferred_transport"] != "fabric_service_channel_v1" || session["fallback_transport"] != "none" || session["backend_relay_allowed"] != false {
t.Fatalf("unexpected dataplane session transports: %#v", session)
}
if session["entry_node_id"] != "entry-1" || session["exit_node_id"] != "exit-1" {
@@ -811,7 +811,7 @@ func TestGetVPNClientProfileForwardsPreferredExit(t *testing.T) {
}
}
func TestVPNDirectHTTPEntryTransportWaitsForLocalGatewayShortcutWhenEntryIsExit(t *testing.T) {
func TestVPNDirectHTTPEntryTransportUsesFarmLocalRouteWhenEntryIsExit(t *testing.T) {
candidate := vpnDirectHTTPEntryTransportCandidate(vpnClientFabricRoute{
SelectedEntryNodeID: "node-1",
SelectedExitNodeID: "node-1",
@@ -823,12 +823,12 @@ func TestVPNDirectHTTPEntryTransportWaitsForLocalGatewayShortcutWhenEntryIsExit(
if candidate == nil {
t.Fatal("candidate is nil")
}
if candidate["safe_client_switch"] != false || candidate["status"] != "available_local_gateway_shortcut_pending" {
t.Fatalf("unexpected local shortcut guard: %#v", candidate)
if candidate["safe_client_switch"] != true || candidate["status"] != "available_farm_local_route" {
t.Fatalf("unexpected farm local route guard: %#v", candidate)
}
}
func TestVPNDirectHTTPEntryTransportAllowsLocalGatewayShortcutWhenReported(t *testing.T) {
func TestVPNDirectHTTPEntryTransportIgnoresLegacyLocalGatewayShortcut(t *testing.T) {
candidate := vpnDirectHTTPEntryTransportCandidate(vpnClientFabricRoute{
SelectedEntryNodeID: "node-1",
SelectedExitNodeID: "node-1",
@@ -841,8 +841,8 @@ func TestVPNDirectHTTPEntryTransportAllowsLocalGatewayShortcutWhenReported(t *te
if candidate == nil {
t.Fatal("candidate is nil")
}
if candidate["safe_client_switch"] != true || candidate["status"] != "available_local_gateway_shortcut" {
t.Fatalf("unexpected local shortcut candidate: %#v", candidate)
if candidate["safe_client_switch"] != true || candidate["status"] != "available_farm_local_route" {
t.Fatalf("unexpected farm route candidate: %#v", candidate)
}
}
@@ -3152,6 +3152,68 @@ func TestListNodeVPNAssignmentsDoesNotRequirePlatformAdmin(t *testing.T) {
}
}
func TestAcquireNodeVPNAssignmentLeaseAllowsEligibleCandidateWithoutPlatformAdmin(t *testing.T) {
store := &fakeRepository{
platformRole: "user",
vpnConnection: VPNConnection{
ID: "vpn-1",
ClusterID: "cluster-1",
Mode: VPNConnectionModeSingleActive,
DesiredState: VPNConnectionDesiredEnabled,
},
nodeVPNAssignments: []NodeVPNAssignment{
{
VPNConnectionID: "vpn-1",
ClusterID: "cluster-1",
OrganizationID: "org-1",
DesiredState: VPNConnectionDesiredEnabled,
AssignmentReason: "eligible_candidate",
},
},
}
service := NewService(store)
lease, err := service.AcquireNodeVPNAssignmentLease(context.Background(), AcquireNodeVPNAssignmentLeaseInput{
ClusterID: "cluster-1",
VPNConnectionID: "vpn-1",
OwnerNodeID: "node-1",
TTL: time.Minute,
Metadata: json.RawMessage(`{"reason":"test"}`),
})
if err != nil {
t.Fatalf("acquire node vpn assignment lease: %v", err)
}
if lease.OwnerNodeID != "node-1" || lease.VPNConnectionID != "vpn-1" || lease.Status != VPNLeaseStatusActive {
t.Fatalf("unexpected lease: %+v", lease)
}
}
func TestAcquireNodeVPNAssignmentLeaseRejectsInvisibleAssignment(t *testing.T) {
store := &fakeRepository{
platformRole: "user",
vpnConnection: VPNConnection{
ID: "vpn-1",
ClusterID: "cluster-1",
Mode: VPNConnectionModeSingleActive,
DesiredState: VPNConnectionDesiredEnabled,
},
nodeVPNAssignments: []NodeVPNAssignment{
{VPNConnectionID: "other-vpn", ClusterID: "cluster-1", AssignmentReason: "eligible_candidate"},
},
}
service := NewService(store)
_, err := service.AcquireNodeVPNAssignmentLease(context.Background(), AcquireNodeVPNAssignmentLeaseInput{
ClusterID: "cluster-1",
VPNConnectionID: "vpn-1",
OwnerNodeID: "node-1",
TTL: time.Minute,
})
if !errors.Is(err, ErrVPNLeaseOwnerNotAllowed) {
t.Fatalf("err = %v, want ErrVPNLeaseOwnerNotAllowed", err)
}
}
func TestRenewNodeVPNAssignmentLeaseAllowsActiveOwnerWithoutPlatformAdmin(t *testing.T) {
store := &fakeRepository{
platformRole: "user",
@@ -6051,18 +6113,24 @@ func TestGetNodeSyntheticMeshConfigReportsRebuildPendingWhenNoAlternateExists(t
if err != nil {
t.Fatalf("synthetic config: %v", err)
}
if containsRouteID(cfg.Routes, "route-bad") {
t.Fatalf("fenced route should be withheld while rebuild is pending: %+v", cfg.Routes)
if !containsRouteID(cfg.Routes, "route-bad") {
t.Fatalf("fenced route should be retained until an alternate exists: %+v", cfg.Routes)
}
if cfg.RoutePathDecisions == nil || cfg.RoutePathDecisions.RebuildRequestCount != 1 || cfg.RoutePathDecisions.DegradedDecisionCount != 1 {
if cfg.RoutePathDecisions == nil || cfg.RoutePathDecisions.RebuildRequestCount != 1 || cfg.RoutePathDecisions.DegradedDecisionCount != 0 {
t.Fatalf("expected rebuild/degraded decision counts: %+v", cfg.RoutePathDecisions)
}
decision := cfg.RoutePathDecisions.Decisions[0]
if decision.DecisionSource != "service_channel_feedback_no_alternate" ||
decision.RebuildStatus != "pending_degraded_fallback" ||
var decision RoutePathDecision
for _, item := range cfg.RoutePathDecisions.Decisions {
if item.DecisionSource == "service_channel_feedback_no_alternate_keep_primary" {
decision = item
break
}
}
if decision.DecisionSource != "service_channel_feedback_no_alternate_keep_primary" ||
decision.RebuildStatus != "requested" ||
decision.RebuildRequestID == "" ||
decision.RebuildAttempt != 3 ||
!containsString(decision.ScoreReasons, "backend_relay_degraded_fallback_until_rebuild") {
!containsString(decision.ScoreReasons, "primary_route_retained_until_rebuild") {
t.Fatalf("unexpected rebuild decision: %+v", decision)
}
}