fix(gateway): enforce audited production traversal
This commit is contained in:
+267
-22
@@ -14,6 +14,7 @@ import (
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -97,6 +98,13 @@ func TestClientFeedbackUsesFixedProtocolVGFVector(t *testing.T) {
|
||||
if _, err := DecodeClientFeedback([]byte{'F', 'B', 'R', 'K', 0}); !errors.Is(err, ErrProviderMalformed) {
|
||||
t.Fatalf("legacy feedback accepted: %v", err)
|
||||
}
|
||||
disconnected, err := EncodeProviderEvent(ProviderEvent{Kind: ProviderEventDisconnected})
|
||||
if err != nil || hex.EncodeToString(disconnected) != "5647463101130000" {
|
||||
t.Fatalf("disconnected event vector = %x, %v", disconnected, err)
|
||||
}
|
||||
if event, err := DecodeProviderEvent(disconnected); err != nil || event.Kind != ProviderEventDisconnected {
|
||||
t.Fatalf("decoded disconnected event = %#v, %v", event, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilityIntersectionAndBoundedQueue(t *testing.T) {
|
||||
@@ -124,6 +132,24 @@ func TestCapabilityIntersectionAndBoundedQueue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServerRejectsPartiallyConfiguredCapabilities(t *testing.T) {
|
||||
serverTLS, _ := testTLS(t)
|
||||
fake := NewFakeApollo(FakeApolloConfig{Now: time.Now()})
|
||||
server, err := NewServer(ServerConfig{
|
||||
TLSConfig: serverTLS, GatewayID: "gateway-1",
|
||||
Capabilities: protocol.CapabilityProfile{SourceRateControl: "server"},
|
||||
ProviderCapabilities: DefaultCapabilities(),
|
||||
Admission: &oneTimeAdmission{},
|
||||
Provider: fake,
|
||||
})
|
||||
if server != nil {
|
||||
_ = server.Close()
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("partial capability configuration was silently replaced with defaults")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyntheticImpairmentPacingAndResourceBounds(t *testing.T) {
|
||||
payload := make([]byte, 1179*16+1)
|
||||
if _, err := FragmentPayload(ChannelVideo, 1, 0, payload); !errors.Is(err, ErrFrameFragmentedLimit) {
|
||||
@@ -187,10 +213,10 @@ func TestApolloFixturesAndLifecycle(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := <-session.Video(); string(got) != string(video) {
|
||||
if got := <-session.Video(); string(got.Payload) != string(video) {
|
||||
t.Fatalf("video changed: %x", got)
|
||||
}
|
||||
if got := <-session.Audio(); string(got) != string(audio) {
|
||||
if got := <-session.Audio(); string(got.Payload) != string(audio) {
|
||||
t.Fatalf("audio changed: %x", got)
|
||||
}
|
||||
if err := session.Input(context.Background(), InputEvent{Sequence: 1, Device: "keyboard", Code: 7, Pressed: true}); err != nil {
|
||||
@@ -321,11 +347,96 @@ func TestAdmissionQUICMTLSRelayAndCleanup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayTelemetrySeparatesQueueProcessingAndPacing(t *testing.T) {
|
||||
serverTLS, clientTLS := testTLS(t)
|
||||
session := &fakeSession{
|
||||
video: make(chan ProviderMedia, 1),
|
||||
audio: make(chan ProviderMedia),
|
||||
events: make(chan ProviderEvent, 1),
|
||||
clipboardWrites: make(chan string, 1),
|
||||
state: protocol.ProviderState{
|
||||
Version: "1", SessionID: "session-timing", State: ProviderStateStarting,
|
||||
Channels: []string{"video", "audio", "input", "feedback"},
|
||||
},
|
||||
pressed: make(map[string]struct{}),
|
||||
}
|
||||
provider := providerStartFunc(func(context.Context, LaunchRequest) (ProviderSession, error) {
|
||||
enqueuedAt := time.Now()
|
||||
session.video <- ProviderMedia{Payload: bytesRepeat(0x5a, 2000), ReceivedAt: enqueuedAt, EnqueuedAt: enqueuedAt}
|
||||
time.Sleep(60 * time.Millisecond)
|
||||
session.mu.Lock()
|
||||
session.state.State = ProviderStateReady
|
||||
session.mu.Unlock()
|
||||
return session, nil
|
||||
})
|
||||
authority := protocol.SessionAuthority{
|
||||
Version: "1", SessionID: "session-timing", GatewayID: "gateway-1", Audience: "versevdi-gateway",
|
||||
ExpiresAt: time.Now().Add(5 * time.Second).UTC().Format(time.RFC3339Nano),
|
||||
Capabilities: DefaultCapabilities(), ProviderProfile: ProviderProfileApollo,
|
||||
ProviderIdentity: "apollo-fixture-1#sha256:fixture-apollo-1",
|
||||
}
|
||||
admission := &oneTimeAdmission{authority: authority, released: make(chan struct{}), disableClipboard: true}
|
||||
server, err := NewServer(ServerConfig{
|
||||
ListenAddress: "127.0.0.1:0", TLSConfig: serverTLS, GatewayID: authority.GatewayID,
|
||||
Capabilities: DefaultCapabilities(), ProviderCapabilities: DefaultCapabilities(),
|
||||
Admission: admission, Provider: provider, PacerKbps: 24,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
serveDone := make(chan error, 1)
|
||||
go func() { serveDone <- server.Serve(ctx) }()
|
||||
request := protocol.TunnelAdmissionRequest{
|
||||
Version: "1", SessionID: authority.SessionID, GatewayID: authority.GatewayID, Audience: authority.Audience,
|
||||
Grant: strings.Repeat("g", 64), ClientNonce: "nonce-0000000001", DeviceSignature: strings.Repeat("s", 86),
|
||||
Capabilities: DefaultCapabilities(),
|
||||
}
|
||||
client, err := Dial(context.Background(), server.Addr().String(), clientTLS, request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
receiveCtx, receiveCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
for index := byte(0); index < 2; index++ {
|
||||
frame, err := client.ReceiveFrame(receiveCtx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if frame.FragmentIndex != index || frame.FragmentCount != 2 {
|
||||
t.Fatalf("timing frame = %#v", frame)
|
||||
}
|
||||
}
|
||||
receiveCancel()
|
||||
metrics := server.Metrics()
|
||||
if metrics.ProcessingSamples != 1 {
|
||||
t.Fatalf("timing samples = %d, want one provider unit", metrics.ProcessingSamples)
|
||||
}
|
||||
if metrics.MediaPackets != 2 {
|
||||
t.Fatalf("media packets = %d, want two fragments", metrics.MediaPackets)
|
||||
}
|
||||
queue, processing, pacing := time.Duration(metrics.QueueDelayNanos), time.Duration(metrics.ProcessingDelayNanos), time.Duration(metrics.PacingDelayNanos)
|
||||
if queue < 40*time.Millisecond || queue > 150*time.Millisecond {
|
||||
t.Fatalf("queue residence = %s, want the controlled 60ms provider queue wait", queue)
|
||||
}
|
||||
if processing >= 100*time.Millisecond {
|
||||
t.Fatalf("processing = %s, pacing leaked into gateway processing", processing)
|
||||
}
|
||||
if pacing < 500*time.Millisecond {
|
||||
t.Fatalf("pacing = %s, want the controlled scheduler wait", pacing)
|
||||
}
|
||||
_ = client.Close()
|
||||
cancel()
|
||||
_ = server.Close()
|
||||
if err := <-serveDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayRejectsProviderWorkOutsideNegotiatedDecodeProfile(t *testing.T) {
|
||||
serverTLS, clientTLS := testTLS(t)
|
||||
fake := NewFakeApollo(FakeApolloConfig{Now: time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)})
|
||||
capabilities := DefaultCapabilities()
|
||||
capabilities.ClientDecode = "h264-opus"
|
||||
capabilities.ClientDecode = []string{"h264-opus"}
|
||||
authority := protocol.SessionAuthority{
|
||||
Version: "1", SessionID: "session-policy", GatewayID: "gateway-1", Audience: "versevdi-gateway",
|
||||
ExpiresAt: time.Now().Add(5 * time.Second).UTC().Format(time.RFC3339Nano),
|
||||
@@ -371,6 +482,75 @@ func TestGatewayRejectsProviderWorkOutsideNegotiatedDecodeProfile(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayNegotiatesRegisteredProfilesWithIndependentClient(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
clientProfiles []string
|
||||
selected string
|
||||
codec string
|
||||
}{
|
||||
{name: "h264-only", clientProfiles: []string{"h264-opus"}, selected: "h264-opus", codec: "H264"},
|
||||
{name: "hevc-only", clientProfiles: []string{"hevc-opus"}, selected: "hevc-opus", codec: "HEVC"},
|
||||
{name: "policy-selects-hevc", clientProfiles: []string{"h264-opus", "hevc-opus"}, selected: "hevc-opus", codec: "HEVC"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
serverTLS, clientTLS := testTLS(t)
|
||||
fake := NewFakeApollo(FakeApolloConfig{Now: time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)})
|
||||
clientCapabilities := DefaultCapabilities()
|
||||
clientCapabilities.ClientDecode = test.clientProfiles
|
||||
authority := protocol.SessionAuthority{
|
||||
Version: "1", SessionID: "session-profile-" + test.name, GatewayID: "gateway-1", Audience: "versevdi-gateway",
|
||||
ExpiresAt: time.Now().Add(5 * time.Second).UTC().Format(time.RFC3339Nano),
|
||||
Capabilities: clientCapabilities, ProviderProfile: ProviderProfileApollo, ProviderIdentity: fake.config.Identity.Key(),
|
||||
}
|
||||
admission := &oneTimeAdmission{
|
||||
authority: authority, released: make(chan struct{}), disableClipboard: true,
|
||||
streamPolicy: protocol.ProviderStreamPolicy{
|
||||
ResolutionWidth: 1920, ResolutionHeight: 1080, Fps: 60,
|
||||
Codec: test.codec, BitrateKbps: 8000, AudioEnabled: true,
|
||||
},
|
||||
}
|
||||
started := make(chan LaunchRequest, 1)
|
||||
provider := providerStartFunc(func(ctx context.Context, request LaunchRequest) (ProviderSession, error) {
|
||||
started <- request
|
||||
return fake.Start(ctx, request)
|
||||
})
|
||||
server, err := NewServer(ServerConfig{
|
||||
ListenAddress: "127.0.0.1:0", TLSConfig: serverTLS, GatewayID: authority.GatewayID,
|
||||
Capabilities: DefaultCapabilities(), ProviderCapabilities: DefaultCapabilities(),
|
||||
Admission: admission, Provider: provider,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
serveDone := make(chan error, 1)
|
||||
go func() { serveDone <- server.Serve(ctx) }()
|
||||
request := protocol.TunnelAdmissionRequest{
|
||||
Version: "1", SessionID: authority.SessionID, GatewayID: authority.GatewayID, Audience: authority.Audience,
|
||||
Grant: strings.Repeat("g", 64), ClientNonce: "nonce-0000000001", DeviceSignature: strings.Repeat("s", 86),
|
||||
Capabilities: clientCapabilities,
|
||||
}
|
||||
client, err := Dial(context.Background(), server.Addr().String(), clientTLS, request)
|
||||
if err != nil {
|
||||
cancel()
|
||||
_ = server.Close()
|
||||
t.Fatal(err)
|
||||
}
|
||||
launch := <-started
|
||||
if !reflect.DeepEqual(launch.Capabilities.ClientDecode, []string{test.selected}) {
|
||||
t.Fatalf("provider selected profiles = %v", launch.Capabilities.ClientDecode)
|
||||
}
|
||||
_ = client.Close()
|
||||
cancel()
|
||||
_ = server.Close()
|
||||
if err := <-serveDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisteredChannelFramesTraversePublicTransport(t *testing.T) {
|
||||
h := newGatewayTransportHarness(t)
|
||||
|
||||
@@ -525,9 +705,60 @@ func TestProviderTerminationEndsPublicGatewaySession(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEncryptedNativeHostTerminationEndsPublicGatewaySession(t *testing.T) {
|
||||
h := newNativeGatewayLifecycleHarness(t, "session-native-terminal")
|
||||
h.native.handleApolloControlPayload(apolloChannelGeneric, true, sourceSealHostControl(t, h.key, 0, apolloControlTypeTerm, []byte{1, 2, 3, 4}))
|
||||
tryQueueNativeMedia(h.native, h.native.video, []byte("queued-video"))
|
||||
tryQueueNativeMedia(h.native, h.native.audio, []byte("queued-audio"))
|
||||
|
||||
eventCtx, eventCancel := context.WithTimeout(context.Background(), time.Second)
|
||||
event, err := h.client.ReceiveProviderEvent(eventCtx)
|
||||
eventCancel()
|
||||
if err != nil || event.Kind != ProviderEventTerminated {
|
||||
t.Fatalf("native provider termination = %#v, %v", event, err)
|
||||
}
|
||||
tryQueueNativeMedia(h.native, h.native.video, []byte("new-video"))
|
||||
tryQueueNativeMedia(h.native, h.native.audio, []byte("new-audio"))
|
||||
h.assertNoMedia(t)
|
||||
h.waitReleased(t)
|
||||
if states := h.reporter.States(); len(states) == 0 || states[len(states)-1].State != ProviderStateTerminated {
|
||||
t.Fatalf("provider states = %#v", states)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeENetDisconnectQuiescesPublicGatewaySession(t *testing.T) {
|
||||
h := newNativeGatewayLifecycleHarness(t, "session-native-disconnect")
|
||||
h.native.handleApolloDisconnect(ErrProviderDisconnected)
|
||||
tryQueueNativeMedia(h.native, h.native.video, []byte("queued-video"))
|
||||
tryQueueNativeMedia(h.native, h.native.audio, []byte("queued-audio"))
|
||||
|
||||
eventCtx, eventCancel := context.WithTimeout(context.Background(), time.Second)
|
||||
event, err := h.client.ReceiveProviderEvent(eventCtx)
|
||||
eventCancel()
|
||||
if err != nil || event.Kind != ProviderEventDisconnected {
|
||||
t.Fatalf("native provider disconnect = %#v, %v", event, err)
|
||||
}
|
||||
tryQueueNativeMedia(h.native, h.native.video, []byte("new-video"))
|
||||
tryQueueNativeMedia(h.native, h.native.audio, []byte("new-audio"))
|
||||
h.assertNoMedia(t)
|
||||
h.waitReleased(t)
|
||||
if states := h.reporter.States(); len(states) == 0 || states[len(states)-1].State != ProviderStateDisconnected || states[len(states)-1].CleanupPending {
|
||||
t.Fatalf("provider states = %#v", states)
|
||||
}
|
||||
}
|
||||
|
||||
type nativeGatewayLifecycleHarness struct {
|
||||
native *nativeApolloSession
|
||||
key []byte
|
||||
client *Client
|
||||
admission *oneTimeAdmission
|
||||
reporter *recordingProviderStateReporter
|
||||
}
|
||||
|
||||
func newNativeGatewayLifecycleHarness(t *testing.T, sessionID string) nativeGatewayLifecycleHarness {
|
||||
t.Helper()
|
||||
serverTLS, clientTLS := testTLS(t)
|
||||
key := []byte("0123456789abcdef")
|
||||
native := newNativeApolloSession("session-native-terminal")
|
||||
native := newNativeApolloSession(sessionID)
|
||||
control, err := newApolloControlCodec(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -557,7 +788,6 @@ func TestEncryptedNativeHostTerminationEndsPublicGatewaySession(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
serveDone := make(chan error, 1)
|
||||
go func() { serveDone <- server.Serve(ctx) }()
|
||||
request := protocol.TunnelAdmissionRequest{
|
||||
@@ -569,28 +799,39 @@ func TestEncryptedNativeHostTerminationEndsPublicGatewaySession(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
native.handleApolloControlPayload(apolloChannelGeneric, true, sourceSealHostControl(t, key, 0, apolloControlTypeTerm, []byte{1, 2, 3, 4}))
|
||||
eventCtx, eventCancel := context.WithTimeout(context.Background(), time.Second)
|
||||
event, err := client.ReceiveProviderEvent(eventCtx)
|
||||
eventCancel()
|
||||
if err != nil || event.Kind != ProviderEventTerminated {
|
||||
t.Fatalf("native provider termination = %#v, %v", event, err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = client.Close()
|
||||
cancel()
|
||||
_ = server.Close()
|
||||
if err := <-serveDone; err != nil {
|
||||
t.Errorf("serve: %v", err)
|
||||
}
|
||||
})
|
||||
return nativeGatewayLifecycleHarness{native: native, key: key, client: client, admission: admission, reporter: reporter}
|
||||
}
|
||||
|
||||
func (h nativeGatewayLifecycleHarness) waitReleased(t *testing.T) {
|
||||
t.Helper()
|
||||
select {
|
||||
case <-admission.released:
|
||||
case <-h.admission.released:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("native termination did not release admission")
|
||||
t.Fatal("native terminal state did not release admission")
|
||||
}
|
||||
if states := reporter.States(); len(states) == 0 || states[len(states)-1].State != ProviderStateTerminated {
|
||||
t.Fatalf("provider states = %#v", states)
|
||||
}
|
||||
_ = client.Close()
|
||||
_ = server.Close()
|
||||
if err := <-serveDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
func (h nativeGatewayLifecycleHarness) assertNoMedia(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
|
||||
defer cancel()
|
||||
if frame, err := h.client.ReceiveFrame(ctx); err == nil {
|
||||
t.Fatalf("media crossed after native terminal signal: channel=%d payload=%q", frame.Channel, frame.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
func tryQueueNativeMedia(session *nativeApolloSession, channel chan ProviderMedia, payload []byte) {
|
||||
session.enqueueMedia(channel, payload, time.Now())
|
||||
}
|
||||
|
||||
func TestProviderDisconnectEndsPublicGatewaySessionReconnectable(t *testing.T) {
|
||||
h := newGatewayTransportHarnessWithoutClipboard(t)
|
||||
h.drainInitialMedia(t)
|
||||
@@ -752,6 +993,7 @@ type oneTimeAdmission struct {
|
||||
releases atomic.Int64
|
||||
released chan struct{}
|
||||
streamPolicy protocol.ProviderStreamPolicy
|
||||
providerWork *protocol.ProviderSessionWork
|
||||
disableClipboard bool
|
||||
}
|
||||
|
||||
@@ -795,9 +1037,12 @@ func (a *oneTimeAdmission) Admit(context.Context, protocol.TunnelAdmissionReques
|
||||
}
|
||||
|
||||
func (a *oneTimeAdmission) ProviderWork(_ context.Context, authority protocol.SessionAuthority) (protocol.ProviderSessionWork, error) {
|
||||
if authority != a.authority {
|
||||
if !reflect.DeepEqual(authority, a.authority) {
|
||||
return protocol.ProviderSessionWork{}, ErrAdmissionRejected
|
||||
}
|
||||
if a.providerWork != nil {
|
||||
return *a.providerWork, nil
|
||||
}
|
||||
streamPolicy := a.streamPolicy
|
||||
if streamPolicy == (protocol.ProviderStreamPolicy{}) {
|
||||
streamPolicy = protocol.ProviderStreamPolicy{ResolutionWidth: 1920, ResolutionHeight: 1080, Fps: 60, Codec: "H264", BitrateKbps: 8000, AudioEnabled: true}
|
||||
|
||||
Reference in New Issue
Block a user