1
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user