3
This commit is contained in:
@@ -140,15 +140,12 @@ func run(ctx context.Context) (smokeReport, error) {
|
||||
return smokeReport{}, fmt.Errorf("test service: %w", err)
|
||||
}
|
||||
fabricSessionStartedAt := time.Now()
|
||||
fabricSession, _, err := mesh.NewClient(nodeB.URL).OpenFabricSession(ctx, mesh.FabricSessionDialOptions{
|
||||
Token: "rap_fsn_mesh_live_smoke",
|
||||
Timeout: 3 * time.Second,
|
||||
})
|
||||
fabricSession, fabricQUICEndpoint, fabricQUICPressure, err := smokeQUICFabricSession(ctx)
|
||||
if err != nil {
|
||||
return smokeReport{}, fmt.Errorf("fabric session open: %w", err)
|
||||
return smokeReport{}, fmt.Errorf("fabric quic session open: %w", err)
|
||||
}
|
||||
defer fabricSession.Close()
|
||||
firstFabricSessionResponse, err := fabricSession.RoundTrip(ctx, fabricproto.Frame{
|
||||
firstFabricSessionResponse, err := smokeFabricSessionRoundTrip(ctx, fabricSession, fabricproto.Frame{
|
||||
Type: fabricproto.FramePing,
|
||||
Sequence: uint64(fabricSessionStartedAt.UnixNano()),
|
||||
Payload: []byte("mesh-live-smoke-fabric-session"),
|
||||
@@ -156,7 +153,7 @@ func run(ctx context.Context) (smokeReport, error) {
|
||||
if err != nil {
|
||||
return smokeReport{}, fmt.Errorf("fabric session first round trip: %w", err)
|
||||
}
|
||||
secondFabricSessionResponse, err := fabricSession.RoundTrip(ctx, fabricproto.Frame{
|
||||
secondFabricSessionResponse, err := smokeFabricSessionRoundTrip(ctx, fabricSession, fabricproto.Frame{
|
||||
Type: fabricproto.FramePing,
|
||||
Sequence: uint64(fabricSessionStartedAt.UnixNano()) + 1,
|
||||
Payload: []byte("mesh-live-smoke-fabric-session-2"),
|
||||
@@ -175,13 +172,9 @@ func run(ctx context.Context) (smokeReport, error) {
|
||||
}
|
||||
fabricVPNBulkPressure, fabricVPNBulkChannels, fabricVPNInteractiveChannels, fabricVPNBulkWindow, fabricVPNInteractiveWindow, fabricVPNPressureLevel, fabricVPNPressureScore, fabricVPNPressureReasons, fabricVPNPressureAction := smokeVPNFlowSchedulerBulkPressure()
|
||||
fabricVPNRouteRecovered, fabricVPNRouteSwitches, fabricVPNRecoveryMS, fabricVPNRecoveryMaxMS, fabricVPNRecoveryAvgMS, fabricVPNRecoveryReason := smokeVPNFlowSchedulerRouteRecovery()
|
||||
fabricQUICAccepted, fabricQUICEndpoint, fabricQUICPressure, err := smokeQUICFabricSession(ctx)
|
||||
if err != nil {
|
||||
return smokeReport{}, fmt.Errorf("fabric quic smoke: %w", err)
|
||||
}
|
||||
|
||||
return smokeReport{
|
||||
Stage: "C17F scoped synthetic config plus live HTTP transport",
|
||||
Stage: "C17F scoped synthetic config plus live QUIC fabric transport",
|
||||
ProductionForwarding: false,
|
||||
ScopedConfigLoaded: nodeAConfig.ConfigVersion == "smoke-config-v1",
|
||||
DirectProbeAccepted: directAck.MessageType == mesh.SyntheticMessageProbeAck,
|
||||
@@ -210,11 +203,11 @@ func run(ctx context.Context) (smokeReport, error) {
|
||||
FabricVPNRecoveryMaxMS: fabricVPNRecoveryMaxMS,
|
||||
FabricVPNRecoveryAvgMS: fabricVPNRecoveryAvgMS,
|
||||
FabricVPNRecoveryReason: fabricVPNRecoveryReason,
|
||||
FabricQUICAccepted: fabricQUICAccepted,
|
||||
FabricQUICAccepted: fabricSessionAccepted,
|
||||
FabricQUICEndpoint: fabricQUICEndpoint,
|
||||
FabricQUICPressure: fabricQUICPressure,
|
||||
FabricSessionLatencyMS: fabricSessionLatency.Milliseconds(),
|
||||
FabricSessionEndpoint: nodeB.URL + "/mesh/v1/fabric/session/ws",
|
||||
FabricSessionEndpoint: "quic://" + fabricQUICEndpoint,
|
||||
PeerEndpoints: map[string]any{
|
||||
"node-a": nodeA.URL,
|
||||
"node-r": nodeR.URL,
|
||||
@@ -269,18 +262,16 @@ func smokeVPNFlowSchedulerRouteRecovery() (bool, uint64, int64, int64, int64, st
|
||||
stat.LastRouteSwitchReason
|
||||
}
|
||||
|
||||
func smokeQUICFabricSession(ctx context.Context) (bool, string, int, error) {
|
||||
func smokeQUICFabricSession(ctx context.Context) (mesh.FabricTransportSession, string, int, error) {
|
||||
server, err := mesh.StartQUICFabricServer(ctx, mesh.QUICFabricServerConfig{
|
||||
ListenAddr: "127.0.0.1:0",
|
||||
TLSConfig: smokeQUICTLSConfig(),
|
||||
})
|
||||
if err != nil {
|
||||
return false, "", 0, err
|
||||
return nil, "", 0, err
|
||||
}
|
||||
defer server.Close()
|
||||
endpoint := server.Addr().String()
|
||||
transport := mesh.NewQUICFabricTransport(nil)
|
||||
defer transport.Close()
|
||||
session, err := transport.Connect(ctx, mesh.FabricTransportTarget{
|
||||
PeerID: "node-b",
|
||||
Endpoint: endpoint,
|
||||
@@ -293,31 +284,12 @@ func smokeQUICFabricSession(ctx context.Context) (bool, string, int, error) {
|
||||
ErrorBuffer: 4,
|
||||
})
|
||||
if err != nil {
|
||||
return false, endpoint, 0, err
|
||||
}
|
||||
defer session.Close()
|
||||
if err := session.Send(ctx, fabricproto.Frame{
|
||||
Type: fabricproto.FramePing,
|
||||
Sequence: uint64(time.Now().UnixNano()),
|
||||
Payload: []byte("mesh-live-smoke-quic"),
|
||||
}); err != nil {
|
||||
return false, endpoint, 0, err
|
||||
}
|
||||
timer := time.NewTimer(3 * time.Second)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case frame := <-session.Frames():
|
||||
snapshot := transport.Snapshot()
|
||||
return frame.Type == fabricproto.FramePong && string(frame.Payload) == "mesh-live-smoke-quic", endpoint, snapshot.CapacityPressurePercent, nil
|
||||
case err := <-session.Errors():
|
||||
return false, endpoint, 0, err
|
||||
case <-timer.C:
|
||||
return false, endpoint, 0, fmt.Errorf("timed out waiting for quic pong")
|
||||
case <-ctx.Done():
|
||||
return false, endpoint, 0, ctx.Err()
|
||||
}
|
||||
_ = transport.Close()
|
||||
_ = server.Close()
|
||||
return nil, endpoint, 0, err
|
||||
}
|
||||
snapshot := transport.Snapshot()
|
||||
return &smokeManagedFabricSession{session: session, transport: transport, server: server}, endpoint, snapshot.CapacityPressurePercent, nil
|
||||
}
|
||||
|
||||
func smokeQUICTLSConfig() *tls.Config {
|
||||
@@ -341,25 +313,20 @@ func smokeQUICTLSConfig() *tls.Config {
|
||||
}
|
||||
}
|
||||
|
||||
func smokeFabricVPNPacketOverSession(ctx context.Context, fabricSession *mesh.FabricSessionClient) (bool, bool, int, error) {
|
||||
func smokeFabricVPNPacketOverSession(ctx context.Context, fabricSession mesh.FabricTransportSession) (bool, bool, int, error) {
|
||||
const interactiveStreamID uint64 = 4400
|
||||
const bulkStreamID uint64 = 4401
|
||||
pump := fabricSession.StartPump(ctx, mesh.FabricSessionPumpOptions{
|
||||
OutboundBuffer: 4,
|
||||
InboundBuffer: 4,
|
||||
ErrorBuffer: 4,
|
||||
})
|
||||
defer pump.Close()
|
||||
for _, frame := range []fabricproto.Frame{
|
||||
{Type: fabricproto.FrameOpenStream, StreamID: interactiveStreamID, TrafficClass: fabricproto.TrafficClassInteractive},
|
||||
{Type: fabricproto.FrameOpenStream, StreamID: bulkStreamID, TrafficClass: fabricproto.TrafficClassBulk},
|
||||
} {
|
||||
if err := pump.Send(ctx, frame); err != nil {
|
||||
if err := fabricSession.Send(ctx, frame); err != nil {
|
||||
return false, false, 0, err
|
||||
}
|
||||
}
|
||||
transport := &vpnruntime.FabricSessionPacketTransport{
|
||||
Sender: pump,
|
||||
Sender: fabricSession,
|
||||
Receiver: fabricSession,
|
||||
StreamID: interactiveStreamID,
|
||||
VPNConnectionID: "vpn-smoke",
|
||||
SendDirection: vpnruntime.FabricDirectionGatewayToClient,
|
||||
@@ -378,7 +345,7 @@ func smokeFabricVPNPacketOverSession(ctx context.Context, fabricSession *mesh.Fa
|
||||
acked := map[uint64]bool{}
|
||||
for {
|
||||
select {
|
||||
case frame := <-pump.Frames():
|
||||
case frame := <-fabricSession.Frames():
|
||||
if frame.Type == fabricproto.FrameAck && frame.Sequence == 1 {
|
||||
acked[frame.StreamID] = true
|
||||
if acked[interactiveStreamID] && acked[bulkStreamID] {
|
||||
@@ -393,7 +360,7 @@ func smokeFabricVPNPacketOverSession(ctx context.Context, fabricSession *mesh.Fa
|
||||
return true, sharded, int(fanout), nil
|
||||
}
|
||||
}
|
||||
case err := <-pump.Errors():
|
||||
case err := <-fabricSession.Errors():
|
||||
return false, false, 0, err
|
||||
case <-timer.C:
|
||||
return false, false, 0, fmt.Errorf("timed out waiting for fabric vpn packet ack")
|
||||
@@ -403,6 +370,68 @@ func smokeFabricVPNPacketOverSession(ctx context.Context, fabricSession *mesh.Fa
|
||||
}
|
||||
}
|
||||
|
||||
type smokeManagedFabricSession struct {
|
||||
session mesh.FabricTransportSession
|
||||
transport *mesh.QUICFabricTransport
|
||||
server *mesh.QUICFabricServer
|
||||
}
|
||||
|
||||
func (s *smokeManagedFabricSession) Send(ctx context.Context, frame fabricproto.Frame) error {
|
||||
return s.session.Send(ctx, frame)
|
||||
}
|
||||
|
||||
func (s *smokeManagedFabricSession) Frames() <-chan fabricproto.Frame {
|
||||
return s.session.Frames()
|
||||
}
|
||||
|
||||
func (s *smokeManagedFabricSession) Errors() <-chan error {
|
||||
return s.session.Errors()
|
||||
}
|
||||
|
||||
func (s *smokeManagedFabricSession) Closed() bool {
|
||||
return s.session.Closed()
|
||||
}
|
||||
|
||||
func (s *smokeManagedFabricSession) Close() error {
|
||||
var firstErr error
|
||||
if s.session != nil {
|
||||
firstErr = s.session.Close()
|
||||
}
|
||||
if s.transport != nil {
|
||||
if err := s.transport.Close(); firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
if s.server != nil {
|
||||
if err := s.server.Close(); firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func smokeFabricSessionRoundTrip(ctx context.Context, session mesh.FabricTransportSession, frame fabricproto.Frame) (fabricproto.Frame, error) {
|
||||
if err := session.Send(ctx, frame); err != nil {
|
||||
return fabricproto.Frame{}, err
|
||||
}
|
||||
timer := time.NewTimer(3 * time.Second)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case response := <-session.Frames():
|
||||
if response.Sequence == frame.Sequence {
|
||||
return response, nil
|
||||
}
|
||||
case err := <-session.Errors():
|
||||
return fabricproto.Frame{}, err
|
||||
case <-timer.C:
|
||||
return fabricproto.Frame{}, fmt.Errorf("timed out waiting for fabric session response")
|
||||
case <-ctx.Done():
|
||||
return fabricproto.Frame{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func smokeIPv4TCPPacket(src [4]byte, dst [4]byte, srcPort uint16, dstPort uint16, flags byte) []byte {
|
||||
packet := make([]byte, 40)
|
||||
packet[0] = 0x45
|
||||
@@ -445,7 +474,7 @@ func writeSmokeScopedConfig(local mesh.PeerIdentity, peers map[string]string, ro
|
||||
func newSmokeNode(local mesh.PeerIdentity) *smokeNode {
|
||||
node := &smokeNode{Local: local}
|
||||
node.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mesh.Server{Local: node.Local, SyntheticRuntime: node.Runtime, FabricSessionEnabled: true, FabricSessionWebSocketEnabled: true}.Handler().ServeHTTP(w, r)
|
||||
mesh.Server{Local: node.Local, SyntheticRuntime: node.Runtime}.Handler().ServeHTTP(w, r)
|
||||
}))
|
||||
node.URL = node.server.URL
|
||||
return node
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
@@ -15,9 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/example/remote-access-platform/agents/rap-node-agent/internal/agent"
|
||||
"github.com/example/remote-access-platform/agents/rap-node-agent/internal/fabricproto"
|
||||
"github.com/example/remote-access-platform/agents/rap-node-agent/internal/hostagent"
|
||||
"github.com/example/remote-access-platform/agents/rap-node-agent/internal/mesh"
|
||||
)
|
||||
|
||||
type installCommandConfig struct {
|
||||
@@ -82,10 +79,6 @@ func main() {
|
||||
if err := runUpdateHostAgentLoop(ctx, os.Args[2:]); err != nil {
|
||||
log.Fatalf("update-host-agent-loop failed: %v", err)
|
||||
}
|
||||
case "fabric-session-smoke":
|
||||
if err := runFabricSessionSmoke(ctx, os.Args[2:]); err != nil {
|
||||
log.Fatalf("fabric-session-smoke failed: %v", err)
|
||||
}
|
||||
default:
|
||||
usage()
|
||||
os.Exit(2)
|
||||
@@ -117,78 +110,6 @@ func applyStagedSelfUpdate() {
|
||||
_ = os.Remove(backup)
|
||||
}
|
||||
|
||||
func runFabricSessionSmoke(ctx context.Context, args []string) error {
|
||||
fs := flag.NewFlagSet("fabric-session-smoke", flag.ContinueOnError)
|
||||
var meshURL string
|
||||
var token string
|
||||
var timeoutSeconds int
|
||||
var payload string
|
||||
var authorityPayload string
|
||||
var authoritySignature string
|
||||
fs.StringVar(&meshURL, "mesh-url", getenv("RAP_MESH_SMOKE_URL", ""), "Mesh base URL, for example http://node:19131.")
|
||||
fs.StringVar(&token, "token", getenv("RAP_FABRIC_SESSION_TOKEN", ""), "Fabric session token starting with rap_fsn_.")
|
||||
fs.IntVar(&timeoutSeconds, "timeout-seconds", getenvInt("RAP_FABRIC_SESSION_SMOKE_TIMEOUT_SECONDS", 5), "Smoke timeout in seconds.")
|
||||
fs.StringVar(&payload, "payload", getenv("RAP_FABRIC_SESSION_SMOKE_PAYLOAD", "rap-fabric-session-smoke"), "Ping payload.")
|
||||
fs.StringVar(&authorityPayload, "authority-payload", getenv("RAP_FABRIC_SESSION_AUTHORITY_PAYLOAD", ""), "Base64 or JSON fabric session authority payload header.")
|
||||
fs.StringVar(&authoritySignature, "authority-signature", getenv("RAP_FABRIC_SESSION_AUTHORITY_SIGNATURE", ""), "Base64 or JSON fabric session authority signature header.")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(meshURL) == "" {
|
||||
return fmt.Errorf("mesh-url is required")
|
||||
}
|
||||
if strings.TrimSpace(token) == "" {
|
||||
return fmt.Errorf("token is required")
|
||||
}
|
||||
if timeoutSeconds <= 0 {
|
||||
timeoutSeconds = 5
|
||||
}
|
||||
smokeCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
defer cancel()
|
||||
header := make(http.Header)
|
||||
if strings.TrimSpace(authorityPayload) != "" {
|
||||
header.Set("X-RAP-Fabric-Session-Authority-Payload", strings.TrimSpace(authorityPayload))
|
||||
}
|
||||
if strings.TrimSpace(authoritySignature) != "" {
|
||||
header.Set("X-RAP-Fabric-Session-Authority-Signature", strings.TrimSpace(authoritySignature))
|
||||
}
|
||||
startedAt := time.Now()
|
||||
response, err := mesh.NewClient(meshURL).SendFabricSessionFrame(smokeCtx, mesh.FabricSessionDialOptions{
|
||||
Token: token,
|
||||
Header: header,
|
||||
Timeout: time.Duration(timeoutSeconds) * time.Second,
|
||||
}, fabricproto.Frame{
|
||||
Type: fabricproto.FramePing,
|
||||
Sequence: uint64(startedAt.UnixNano()),
|
||||
Payload: []byte(payload),
|
||||
})
|
||||
duration := time.Since(startedAt)
|
||||
result := map[string]any{
|
||||
"schema_version": "rap.fabric_session_smoke_result.v1",
|
||||
"mesh_url": strings.TrimSpace(meshURL),
|
||||
"ok": err == nil && response.Type == fabricproto.FramePong && string(response.Payload) == payload,
|
||||
"latency_ms": duration.Milliseconds(),
|
||||
"response_type": response.Type,
|
||||
"sequence": response.Sequence,
|
||||
"authority": strings.TrimSpace(authorityPayload) != "" || strings.TrimSpace(authoritySignature) != "",
|
||||
}
|
||||
if err != nil {
|
||||
result["error"] = err.Error()
|
||||
}
|
||||
encoded, marshalErr := json.MarshalIndent(result, "", " ")
|
||||
if marshalErr != nil {
|
||||
return marshalErr
|
||||
}
|
||||
fmt.Println(string(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.Type != fabricproto.FramePong || string(response.Payload) != payload {
|
||||
return fmt.Errorf("fabric session smoke returned unexpected response type=%d payload=%q", response.Type, string(response.Payload))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runInstallLinux(ctx context.Context, args []string) error {
|
||||
fs := flag.NewFlagSet("install-linux", flag.ContinueOnError)
|
||||
cfg := hostagent.LinuxInstallConfig{}
|
||||
@@ -215,16 +136,15 @@ func runInstallLinux(ctx context.Context, args []string) error {
|
||||
fs.IntVar(&cfg.AutoUpdateHealthTimeoutSeconds, "auto-update-health-timeout-seconds", getenvInt("RAP_UPDATE_HEALTH_TIMEOUT_SECONDS", 30), "Updated service health timeout in seconds.")
|
||||
fs.StringVar(&cfg.HostAgentSourcePath, "host-agent-source-path", getenv("RAP_HOST_AGENT_SOURCE_PATH", ""), "Source rap-host-agent path copied to the persistent updater location.")
|
||||
fs.BoolVar(&cfg.RuntimeConfig.WorkloadSupervisionEnabled, "workload-supervision-enabled", getenvBool("RAP_WORKLOAD_SUPERVISION_ENABLED", false), "Enable node-agent workload status reporting.")
|
||||
fs.BoolVar(&cfg.RuntimeConfig.MeshSyntheticRuntimeEnabled, "mesh-synthetic-runtime-enabled", getenvBool("RAP_MESH_SYNTHETIC_RUNTIME_ENABLED", true), "Enable synthetic mesh runtime.")
|
||||
fs.BoolVar(&cfg.RuntimeConfig.MeshSyntheticRuntimeEnabled, "mesh-synthetic-runtime-enabled", getenvBool("RAP_MESH_SYNTHETIC_RUNTIME_ENABLED", false), "Enable historical synthetic mesh runtime.")
|
||||
fs.BoolVar(&cfg.RuntimeConfig.MeshProductionForwardingEnabled, "mesh-production-forwarding-enabled", getenvBool("RAP_MESH_PRODUCTION_FORWARDING_ENABLED", false), "Enable production forwarding gate; runtime still fail-closed if unavailable.")
|
||||
fs.BoolVar(&cfg.RuntimeConfig.MeshFabricSessionEnabled, "mesh-fabric-session-enabled", getenvBool("RAP_MESH_FABRIC_SESSION_ENABLED", false), "Enable authenticated fabric session endpoint.")
|
||||
fs.BoolVar(&cfg.RuntimeConfig.VPNFabricSessionTransportEnabled, "vpn-fabric-session-transport-enabled", getenvBool("RAP_VPN_FABRIC_SESSION_TRANSPORT_ENABLED", false), "Route VPN packet transport over persistent fabric sessions.")
|
||||
fs.BoolVar(&cfg.RuntimeConfig.MeshQUICFabricEnabled, "mesh-quic-fabric-enabled", getenvBool("RAP_MESH_QUIC_FABRIC_ENABLED", false), "Enable QUIC/UDP fabric listener.")
|
||||
fs.StringVar(&cfg.RuntimeConfig.MeshQUICFabricListenAddr, "mesh-quic-fabric-listen-addr", getenv("RAP_MESH_QUIC_FABRIC_LISTEN_ADDR", ""), "QUIC/UDP fabric listen address.")
|
||||
fs.IntVar(&cfg.RuntimeConfig.VPNFabricSessionStreamShards, "vpn-fabric-session-stream-shards", getenvInt("RAP_VPN_FABRIC_SESSION_STREAM_SHARDS", 4), "VPN fabric-session stream shards per traffic class.")
|
||||
fs.IntVar(&cfg.RuntimeConfig.VPNFabricQUICMaxStreamsPerConn, "vpn-fabric-quic-max-streams-per-conn", getenvInt("RAP_VPN_FABRIC_QUIC_MAX_STREAMS_PER_CONN", 64), "Maximum logical fabric-session streams per cached VPN QUIC carrier connection.")
|
||||
fs.IntVar(&cfg.RuntimeConfig.VPNFabricQUICIdleTTLSeconds, "vpn-fabric-quic-idle-ttl-seconds", getenvInt("RAP_VPN_FABRIC_QUIC_IDLE_TTL_SECONDS", 300), "Idle TTL seconds for cached VPN QUIC carrier connections.")
|
||||
fs.StringVar(&cfg.RuntimeConfig.MeshListenAddr, "mesh-listen-addr", getenv("RAP_MESH_LISTEN_ADDR", ":19131"), "Synthetic mesh HTTP listen address.")
|
||||
fs.StringVar(&cfg.RuntimeConfig.MeshListenAddr, "mesh-listen-addr", getenv("RAP_MESH_LISTEN_ADDR", ""), "Historical synthetic mesh HTTP listen address.")
|
||||
fs.StringVar(&cfg.RuntimeConfig.MeshListenPortMode, "mesh-listen-port-mode", getenv("RAP_MESH_LISTEN_PORT_MODE", "auto"), "Mesh listen port behavior: manual, auto, or disabled.")
|
||||
fs.IntVar(&cfg.RuntimeConfig.MeshListenAutoPortStart, "mesh-listen-auto-port-start", getenvInt("RAP_MESH_LISTEN_AUTO_PORT_START", 19131), "First port used when mesh listen port mode is auto.")
|
||||
fs.IntVar(&cfg.RuntimeConfig.MeshListenAutoPortEnd, "mesh-listen-auto-port-end", getenvInt("RAP_MESH_LISTEN_AUTO_PORT_END", 19231), "Last port used when mesh listen port mode is auto.")
|
||||
@@ -303,16 +223,15 @@ func runInstallWindows(ctx context.Context, args []string) error {
|
||||
fs.IntVar(&cfg.AutoUpdateHealthTimeoutSeconds, "auto-update-health-timeout-seconds", getenvInt("RAP_UPDATE_HEALTH_TIMEOUT_SECONDS", 30), "Updated service health timeout in seconds.")
|
||||
fs.StringVar(&cfg.HostAgentSourcePath, "host-agent-source-path", getenv("RAP_HOST_AGENT_SOURCE_PATH", ""), "Source rap-host-agent.exe path copied to the persistent updater location.")
|
||||
fs.BoolVar(&cfg.RuntimeConfig.WorkloadSupervisionEnabled, "workload-supervision-enabled", getenvBool("RAP_WORKLOAD_SUPERVISION_ENABLED", false), "Enable node-agent workload status reporting.")
|
||||
fs.BoolVar(&cfg.RuntimeConfig.MeshSyntheticRuntimeEnabled, "mesh-synthetic-runtime-enabled", getenvBool("RAP_MESH_SYNTHETIC_RUNTIME_ENABLED", true), "Enable synthetic mesh runtime.")
|
||||
fs.BoolVar(&cfg.RuntimeConfig.MeshSyntheticRuntimeEnabled, "mesh-synthetic-runtime-enabled", getenvBool("RAP_MESH_SYNTHETIC_RUNTIME_ENABLED", false), "Enable historical synthetic mesh runtime.")
|
||||
fs.BoolVar(&cfg.RuntimeConfig.MeshProductionForwardingEnabled, "mesh-production-forwarding-enabled", getenvBool("RAP_MESH_PRODUCTION_FORWARDING_ENABLED", false), "Enable production forwarding gate; runtime still fail-closed if unavailable.")
|
||||
fs.BoolVar(&cfg.RuntimeConfig.MeshFabricSessionEnabled, "mesh-fabric-session-enabled", getenvBool("RAP_MESH_FABRIC_SESSION_ENABLED", false), "Enable authenticated fabric session endpoint.")
|
||||
fs.BoolVar(&cfg.RuntimeConfig.VPNFabricSessionTransportEnabled, "vpn-fabric-session-transport-enabled", getenvBool("RAP_VPN_FABRIC_SESSION_TRANSPORT_ENABLED", false), "Route VPN packet transport over persistent fabric sessions.")
|
||||
fs.BoolVar(&cfg.RuntimeConfig.MeshQUICFabricEnabled, "mesh-quic-fabric-enabled", getenvBool("RAP_MESH_QUIC_FABRIC_ENABLED", false), "Enable QUIC/UDP fabric listener.")
|
||||
fs.StringVar(&cfg.RuntimeConfig.MeshQUICFabricListenAddr, "mesh-quic-fabric-listen-addr", getenv("RAP_MESH_QUIC_FABRIC_LISTEN_ADDR", ""), "QUIC/UDP fabric listen address.")
|
||||
fs.IntVar(&cfg.RuntimeConfig.VPNFabricSessionStreamShards, "vpn-fabric-session-stream-shards", getenvInt("RAP_VPN_FABRIC_SESSION_STREAM_SHARDS", 4), "VPN fabric-session stream shards per traffic class.")
|
||||
fs.IntVar(&cfg.RuntimeConfig.VPNFabricQUICMaxStreamsPerConn, "vpn-fabric-quic-max-streams-per-conn", getenvInt("RAP_VPN_FABRIC_QUIC_MAX_STREAMS_PER_CONN", 64), "Maximum logical fabric-session streams per cached VPN QUIC carrier connection.")
|
||||
fs.IntVar(&cfg.RuntimeConfig.VPNFabricQUICIdleTTLSeconds, "vpn-fabric-quic-idle-ttl-seconds", getenvInt("RAP_VPN_FABRIC_QUIC_IDLE_TTL_SECONDS", 300), "Idle TTL seconds for cached VPN QUIC carrier connections.")
|
||||
fs.StringVar(&cfg.RuntimeConfig.MeshListenAddr, "mesh-listen-addr", getenv("RAP_MESH_LISTEN_ADDR", ":19131"), "Synthetic mesh HTTP listen address.")
|
||||
fs.StringVar(&cfg.RuntimeConfig.MeshListenAddr, "mesh-listen-addr", getenv("RAP_MESH_LISTEN_ADDR", ""), "Historical synthetic mesh HTTP listen address.")
|
||||
fs.StringVar(&cfg.RuntimeConfig.MeshListenPortMode, "mesh-listen-port-mode", getenv("RAP_MESH_LISTEN_PORT_MODE", "auto"), "Mesh listen port behavior: manual, auto, or disabled.")
|
||||
fs.IntVar(&cfg.RuntimeConfig.MeshListenAutoPortStart, "mesh-listen-auto-port-start", getenvInt("RAP_MESH_LISTEN_AUTO_PORT_START", 19131), "First port used when mesh listen port mode is auto.")
|
||||
fs.IntVar(&cfg.RuntimeConfig.MeshListenAutoPortEnd, "mesh-listen-auto-port-end", getenvInt("RAP_MESH_LISTEN_AUTO_PORT_END", 19231), "Last port used when mesh listen port mode is auto.")
|
||||
@@ -513,16 +432,19 @@ func runUpdateLoop(ctx context.Context, args []string) error {
|
||||
}
|
||||
cfg.HostAgentUpdateEnabled = hostAgentStatusEnabled
|
||||
cfg.HostAgentUpdateRequest = hostagent.HostAgentUpdateRequest{
|
||||
BackendURL: req.BackendURL,
|
||||
ClusterID: req.ClusterID,
|
||||
NodeID: req.NodeID,
|
||||
StateDir: req.StateDir,
|
||||
CurrentVersion: hostAgentVersion,
|
||||
Channel: req.Channel,
|
||||
OS: firstNonEmptyLocal(req.OS, runtime.GOOS),
|
||||
Arch: firstNonEmptyLocal(req.Arch, runtime.GOARCH),
|
||||
InstallType: hostagent.BinaryUpdateInstallType,
|
||||
BinaryPath: hostAgentBinaryPath,
|
||||
BackendURL: req.BackendURL,
|
||||
ClusterID: req.ClusterID,
|
||||
NodeID: req.NodeID,
|
||||
StateDir: req.StateDir,
|
||||
ClusterAuthorityPublicKey: req.ClusterAuthorityPublicKey,
|
||||
FabricRegistryRecordsJSON: req.FabricRegistryRecordsJSON,
|
||||
MeshRegion: req.MeshRegion,
|
||||
CurrentVersion: hostAgentVersion,
|
||||
Channel: req.Channel,
|
||||
OS: firstNonEmptyLocal(req.OS, runtime.GOOS),
|
||||
Arch: firstNonEmptyLocal(req.Arch, runtime.GOARCH),
|
||||
InstallType: hostagent.BinaryUpdateInstallType,
|
||||
BinaryPath: hostAgentBinaryPath,
|
||||
}
|
||||
if req.InstallType == hostagent.WindowsUpdateInstallType || runtime.GOOS == "windows" {
|
||||
cfg.HostAgentUpdateRequest.InstallType = "windows_binary"
|
||||
@@ -569,6 +491,9 @@ func parseMonitor(args []string) (hostagent.MonitorConfig, error) {
|
||||
fs.StringVar(&cfg.ClusterID, "cluster-id", getenv("RAP_CLUSTER_ID", ""), "Cluster ID.")
|
||||
fs.StringVar(&cfg.NodeID, "node-id", getenv("RAP_NODE_ID", ""), "Already enrolled node ID.")
|
||||
fs.StringVar(&cfg.StateDir, "state-dir", getenv("RAP_NODE_STATE_DIR", hostagent.DefaultStateDir), "Host path containing node-agent identity.json.")
|
||||
fs.StringVar(&cfg.ClusterAuthorityPublicKey, "cluster-authority-public-key", getenv("RAP_CLUSTER_AUTHORITY_PUBLIC_KEY", ""), "Pinned Ed25519 cluster authority public key for signed fabric registry records.")
|
||||
fs.StringVar(&cfg.FabricRegistryRecordsJSON, "fabric-registry-records-json", getenv("RAP_FABRIC_REGISTRY_RECORDS_JSON", ""), "JSON array of signed QUIC-only fabric registry records used to reach update/control services.")
|
||||
fs.StringVar(&cfg.MeshRegion, "mesh-region", getenv("RAP_MESH_REGION", ""), "Region/site hint for fabric registry endpoint selection.")
|
||||
fs.StringVar(&cfg.Product, "product", getenv("RAP_MONITOR_PRODUCT", hostagent.DefaultMonitorProduct), "Status product name.")
|
||||
fs.StringVar(&cfg.CurrentVersion, "current-version", getenv("RAP_HOST_AGENT_VERSION", agent.Version), "Current rap-host-agent version.")
|
||||
fs.StringVar(&cfg.DockerBinary, "docker-binary", getenv("RAP_DOCKER_BINARY", "docker"), "Docker CLI binary.")
|
||||
@@ -716,6 +641,9 @@ func parseHostAgentUpdate(args []string) (hostagent.HostAgentUpdateRequest, int,
|
||||
fs.StringVar(&req.ClusterID, "cluster-id", getenv("RAP_CLUSTER_ID", ""), "Cluster ID.")
|
||||
fs.StringVar(&req.NodeID, "node-id", getenv("RAP_NODE_ID", ""), "Already enrolled node ID.")
|
||||
fs.StringVar(&req.StateDir, "state-dir", getenv("RAP_NODE_STATE_DIR", ""), "Host path containing node-agent identity.json.")
|
||||
fs.StringVar(&req.ClusterAuthorityPublicKey, "cluster-authority-public-key", getenv("RAP_CLUSTER_AUTHORITY_PUBLIC_KEY", ""), "Pinned Ed25519 cluster authority public key for signed fabric registry records.")
|
||||
fs.StringVar(&req.FabricRegistryRecordsJSON, "fabric-registry-records-json", getenv("RAP_FABRIC_REGISTRY_RECORDS_JSON", ""), "JSON array of signed QUIC-only fabric registry records used to reach update/control services.")
|
||||
fs.StringVar(&req.MeshRegion, "mesh-region", getenv("RAP_MESH_REGION", ""), "Region/site hint for fabric registry endpoint selection.")
|
||||
fs.StringVar(&req.CurrentVersion, "current-version", getenv("RAP_HOST_AGENT_VERSION", agent.Version), "Currently installed rap-host-agent version.")
|
||||
fs.StringVar(&req.Channel, "channel", getenv("RAP_UPDATE_CHANNEL", ""), "Optional update channel override.")
|
||||
fs.StringVar(&req.OS, "os", getenv("RAP_HOST_AGENT_UPDATE_OS", runtime.GOOS), "Host-agent artifact OS selector.")
|
||||
@@ -739,6 +667,9 @@ func registerUpdateFlags(fs *flag.FlagSet, req *hostagent.UpdateRequest, healthT
|
||||
fs.StringVar(&req.ClusterID, "cluster-id", getenv("RAP_CLUSTER_ID", ""), "Cluster ID.")
|
||||
fs.StringVar(&req.NodeID, "node-id", getenv("RAP_NODE_ID", ""), "Already enrolled node ID.")
|
||||
fs.StringVar(&req.StateDir, "state-dir", getenv("RAP_NODE_STATE_DIR", ""), "Host path containing node-agent identity.json; used when node-id is not known yet.")
|
||||
fs.StringVar(&req.ClusterAuthorityPublicKey, "cluster-authority-public-key", getenv("RAP_CLUSTER_AUTHORITY_PUBLIC_KEY", ""), "Pinned Ed25519 cluster authority public key for signed fabric registry records.")
|
||||
fs.StringVar(&req.FabricRegistryRecordsJSON, "fabric-registry-records-json", getenv("RAP_FABRIC_REGISTRY_RECORDS_JSON", ""), "JSON array of signed QUIC-only fabric registry records used to reach update/control services.")
|
||||
fs.StringVar(&req.MeshRegion, "mesh-region", getenv("RAP_MESH_REGION", ""), "Region/site hint for fabric registry endpoint selection.")
|
||||
fs.StringVar(&req.Product, "product", getenv("RAP_UPDATE_PRODUCT", hostagent.DefaultUpdateProduct), "Update product name.")
|
||||
fs.StringVar(&req.CurrentVersion, "current-version", getenv("RAP_NODE_AGENT_VERSION", agent.Version), "Currently running product version.")
|
||||
fs.StringVar(&req.OS, "os", getenv("RAP_UPDATE_OS", runtime.GOOS), "Artifact OS selector.")
|
||||
@@ -797,16 +728,15 @@ func parseInstall(args []string) (installCommandConfig, error) {
|
||||
fs.IntVar(&autoUpdate.MonitorDiskCritical, "monitor-disk-critical-percent", getenvInt("RAP_MONITOR_DISK_CRITICAL_PERCENT", hostagent.DefaultMonitorDiskCriticalPercent), "Disk used percent that reports failure after cleanup.")
|
||||
fs.BoolVar(&autoUpdate.MonitorCleanupDocker, "monitor-cleanup-docker", getenvBool("RAP_MONITOR_CLEANUP_DOCKER", true), "Run safe docker prune cleanup when disk is above cleanup threshold.")
|
||||
fs.BoolVar(&cfg.WorkloadSupervisionEnabled, "workload-supervision-enabled", getenvBool("RAP_WORKLOAD_SUPERVISION_ENABLED", false), "Enable node-agent workload status reporting.")
|
||||
fs.BoolVar(&cfg.MeshSyntheticRuntimeEnabled, "mesh-synthetic-runtime-enabled", getenvBool("RAP_MESH_SYNTHETIC_RUNTIME_ENABLED", false), "Enable synthetic mesh runtime.")
|
||||
fs.BoolVar(&cfg.MeshSyntheticRuntimeEnabled, "mesh-synthetic-runtime-enabled", getenvBool("RAP_MESH_SYNTHETIC_RUNTIME_ENABLED", false), "Enable historical synthetic mesh runtime.")
|
||||
fs.BoolVar(&cfg.MeshProductionForwardingEnabled, "mesh-production-forwarding-enabled", getenvBool("RAP_MESH_PRODUCTION_FORWARDING_ENABLED", false), "Enable production forwarding gate; runtime still fail-closed if unavailable.")
|
||||
fs.BoolVar(&cfg.MeshFabricSessionEnabled, "mesh-fabric-session-enabled", getenvBool("RAP_MESH_FABRIC_SESSION_ENABLED", false), "Enable authenticated fabric session endpoint.")
|
||||
fs.BoolVar(&cfg.VPNFabricSessionTransportEnabled, "vpn-fabric-session-transport-enabled", getenvBool("RAP_VPN_FABRIC_SESSION_TRANSPORT_ENABLED", false), "Route VPN packet transport over persistent fabric sessions.")
|
||||
fs.BoolVar(&cfg.MeshQUICFabricEnabled, "mesh-quic-fabric-enabled", getenvBool("RAP_MESH_QUIC_FABRIC_ENABLED", false), "Enable QUIC/UDP fabric listener.")
|
||||
fs.StringVar(&cfg.MeshQUICFabricListenAddr, "mesh-quic-fabric-listen-addr", getenv("RAP_MESH_QUIC_FABRIC_LISTEN_ADDR", ""), "QUIC/UDP fabric listen address.")
|
||||
fs.IntVar(&cfg.VPNFabricSessionStreamShards, "vpn-fabric-session-stream-shards", getenvInt("RAP_VPN_FABRIC_SESSION_STREAM_SHARDS", 4), "VPN fabric-session stream shards per traffic class.")
|
||||
fs.IntVar(&cfg.VPNFabricQUICMaxStreamsPerConn, "vpn-fabric-quic-max-streams-per-conn", getenvInt("RAP_VPN_FABRIC_QUIC_MAX_STREAMS_PER_CONN", 64), "Maximum logical fabric-session streams per cached VPN QUIC carrier connection.")
|
||||
fs.IntVar(&cfg.VPNFabricQUICIdleTTLSeconds, "vpn-fabric-quic-idle-ttl-seconds", getenvInt("RAP_VPN_FABRIC_QUIC_IDLE_TTL_SECONDS", 300), "Idle TTL seconds for cached VPN QUIC carrier connections.")
|
||||
fs.StringVar(&cfg.MeshListenAddr, "mesh-listen-addr", getenv("RAP_MESH_LISTEN_ADDR", ""), "Synthetic mesh HTTP listen address inside container.")
|
||||
fs.StringVar(&cfg.MeshListenAddr, "mesh-listen-addr", getenv("RAP_MESH_LISTEN_ADDR", ""), "Historical synthetic mesh HTTP listen address inside container.")
|
||||
fs.StringVar(&cfg.MeshListenPortMode, "mesh-listen-port-mode", getenv("RAP_MESH_LISTEN_PORT_MODE", ""), "Mesh listen port behavior: manual, auto, or disabled.")
|
||||
fs.IntVar(&cfg.MeshListenAutoPortStart, "mesh-listen-auto-port-start", getenvInt("RAP_MESH_LISTEN_AUTO_PORT_START", 0), "First port used when mesh listen port mode is auto.")
|
||||
fs.IntVar(&cfg.MeshListenAutoPortEnd, "mesh-listen-auto-port-end", getenvInt("RAP_MESH_LISTEN_AUTO_PORT_END", 0), "Last port used when mesh listen port mode is auto.")
|
||||
@@ -941,13 +871,12 @@ func usage() {
|
||||
rap-host-agent install -backend-url URL -cluster-id ID -join-token TOKEN -node-name NAME [docker options]
|
||||
rap-host-agent install-windows -profile-url URL -install-token TOKEN [-node-name NAME] [windows options]
|
||||
rap-host-agent install-linux -profile-url URL -install-token TOKEN [-node-name NAME] [linux/systemd options]
|
||||
rap-host-agent install-updater -backend-url URL -cluster-id ID -state-dir DIR -container-name NAME
|
||||
rap-host-agent update-host-agent -backend-url URL -cluster-id ID -state-dir DIR
|
||||
rap-host-agent update-host-agent-loop -backend-url URL -cluster-id ID -state-dir DIR
|
||||
rap-host-agent monitor-loop -backend-url URL -cluster-id ID -state-dir DIR --watch-container NAME
|
||||
rap-host-agent monitor-once -backend-url URL -cluster-id ID -state-dir DIR --watch-container NAME
|
||||
rap-host-agent fabric-session-smoke -mesh-url URL -token rap_fsn_TOKEN [-authority-payload VALUE -authority-signature VALUE]
|
||||
rap-host-agent update -backend-url URL -cluster-id ID -node-id ID [-container-name NAME]
|
||||
rap-host-agent update-loop -backend-url URL -cluster-id ID -node-id ID [-container-name NAME]
|
||||
rap-host-agent install-updater (-backend-url URL | -fabric-registry-records-json JSON) -cluster-id ID -state-dir DIR -container-name NAME
|
||||
rap-host-agent update-host-agent (-backend-url URL | -fabric-registry-records-json JSON) -cluster-id ID -state-dir DIR
|
||||
rap-host-agent update-host-agent-loop (-backend-url URL | -fabric-registry-records-json JSON) -cluster-id ID -state-dir DIR
|
||||
rap-host-agent monitor-loop (-backend-url URL | -fabric-registry-records-json JSON) -cluster-id ID -state-dir DIR --watch-container NAME
|
||||
rap-host-agent monitor-once (-backend-url URL | -fabric-registry-records-json JSON) -cluster-id ID -state-dir DIR --watch-container NAME
|
||||
rap-host-agent update (-backend-url URL | -fabric-registry-records-json JSON) -cluster-id ID -node-id ID [-container-name NAME]
|
||||
rap-host-agent update-loop (-backend-url URL | -fabric-registry-records-json JSON) -cluster-id ID -node-id ID [-container-name NAME]
|
||||
rap-host-agent status [-container-name NAME]`)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -204,7 +205,7 @@ func TestRouteManagerDecisionsFromControlPlaneConsumesRebuildRouteCommand(t *tes
|
||||
}
|
||||
decision := decisions[0]
|
||||
if decision.RouteID != "route-primary" ||
|
||||
decision.RebuildStatus != "pending_degraded_fallback" ||
|
||||
decision.RebuildStatus != "pending_degraded_route_state" ||
|
||||
decision.DecisionSource != "service_channel_remediation_command" ||
|
||||
decision.RebuildRequestID != "cmd-rebuild" {
|
||||
t.Fatalf("unexpected rebuild remediation decision: %+v", decision)
|
||||
@@ -279,7 +280,6 @@ func TestGatewayTransportForAssignmentUsesFabricSessionWhenEnabled(t *testing.T)
|
||||
&syntheticMeshState{
|
||||
ProductionForwardTransport: noopProductionForwardTransport{},
|
||||
VPNFabricInbox: inbox,
|
||||
VPNFabricSessionPeers: mesh.NewFabricSessionPeerManager(),
|
||||
PeerEndpointCandidates: map[string][]mesh.PeerEndpointCandidate{
|
||||
"entry-1": {{
|
||||
EndpointID: "entry-1-quic",
|
||||
@@ -322,7 +322,6 @@ func TestGatewayTransportForAssignmentFallsBackWhenFabricSessionUnavailable(t *t
|
||||
&syntheticMeshState{
|
||||
ProductionForwardTransport: noopProductionForwardTransport{},
|
||||
VPNFabricInbox: inbox,
|
||||
VPNFabricSessionPeers: mesh.NewFabricSessionPeerManager(),
|
||||
PeerEndpoints: map[string]string{},
|
||||
Routes: []mesh.SyntheticRoute{{
|
||||
RouteID: "route-exit-entry",
|
||||
@@ -424,6 +423,496 @@ func testMainQUICCertSHA256(t *testing.T, config *tls.Config) string {
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func TestFabricControlForwardHandlerUsesRegistryQUICControlAPI(t *testing.T) {
|
||||
tlsConfig := testMainQUICTLSConfig(t)
|
||||
server, err := mesh.StartQUICFabricServer(context.Background(), mesh.QUICFabricServerConfig{
|
||||
ListenAddr: "127.0.0.1:0",
|
||||
TLSConfig: tlsConfig,
|
||||
FabricControlHandler: func(_ context.Context, payload []byte) ([]byte, error) {
|
||||
var req client.RawControlRequest
|
||||
if err := json.Unmarshal(payload, &req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Path != "/auth/login" {
|
||||
return nil, fmt.Errorf("unexpected path %s", req.Path)
|
||||
}
|
||||
return json.Marshal(client.RawControlResponse{StatusCode: 200, Body: json.RawMessage(`{"via":"fabric"}`)})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("start quic fabric server: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
now := time.Now().UTC()
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("generate key: %v", err)
|
||||
}
|
||||
issuer := mesh.FabricRegistryTrustedIssuer{IssuerID: "authority-1", Role: mesh.FabricRegistryAuthorityControl, PublicKey: publicKey}
|
||||
record := mesh.FabricRegistryGossipRecord{
|
||||
SchemaVersion: mesh.FabricRegistryGossipRecordSchema,
|
||||
ClusterID: "cluster-1",
|
||||
Service: mesh.FabricRegistryServiceControlAPI,
|
||||
Scope: mesh.FabricRegistryScopeCluster,
|
||||
Epoch: 1,
|
||||
IssuedAt: now.Add(-time.Minute),
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
IssuerNodeID: "authority-1",
|
||||
IssuerRole: mesh.FabricRegistryAuthorityControl,
|
||||
Endpoints: []mesh.FabricRegistryEndpoint{{
|
||||
EndpointID: "control-a",
|
||||
Address: "quic://" + server.Addr().String(),
|
||||
Transport: "direct_quic",
|
||||
PeerCertSHA256: testMainQUICCertSHA256(t, tlsConfig),
|
||||
}},
|
||||
}
|
||||
signed, err := mesh.SignFabricRegistryGossipRecord(record, issuer, privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("sign registry record: %v", err)
|
||||
}
|
||||
registry := mesh.NewFabricRegistry()
|
||||
if _, _, err := registry.ApplyGossipRecord(signed, mesh.FabricRegistryVerificationPolicy{
|
||||
LocalClusterID: "cluster-1",
|
||||
TrustedIssuers: []mesh.FabricRegistryTrustedIssuer{issuer},
|
||||
RequiredSignatures: 1,
|
||||
Now: now,
|
||||
}, true); err != nil {
|
||||
t.Fatalf("apply registry record: %v", err)
|
||||
}
|
||||
transport := mesh.NewQUICFabricTransport(nil)
|
||||
transport.SetLocalPeerID("node-a")
|
||||
handler := fabricControlForwardHandlerFromMeshState(nil, state.Identity{ClusterID: "cluster-1", NodeID: "node-a"}, &syntheticMeshState{
|
||||
FabricRegistry: registry,
|
||||
VPNFabricQUICTransport: transport,
|
||||
ListenerRuntimeConfig: config.Config{MeshRegion: "test"},
|
||||
})
|
||||
payload, err := handler(context.Background(), []byte(`{"method":"POST","path":"/auth/login","body":{"user":"a"}}`))
|
||||
if err != nil {
|
||||
t.Fatalf("fabric control handler: %v", err)
|
||||
}
|
||||
var response client.RawControlResponse
|
||||
if err := json.Unmarshal(payload, &response); err != nil {
|
||||
t.Fatalf("decode raw control response: %v", err)
|
||||
}
|
||||
if response.StatusCode != 200 || string(response.Body) != `{"via":"fabric"}` {
|
||||
t.Fatalf("response = %+v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeartbeatViaFabricControlUsesRegistryQUICControlAPI(t *testing.T) {
|
||||
tlsConfig := testMainQUICTLSConfig(t)
|
||||
server, err := mesh.StartQUICFabricServer(context.Background(), mesh.QUICFabricServerConfig{
|
||||
ListenAddr: "127.0.0.1:0",
|
||||
TLSConfig: tlsConfig,
|
||||
FabricControlHandler: func(_ context.Context, payload []byte) ([]byte, error) {
|
||||
var req client.RawControlRequest
|
||||
if err := json.Unmarshal(payload, &req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Method != http.MethodPost || req.Path != "/clusters/cluster-1/nodes/node-a/heartbeats" {
|
||||
return nil, fmt.Errorf("unexpected request: %+v", req)
|
||||
}
|
||||
return json.Marshal(client.RawControlResponse{
|
||||
StatusCode: 202,
|
||||
Body: json.RawMessage(`{
|
||||
"heartbeat":{"id":"hb-1"},
|
||||
"testing_flags":{"enabled":true,"synthetic_links_enabled":true,"applied_scopes":["cluster"]},
|
||||
"update_hint":{"schema_version":"rap.node_update_hint.v1","check_now":true,"generation":"gen-1"}
|
||||
}`),
|
||||
})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("start quic fabric server: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
now := time.Now().UTC()
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("generate key: %v", err)
|
||||
}
|
||||
issuer := mesh.FabricRegistryTrustedIssuer{IssuerID: "authority-1", Role: mesh.FabricRegistryAuthorityControl, PublicKey: publicKey}
|
||||
record := mesh.FabricRegistryGossipRecord{
|
||||
SchemaVersion: mesh.FabricRegistryGossipRecordSchema,
|
||||
ClusterID: "cluster-1",
|
||||
Service: mesh.FabricRegistryServiceControlAPI,
|
||||
Scope: mesh.FabricRegistryScopeCluster,
|
||||
Epoch: 1,
|
||||
IssuedAt: now.Add(-time.Minute),
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
IssuerNodeID: "authority-1",
|
||||
IssuerRole: mesh.FabricRegistryAuthorityControl,
|
||||
Endpoints: []mesh.FabricRegistryEndpoint{{
|
||||
EndpointID: "control-a",
|
||||
Address: "quic://" + server.Addr().String(),
|
||||
Transport: "direct_quic",
|
||||
PeerCertSHA256: testMainQUICCertSHA256(t, tlsConfig),
|
||||
}},
|
||||
}
|
||||
signed, err := mesh.SignFabricRegistryGossipRecord(record, issuer, privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("sign registry record: %v", err)
|
||||
}
|
||||
registry := mesh.NewFabricRegistry()
|
||||
if _, _, err := registry.ApplyGossipRecord(signed, mesh.FabricRegistryVerificationPolicy{
|
||||
LocalClusterID: "cluster-1",
|
||||
TrustedIssuers: []mesh.FabricRegistryTrustedIssuer{issuer},
|
||||
RequiredSignatures: 1,
|
||||
Now: now,
|
||||
}, true); err != nil {
|
||||
t.Fatalf("apply registry record: %v", err)
|
||||
}
|
||||
response, viaFabric, err := heartbeatViaFabricControl(context.Background(), state.Identity{ClusterID: "cluster-1", NodeID: "node-a"}, &syntheticMeshState{
|
||||
FabricRegistry: registry,
|
||||
VPNFabricQUICTransport: mesh.NewQUICFabricTransport(nil),
|
||||
}, client.HeartbeatRequest{HealthStatus: "healthy"})
|
||||
if err != nil {
|
||||
t.Fatalf("heartbeat via fabric: %v", err)
|
||||
}
|
||||
if !viaFabric || !response.TestingFlags.Enabled || response.UpdateHint == nil || response.UpdateHint.Generation != "gen-1" {
|
||||
t.Fatalf("unexpected heartbeat response viaFabric=%t response=%+v", viaFabric, response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyntheticMeshConfigRefreshUsesRegistryQUICControlAPI(t *testing.T) {
|
||||
tlsConfig := testMainQUICTLSConfig(t)
|
||||
server, err := mesh.StartQUICFabricServer(context.Background(), mesh.QUICFabricServerConfig{
|
||||
ListenAddr: "127.0.0.1:0",
|
||||
TLSConfig: tlsConfig,
|
||||
FabricControlHandler: func(_ context.Context, payload []byte) ([]byte, error) {
|
||||
var req client.RawControlRequest
|
||||
if err := json.Unmarshal(payload, &req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Method != http.MethodGet || req.Path != "/clusters/cluster-1/nodes/node-a/mesh/synthetic-config" {
|
||||
return nil, fmt.Errorf("unexpected request: %+v", req)
|
||||
}
|
||||
return json.Marshal(client.RawControlResponse{
|
||||
StatusCode: 200,
|
||||
Body: json.RawMessage(`{
|
||||
"synthetic_mesh_config":{
|
||||
"enabled":true,
|
||||
"config_version":"fabric-gen-1",
|
||||
"peer_directory_version":"pd-1",
|
||||
"policy_version":"pol-1",
|
||||
"peer_endpoints":{},
|
||||
"routes":[]
|
||||
}
|
||||
}`),
|
||||
})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("start quic fabric server: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
registry := signedTestControlRegistry(t, "cluster-1", "quic://"+server.Addr().String(), testMainQUICCertSHA256(t, tlsConfig))
|
||||
loaded, err := loadSyntheticMeshConfigRuntime(context.Background(), config.Config{}, state.Identity{ClusterID: "cluster-1", NodeID: "node-a"}, nil, &syntheticMeshState{
|
||||
FabricRegistry: registry,
|
||||
VPNFabricQUICTransport: mesh.NewQUICFabricTransport(nil),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("load synthetic mesh config via fabric: %v", err)
|
||||
}
|
||||
if loaded.Source != "control_plane" || loaded.ConfigVersion != "fabric-gen-1" {
|
||||
t.Fatalf("loaded = %+v", loaded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportMeshLinkUsesRegistryQUICControlAPI(t *testing.T) {
|
||||
tlsConfig := testMainQUICTLSConfig(t)
|
||||
var received client.RawControlRequest
|
||||
server, err := mesh.StartQUICFabricServer(context.Background(), mesh.QUICFabricServerConfig{
|
||||
ListenAddr: "127.0.0.1:0",
|
||||
TLSConfig: tlsConfig,
|
||||
FabricControlHandler: func(_ context.Context, payload []byte) ([]byte, error) {
|
||||
if err := json.Unmarshal(payload, &received); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if received.Method != http.MethodPost || received.Path != "/clusters/cluster-1/mesh/links" {
|
||||
return nil, fmt.Errorf("unexpected request: %+v", received)
|
||||
}
|
||||
return json.Marshal(client.RawControlResponse{StatusCode: 202, Body: json.RawMessage(`{"ok":true}`)})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("start quic fabric server: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
registry := signedTestControlRegistry(t, "cluster-1", "quic://"+server.Addr().String(), testMainQUICCertSHA256(t, tlsConfig))
|
||||
err = reportMeshLink(context.Background(), nil, state.Identity{ClusterID: "cluster-1", NodeID: "node-a"}, &syntheticMeshState{
|
||||
FabricRegistry: registry,
|
||||
VPNFabricQUICTransport: mesh.NewQUICFabricTransport(nil),
|
||||
}, client.MeshLinkObservationRequest{
|
||||
SourceNodeID: "node-a",
|
||||
TargetNodeID: "node-b",
|
||||
LinkStatus: "reachable",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("report mesh link via fabric: %v", err)
|
||||
}
|
||||
if len(received.Body) == 0 || !strings.Contains(string(received.Body), `"target_node_id":"node-b"`) {
|
||||
t.Fatalf("unexpected received body: %s", string(received.Body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportTelemetryUsesRegistryQUICControlAPI(t *testing.T) {
|
||||
tlsConfig := testMainQUICTLSConfig(t)
|
||||
var received client.RawControlRequest
|
||||
server, err := mesh.StartQUICFabricServer(context.Background(), mesh.QUICFabricServerConfig{
|
||||
ListenAddr: "127.0.0.1:0",
|
||||
TLSConfig: tlsConfig,
|
||||
FabricControlHandler: func(_ context.Context, payload []byte) ([]byte, error) {
|
||||
if err := json.Unmarshal(payload, &received); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if received.Method != http.MethodPost || received.Path != "/clusters/cluster-1/nodes/node-a/telemetry" {
|
||||
return nil, fmt.Errorf("unexpected request: %+v", received)
|
||||
}
|
||||
return json.Marshal(client.RawControlResponse{StatusCode: 202, Body: json.RawMessage(`{"ok":true}`)})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("start quic fabric server: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
registry := signedTestControlRegistry(t, "cluster-1", "quic://"+server.Addr().String(), testMainQUICCertSHA256(t, tlsConfig))
|
||||
err = reportTelemetry(context.Background(), nil, state.Identity{ClusterID: "cluster-1", NodeID: "node-a"}, &syntheticMeshState{
|
||||
FabricRegistry: registry,
|
||||
VPNFabricQUICTransport: mesh.NewQUICFabricTransport(nil),
|
||||
}, client.TelemetryRequest{Payload: map[string]any{"fabric": "quic"}})
|
||||
if err != nil {
|
||||
t.Fatalf("report telemetry via fabric: %v", err)
|
||||
}
|
||||
if len(received.Body) == 0 || !strings.Contains(string(received.Body), `"fabric":"quic"`) {
|
||||
t.Fatalf("unexpected received body: %s", string(received.Body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkloadControlUsesRegistryQUICControlAPI(t *testing.T) {
|
||||
tlsConfig := testMainQUICTLSConfig(t)
|
||||
var paths []string
|
||||
server, err := mesh.StartQUICFabricServer(context.Background(), mesh.QUICFabricServerConfig{
|
||||
ListenAddr: "127.0.0.1:0",
|
||||
TLSConfig: tlsConfig,
|
||||
FabricControlHandler: func(_ context.Context, payload []byte) ([]byte, error) {
|
||||
var req client.RawControlRequest
|
||||
if err := json.Unmarshal(payload, &req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths = append(paths, req.Method+" "+req.Path)
|
||||
switch req.Path {
|
||||
case "/clusters/cluster-1/nodes/node-a/workloads/desired":
|
||||
return json.Marshal(client.RawControlResponse{
|
||||
StatusCode: 200,
|
||||
Body: json.RawMessage(`{"desired_workloads":[{"service_type":"vpn-egress","desired_state":"enabled","runtime_mode":"node"}]}`),
|
||||
})
|
||||
case "/clusters/cluster-1/nodes/node-a/workloads/vpn-egress/status":
|
||||
if len(req.Body) == 0 || !strings.Contains(string(req.Body), `"reported_state":"running"`) {
|
||||
return nil, fmt.Errorf("unexpected status body: %s", string(req.Body))
|
||||
}
|
||||
return json.Marshal(client.RawControlResponse{StatusCode: 204, Body: json.RawMessage(`{}`)})
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected request: %+v", req)
|
||||
}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("start quic fabric server: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
registry := signedTestControlRegistry(t, "cluster-1", "quic://"+server.Addr().String(), testMainQUICCertSHA256(t, tlsConfig))
|
||||
meshState := &syntheticMeshState{
|
||||
FabricRegistry: registry,
|
||||
VPNFabricQUICTransport: mesh.NewQUICFabricTransport(nil),
|
||||
}
|
||||
identity := state.Identity{ClusterID: "cluster-1", NodeID: "node-a"}
|
||||
desired, err := desiredWorkloads(context.Background(), nil, identity, meshState)
|
||||
if err != nil {
|
||||
t.Fatalf("desired workloads via fabric: %v", err)
|
||||
}
|
||||
if len(desired) != 1 || desired[0].ServiceType != "vpn-egress" {
|
||||
t.Fatalf("desired = %+v", desired)
|
||||
}
|
||||
if err := reportSingleWorkloadStatus(context.Background(), nil, identity, meshState, "vpn-egress", client.WorkloadStatusRequest{ReportedState: "running"}); err != nil {
|
||||
t.Fatalf("report workload status via fabric: %v", err)
|
||||
}
|
||||
want := []string{
|
||||
"GET /clusters/cluster-1/nodes/node-a/workloads/desired",
|
||||
"POST /clusters/cluster-1/nodes/node-a/workloads/vpn-egress/status",
|
||||
}
|
||||
if !reflect.DeepEqual(paths, want) {
|
||||
t.Fatalf("paths = %+v, want %+v", paths, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminRuntimeProjectionUsesRegistryQUICControlAPI(t *testing.T) {
|
||||
tlsConfig := testMainQUICTLSConfig(t)
|
||||
var received client.RawControlRequest
|
||||
server, err := mesh.StartQUICFabricServer(context.Background(), mesh.QUICFabricServerConfig{
|
||||
ListenAddr: "127.0.0.1:0",
|
||||
TLSConfig: tlsConfig,
|
||||
FabricControlHandler: func(_ context.Context, payload []byte) ([]byte, error) {
|
||||
if err := json.Unmarshal(payload, &received); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if received.Method != http.MethodPost || received.Path != "/clusters/cluster-1/nodes/node-a/admin-runtime/projection" {
|
||||
return nil, fmt.Errorf("unexpected request: %+v", received)
|
||||
}
|
||||
return json.Marshal(client.RawControlResponse{
|
||||
StatusCode: 200,
|
||||
Body: json.RawMessage(`{"schema_version":"rap.admin_runtime_projection.v1","status":"ok","status_code":200,"body":{"page":"cluster"}}`),
|
||||
})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("start quic fabric server: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
registry := signedTestControlRegistry(t, "cluster-1", "quic://"+server.Addr().String(), testMainQUICCertSHA256(t, tlsConfig))
|
||||
projection, err := controlAPIProjectionClient{
|
||||
Identity: state.Identity{ClusterID: "cluster-1", NodeID: "node-a"},
|
||||
MeshState: &syntheticMeshState{
|
||||
FabricRegistry: registry,
|
||||
VPNFabricQUICTransport: mesh.NewQUICFabricTransport(nil),
|
||||
},
|
||||
}.Project(context.Background(), webingress.ControlAPIProjectionRequest{
|
||||
SchemaVersion: "rap.web_ingress_projection.v1",
|
||||
Method: http.MethodGet,
|
||||
Path: "/cluster-admin",
|
||||
Scope: "cluster",
|
||||
ServiceClass: "cluster_admin",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("admin projection via fabric: %v", err)
|
||||
}
|
||||
if projection.StatusCode != 200 || string(projection.Body) != `{"page":"cluster"}` {
|
||||
t.Fatalf("projection = %+v", projection)
|
||||
}
|
||||
if len(received.Body) == 0 || !strings.Contains(string(received.Body), `"service_class":"cluster_admin"`) {
|
||||
t.Fatalf("unexpected received body: %s", string(received.Body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestVPNAssignmentControlUsesRegistryQUICControlAPI(t *testing.T) {
|
||||
tlsConfig := testMainQUICTLSConfig(t)
|
||||
var paths []string
|
||||
server, err := mesh.StartQUICFabricServer(context.Background(), mesh.QUICFabricServerConfig{
|
||||
ListenAddr: "127.0.0.1:0",
|
||||
TLSConfig: tlsConfig,
|
||||
FabricControlHandler: func(_ context.Context, payload []byte) ([]byte, error) {
|
||||
var req client.RawControlRequest
|
||||
if err := json.Unmarshal(payload, &req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths = append(paths, req.Method+" "+req.Path)
|
||||
switch req.Path {
|
||||
case "/clusters/cluster-1/nodes/node-a/vpn/assignments":
|
||||
return json.Marshal(client.RawControlResponse{
|
||||
StatusCode: 200,
|
||||
Body: json.RawMessage(`{"vpn_assignments":[{"vpn_connection_id":"vpn-1","desired_state":"enabled","assignment_reason":"eligible_candidate"}]}`),
|
||||
})
|
||||
case "/clusters/cluster-1/nodes/node-a/vpn/assignments/vpn-1/lease/acquire":
|
||||
return json.Marshal(client.RawControlResponse{
|
||||
StatusCode: 201,
|
||||
Body: json.RawMessage(`{"lease":{"lease_id":"lease-1","owner_node_id":"node-a","lease_generation":1,"status":"active"}}`),
|
||||
})
|
||||
case "/clusters/cluster-1/nodes/node-a/vpn/assignments/vpn-1/lease/lease-1/renew":
|
||||
return json.Marshal(client.RawControlResponse{StatusCode: 204, Body: json.RawMessage(`{}`)})
|
||||
case "/clusters/cluster-1/nodes/node-a/vpn/assignments/vpn-1/status":
|
||||
if len(req.Body) == 0 || !strings.Contains(string(req.Body), `"observed_status":"assigned"`) {
|
||||
return nil, fmt.Errorf("unexpected status body: %s", string(req.Body))
|
||||
}
|
||||
return json.Marshal(client.RawControlResponse{StatusCode: 204, Body: json.RawMessage(`{}`)})
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected request: %+v", req)
|
||||
}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("start quic fabric server: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
registry := signedTestControlRegistry(t, "cluster-1", "quic://"+server.Addr().String(), testMainQUICCertSHA256(t, tlsConfig))
|
||||
meshState := &syntheticMeshState{
|
||||
FabricRegistry: registry,
|
||||
VPNFabricQUICTransport: mesh.NewQUICFabricTransport(nil),
|
||||
}
|
||||
identity := state.Identity{ClusterID: "cluster-1", NodeID: "node-a"}
|
||||
assignments, err := nodeVPNAssignments(context.Background(), nil, identity, meshState)
|
||||
if err != nil {
|
||||
t.Fatalf("vpn assignments via fabric: %v", err)
|
||||
}
|
||||
if len(assignments) != 1 || assignments[0].VPNConnectionID != "vpn-1" {
|
||||
t.Fatalf("assignments = %+v", assignments)
|
||||
}
|
||||
lease, err := acquireNodeVPNAssignmentLease(context.Background(), nil, identity, meshState, "vpn-1", client.NodeVPNAssignmentLeaseAcquireRequest{TTLSeconds: 300})
|
||||
if err != nil {
|
||||
t.Fatalf("acquire lease via fabric: %v", err)
|
||||
}
|
||||
if lease == nil || lease.LeaseID != "lease-1" {
|
||||
t.Fatalf("lease = %+v", lease)
|
||||
}
|
||||
if err := renewNodeVPNAssignmentLease(context.Background(), nil, identity, meshState, "vpn-1", "lease-1", client.NodeVPNAssignmentLeaseRenewRequest{TTLSeconds: 300}); err != nil {
|
||||
t.Fatalf("renew lease via fabric: %v", err)
|
||||
}
|
||||
if err := reportNodeVPNAssignmentStatus(context.Background(), nil, identity, meshState, "vpn-1", client.NodeVPNAssignmentStatusRequest{ObservedStatus: "assigned"}); err != nil {
|
||||
t.Fatalf("report status via fabric: %v", err)
|
||||
}
|
||||
want := []string{
|
||||
"GET /clusters/cluster-1/nodes/node-a/vpn/assignments",
|
||||
"POST /clusters/cluster-1/nodes/node-a/vpn/assignments/vpn-1/lease/acquire",
|
||||
"POST /clusters/cluster-1/nodes/node-a/vpn/assignments/vpn-1/lease/lease-1/renew",
|
||||
"POST /clusters/cluster-1/nodes/node-a/vpn/assignments/vpn-1/status",
|
||||
}
|
||||
if !reflect.DeepEqual(paths, want) {
|
||||
t.Fatalf("paths = %+v, want %+v", paths, want)
|
||||
}
|
||||
}
|
||||
|
||||
func signedTestControlRegistry(t *testing.T, clusterID string, endpoint string, certSHA256 string) *mesh.FabricRegistry {
|
||||
t.Helper()
|
||||
now := time.Now().UTC()
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("generate key: %v", err)
|
||||
}
|
||||
issuer := mesh.FabricRegistryTrustedIssuer{IssuerID: "authority-1", Role: mesh.FabricRegistryAuthorityControl, PublicKey: publicKey}
|
||||
record := mesh.FabricRegistryGossipRecord{
|
||||
SchemaVersion: mesh.FabricRegistryGossipRecordSchema,
|
||||
ClusterID: clusterID,
|
||||
Service: mesh.FabricRegistryServiceControlAPI,
|
||||
Scope: mesh.FabricRegistryScopeCluster,
|
||||
Epoch: 1,
|
||||
IssuedAt: now.Add(-time.Minute),
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
IssuerNodeID: "authority-1",
|
||||
IssuerRole: mesh.FabricRegistryAuthorityControl,
|
||||
Endpoints: []mesh.FabricRegistryEndpoint{{
|
||||
EndpointID: "control-a",
|
||||
Address: endpoint,
|
||||
Transport: "direct_quic",
|
||||
PeerCertSHA256: certSHA256,
|
||||
}},
|
||||
}
|
||||
signed, err := mesh.SignFabricRegistryGossipRecord(record, issuer, privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("sign registry record: %v", err)
|
||||
}
|
||||
registry := mesh.NewFabricRegistry()
|
||||
if _, _, err := registry.ApplyGossipRecord(signed, mesh.FabricRegistryVerificationPolicy{
|
||||
LocalClusterID: clusterID,
|
||||
TrustedIssuers: []mesh.FabricRegistryTrustedIssuer{issuer},
|
||||
RequiredSignatures: 1,
|
||||
Now: now,
|
||||
}, true); err != nil {
|
||||
t.Fatalf("apply registry record: %v", err)
|
||||
}
|
||||
return registry
|
||||
}
|
||||
|
||||
func TestRouteManagerDecisionsFromControlPlaneKeepsExplicitRemediationCommand(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
report := &client.RoutePathDecisionReport{Decisions: []client.RoutePathDecision{{
|
||||
@@ -493,9 +982,10 @@ func TestFabricServiceChannelAccessStatsReportsDataPlaneViolations(t *testing.T)
|
||||
OccurredAt: time.Unix(10, 0).UTC(),
|
||||
})
|
||||
report := stats.Report(time.Unix(20, 0).UTC())
|
||||
if report["backend_fallback_blocked"] != int64(1) ||
|
||||
if report["degraded_compatibility_blocked"] != int64(1) ||
|
||||
report["fabric_route_send_failure"] != int64(1) ||
|
||||
report["last_data_plane_violation_status"] != "fabric_route_send_failed_backend_fallback_blocked" ||
|
||||
report["last_data_plane_violation_status"] != "degraded_compatibility_blocked" ||
|
||||
report["last_data_plane_violation_status_raw"] != "fabric_route_send_failed_backend_fallback_blocked" ||
|
||||
report["last_data_plane_violation_reason"] != "mesh synthetic route not found" {
|
||||
t.Fatalf("unexpected violation report: %+v", report)
|
||||
}
|
||||
@@ -790,7 +1280,56 @@ func TestVerifyEnrollmentBootstrapRejectsPinnedAuthorityMismatch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLoadedSyntheticMeshConfigMigratesLegacyControlPlaneSurfaces(t *testing.T) {
|
||||
func TestLoadFabricRegistryBootstrapAcceptsSignedCandidate(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey: %v", err)
|
||||
}
|
||||
record := mesh.FabricRegistryGossipRecord{
|
||||
SchemaVersion: mesh.FabricRegistryGossipRecordSchema,
|
||||
ClusterID: "cluster-1",
|
||||
Service: mesh.FabricRegistryServiceControlAPI,
|
||||
Scope: mesh.FabricRegistryScopeCluster,
|
||||
Epoch: 1,
|
||||
IssuedAt: now.Add(-time.Minute),
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
IssuerNodeID: "authority-node",
|
||||
IssuerRole: mesh.FabricRegistryAuthorityControl,
|
||||
Endpoints: []mesh.FabricRegistryEndpoint{
|
||||
{EndpointID: "control-a", Address: "quic://control.example.test:19443", Transport: "direct_quic"},
|
||||
},
|
||||
}
|
||||
signed, err := mesh.SignFabricRegistryGossipRecord(record, mesh.FabricRegistryTrustedIssuer{
|
||||
IssuerID: "cluster-authority",
|
||||
Role: mesh.FabricRegistryAuthorityControl,
|
||||
}, privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("sign registry record: %v", err)
|
||||
}
|
||||
raw, err := json.Marshal([]mesh.FabricRegistryGossipRecord{signed})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal registry records: %v", err)
|
||||
}
|
||||
registry, report := loadFabricRegistryBootstrap(config.Config{
|
||||
ClusterAuthorityPublicKey: base64.StdEncoding.EncodeToString(publicKey),
|
||||
FabricRegistryRecordsJSON: string(raw),
|
||||
}, state.Identity{ClusterID: "cluster-1"})
|
||||
if registry == nil || report.Total != 1 || report.Candidate != 1 || report.Rejected != 0 {
|
||||
t.Fatalf("unexpected registry bootstrap report: %+v registry=%v", report, registry)
|
||||
}
|
||||
if _, ok := registry.Active("cluster-1", mesh.FabricRegistryServiceControlAPI, mesh.FabricRegistryScopeCluster, "", now); ok {
|
||||
t.Fatal("bootstrap record should remain candidate until live verification")
|
||||
}
|
||||
if !registry.MarkLiveVerified("cluster-1", mesh.FabricRegistryServiceControlAPI, mesh.FabricRegistryScopeCluster, "", now) {
|
||||
t.Fatal("MarkLiveVerified = false")
|
||||
}
|
||||
if _, ok := registry.Active("cluster-1", mesh.FabricRegistryServiceControlAPI, mesh.FabricRegistryScopeCluster, "", now); !ok {
|
||||
t.Fatal("expected active record after live verification")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLoadedSyntheticMeshConfigMigratesNonQUICControlPlaneSurfaces(t *testing.T) {
|
||||
loaded := loadedSyntheticMeshConfig{
|
||||
PeerEndpoints: map[string]string{
|
||||
"node-a": "https://node-a.example.test:443",
|
||||
@@ -798,7 +1337,7 @@ func TestNormalizeLoadedSyntheticMeshConfigMigratesLegacyControlPlaneSurfaces(t
|
||||
PeerEndpointCandidates: map[string][]mesh.PeerEndpointCandidate{
|
||||
"node-b": {
|
||||
{
|
||||
EndpointID: "node-b-legacy",
|
||||
EndpointID: "node-b-http-migration",
|
||||
NodeID: "node-b",
|
||||
Transport: "direct_http",
|
||||
Address: "https://node-b.example.test:443",
|
||||
@@ -816,7 +1355,7 @@ func TestNormalizeLoadedSyntheticMeshConfigMigratesLegacyControlPlaneSurfaces(t
|
||||
},
|
||||
RendezvousLeases: []mesh.PeerRendezvousLease{
|
||||
{
|
||||
LeaseID: "lease-legacy",
|
||||
LeaseID: "lease-http-migration",
|
||||
PeerNodeID: "node-b",
|
||||
RelayNodeID: "node-r",
|
||||
RelayEndpoint: "http://node-r.example.test:19001",
|
||||
@@ -824,7 +1363,7 @@ func TestNormalizeLoadedSyntheticMeshConfigMigratesLegacyControlPlaneSurfaces(t
|
||||
},
|
||||
},
|
||||
RoutePathDecisions: &client.RoutePathDecisionReport{
|
||||
Decisions: []client.RoutePathDecision{{DecisionID: "decision-legacy", SelectedRelayEndpoint: "http://node-r.example.test:19001"}},
|
||||
Decisions: []client.RoutePathDecision{{DecisionID: "decision-http-migration", SelectedRelayEndpoint: "http://node-r.example.test:19001"}},
|
||||
},
|
||||
}
|
||||
normalizeLoadedSyntheticMeshConfigQUICOnly(&loaded)
|
||||
@@ -849,14 +1388,14 @@ func TestNormalizeLoadedSyntheticMeshConfigMigratesLegacyControlPlaneSurfaces(t
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLoadedSyntheticMeshConfigRejectsUnnormalizedLegacyControlPlaneSurfaces(t *testing.T) {
|
||||
func TestValidateLoadedSyntheticMeshConfigRejectsUnnormalizedNonQUICControlPlaneSurfaces(t *testing.T) {
|
||||
err := validateLoadedSyntheticMeshConfigQUICOnly(loadedSyntheticMeshConfig{
|
||||
RoutePathDecisions: &client.RoutePathDecisionReport{
|
||||
Decisions: []client.RoutePathDecision{{DecisionID: "decision-legacy", SelectedRelayEndpoint: "http://node-r.example.test:19001"}},
|
||||
Decisions: []client.RoutePathDecision{{DecisionID: "decision-http-migration", SelectedRelayEndpoint: "http://node-r.example.test:19001"}},
|
||||
},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "QUIC selected relay endpoint") {
|
||||
t.Fatalf("expected legacy selected relay endpoint rejection, got %v", err)
|
||||
t.Fatalf("expected non-QUIC selected relay endpoint rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -942,7 +1481,6 @@ func TestHeartbeatPayloadIncludesMeshEndpointReport(t *testing.T) {
|
||||
MeshRegion: "eu",
|
||||
MeshSyntheticRuntimeEnabled: true,
|
||||
MeshProductionForwardingEnabled: true,
|
||||
MeshFabricSessionEnabled: true,
|
||||
VPNFabricSessionTransportEnabled: true,
|
||||
VPNFabricSessionStreamShards: 6,
|
||||
VPNFabricQUICMaxStreamsPerConn: 24,
|
||||
@@ -952,7 +1490,6 @@ func TestHeartbeatPayloadIncludesMeshEndpointReport(t *testing.T) {
|
||||
ClusterID: "cluster-1",
|
||||
NodeID: "node-a",
|
||||
}, &syntheticMeshState{
|
||||
VPNFabricSessionPeers: mesh.NewFabricSessionPeerManager(),
|
||||
VPNFabricQUICTransport: func() *mesh.QUICFabricTransport {
|
||||
transport := mesh.NewQUICFabricTransport(nil)
|
||||
transport.MaxStreamsPerConn = 24
|
||||
@@ -1010,8 +1547,7 @@ func TestHeartbeatPayloadIncludesMeshEndpointReport(t *testing.T) {
|
||||
if report, ok := payload.Metadata["vpn_fabric_session_transport_report"].(map[string]any); !ok ||
|
||||
report["packet_payload"] != "rap.vpn_packet_batch.fabric.v1" ||
|
||||
report["transport"] != "fabric_session_binary_frames" ||
|
||||
report["stream_shards_per_class"] != 6 ||
|
||||
report["peer_sessions"] == nil {
|
||||
report["stream_shards_per_class"] != 6 {
|
||||
t.Fatalf("vpn fabric session report missing: %+v", payload.Metadata)
|
||||
} else if report["quic_sessions"] == nil || report["quic_max_streams_per_conn"] != 24 {
|
||||
t.Fatalf("vpn fabric quic session report missing: %+v", report)
|
||||
@@ -1242,14 +1778,14 @@ func TestVPNFabricSessionTargetPrefersRankedQUICCandidate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVPNFabricSessionTargetFallsBackToLegacyPeerEndpoint(t *testing.T) {
|
||||
func TestVPNFabricSessionTargetRejectsNonQUICPeerEndpoint(t *testing.T) {
|
||||
_, ok := vpnFabricSessionTarget(&syntheticMeshState{
|
||||
PeerEndpoints: map[string]string{
|
||||
"node-b": "https://node-b.example.test:443/",
|
||||
},
|
||||
}, "node-b")
|
||||
if ok {
|
||||
t.Fatal("legacy peer endpoint unexpectedly produced a QUIC target")
|
||||
t.Fatal("non-QUIC peer endpoint unexpectedly produced a QUIC target")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1257,7 +1793,7 @@ func TestVPNFabricSessionTargetsIncludeRankedQUICCandidatesWithoutLegacyFallback
|
||||
now := time.Now().UTC()
|
||||
targets := vpnFabricSessionTargets(&syntheticMeshState{
|
||||
PeerEndpoints: map[string]string{
|
||||
"node-b": "https://node-b-legacy.example.test:443/",
|
||||
"node-b": "https://node-b-http-migration.example.test:443/",
|
||||
},
|
||||
PeerEndpointCandidates: map[string][]mesh.PeerEndpointCandidate{
|
||||
"node-b": {
|
||||
@@ -2731,7 +3267,7 @@ func TestWebIngressForwardHandlerFromConfigVerifiesSignedEnvelope(t *testing.T)
|
||||
keyID := "web-key-1"
|
||||
handler := webIngressForwardHandlerFromConfig(config.Config{
|
||||
WebIngressTrustedKeysJSON: webingress.TrustedKeysJSONForPublicKey(keyID, publicKey),
|
||||
}, state.Identity{ClusterID: "cluster-1", NodeID: "node-1"}, nil)
|
||||
}, state.Identity{ClusterID: "cluster-1", NodeID: "node-1"}, nil, nil)
|
||||
if handler == nil {
|
||||
t.Fatal("handler is nil")
|
||||
}
|
||||
@@ -2780,10 +3316,10 @@ func TestWebIngressForwardHandlerFromConfigVerifiesSignedEnvelope(t *testing.T)
|
||||
}
|
||||
|
||||
func TestWebIngressForwardHandlerFromConfigDisabledWithoutTrustedKeys(t *testing.T) {
|
||||
if handler := webIngressForwardHandlerFromConfig(config.Config{}, state.Identity{}, nil); handler != nil {
|
||||
if handler := webIngressForwardHandlerFromConfig(config.Config{}, state.Identity{}, nil, nil); handler != nil {
|
||||
t.Fatal("handler should be nil without trusted keys")
|
||||
}
|
||||
if handler := webIngressForwardHandlerFromConfig(config.Config{WebIngressTrustedKeysJSON: `{"bad":"key"}`}, state.Identity{}, nil); handler != nil {
|
||||
if handler := webIngressForwardHandlerFromConfig(config.Config{WebIngressTrustedKeysJSON: `{"bad":"key"}`}, state.Identity{}, nil, nil); handler != nil {
|
||||
t.Fatal("handler should be nil with invalid trusted keys")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user