fix(gateway): close Phase 3C audit gaps
This commit is contained in:
+221
-7
@@ -321,6 +321,56 @@ func TestAdmissionQUICMTLSRelayAndCleanup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
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),
|
||||
Capabilities: capabilities, ProviderProfile: ProviderProfileApollo, ProviderIdentity: fake.config.Identity.Key(),
|
||||
}
|
||||
admission := &oneTimeAdmission{
|
||||
authority: authority, released: make(chan struct{}),
|
||||
streamPolicy: protocol.ProviderStreamPolicy{
|
||||
ResolutionWidth: 2560, ResolutionHeight: 1440, Fps: 120,
|
||||
Codec: "HEVC", BitrateKbps: 40000, AudioEnabled: true,
|
||||
},
|
||||
}
|
||||
server, err := NewServer(ServerConfig{
|
||||
ListenAddress: "127.0.0.1:0", TLSConfig: serverTLS, GatewayID: "gateway-1",
|
||||
Capabilities: capabilities, ProviderCapabilities: capabilities, Admission: admission, Provider: fake,
|
||||
})
|
||||
if err != nil {
|
||||
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{
|
||||
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: capabilities,
|
||||
}
|
||||
if _, err := Dial(context.Background(), server.Addr().String(), clientTLS, request); err == nil {
|
||||
t.Fatal("gateway accepted HEVC provider work for an H.264-only negotiated profile")
|
||||
}
|
||||
select {
|
||||
case <-admission.released:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("gateway did not release rejected provider work")
|
||||
}
|
||||
if session, _ := fake.LastSession().(*fakeSession); session != nil {
|
||||
t.Fatal("provider started before policy/capability rejection")
|
||||
}
|
||||
_ = server.Close()
|
||||
if err := <-serveDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisteredChannelFramesTraversePublicTransport(t *testing.T) {
|
||||
h := newGatewayTransportHarness(t)
|
||||
|
||||
@@ -456,6 +506,116 @@ func TestProviderClipboardAuditWaitsForPublicTransportDelivery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderTerminationEndsPublicGatewaySession(t *testing.T) {
|
||||
h := newGatewayTransportHarnessWithoutClipboard(t)
|
||||
h.drainInitialMedia(t)
|
||||
h.session.EmitEvent(ProviderEvent{Kind: ProviderEventTerminated, Payload: []byte{1, 2, 3, 4}})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
event, err := h.client.ReceiveProviderEvent(ctx)
|
||||
cancel()
|
||||
if err != nil || event.Kind != ProviderEventTerminated {
|
||||
t.Fatalf("provider termination event = %#v, %v", event, err)
|
||||
}
|
||||
h.waitReleased(t)
|
||||
if states := h.reporter.States(); len(states) == 0 || states[len(states)-1].State != ProviderStateTerminated {
|
||||
t.Fatalf("provider states = %#v", states)
|
||||
}
|
||||
h.assertMediaClosed(t)
|
||||
}
|
||||
|
||||
func TestEncryptedNativeHostTerminationEndsPublicGatewaySession(t *testing.T) {
|
||||
serverTLS, clientTLS := testTLS(t)
|
||||
key := []byte("0123456789abcdef")
|
||||
native := newNativeApolloSession("session-native-terminal")
|
||||
control, err := newApolloControlCodec(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
native.control = control
|
||||
close(native.readDone)
|
||||
session := nativeLifecycleSession{native}
|
||||
provider := providerStartFunc(func(context.Context, LaunchRequest) (ProviderSession, error) {
|
||||
if err := native.Ready(context.Background()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return session, nil
|
||||
})
|
||||
authority := protocol.SessionAuthority{
|
||||
Version: "1", SessionID: native.sessionID, 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}
|
||||
reporter := &recordingProviderStateReporter{}
|
||||
server, err := NewServer(ServerConfig{
|
||||
ListenAddress: "127.0.0.1:0", TLSConfig: serverTLS, GatewayID: authority.GatewayID,
|
||||
Capabilities: DefaultCapabilities(), ProviderCapabilities: DefaultCapabilities(),
|
||||
Admission: admission, ProviderStateReporter: reporter, Provider: provider,
|
||||
})
|
||||
if err != nil {
|
||||
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{
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
select {
|
||||
case <-admission.released:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("native termination 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 TestProviderDisconnectEndsPublicGatewaySessionReconnectable(t *testing.T) {
|
||||
h := newGatewayTransportHarnessWithoutClipboard(t)
|
||||
h.drainInitialMedia(t)
|
||||
h.session.Disconnect()
|
||||
|
||||
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)
|
||||
}
|
||||
h.assertMediaClosed(t)
|
||||
}
|
||||
|
||||
func TestProviderTerminalCleanupFailureReportsCleanupPending(t *testing.T) {
|
||||
h := newGatewayTransportHarnessWithoutClipboard(t)
|
||||
h.drainInitialMedia(t)
|
||||
h.session.failure = FakeFailureTerminationTimeout
|
||||
h.session.EmitEvent(ProviderEvent{Kind: ProviderEventTerminated, Payload: []byte{1, 2, 3, 4}})
|
||||
|
||||
h.waitReleased(t)
|
||||
if states := h.reporter.States(); len(states) == 0 || states[len(states)-1].State != ProviderStateCleanup || !states[len(states)-1].CleanupPending {
|
||||
t.Fatalf("provider states = %#v", states)
|
||||
}
|
||||
h.assertMediaClosed(t)
|
||||
}
|
||||
|
||||
func testChannelFrame(flowID string, sequence int64, payload []byte) protocol.ChannelFrame {
|
||||
return protocol.ChannelFrame{Version: "1", FlowID: flowID, Sequence: sequence, Flags: 0, FragmentIndex: 0, FragmentCount: 1, TimestampMs: time.Now().UnixMilli(), Payload: base64.StdEncoding.EncodeToString(payload)}
|
||||
}
|
||||
@@ -468,12 +628,32 @@ type gatewayTransportHarness struct {
|
||||
server *Server
|
||||
}
|
||||
|
||||
type providerStartFunc func(context.Context, LaunchRequest) (ProviderSession, error)
|
||||
|
||||
func (fn providerStartFunc) Start(ctx context.Context, request LaunchRequest) (ProviderSession, error) {
|
||||
return fn(ctx, request)
|
||||
}
|
||||
|
||||
type nativeLifecycleSession struct{ *nativeApolloSession }
|
||||
|
||||
func (s nativeLifecycleSession) Telemetry() ProviderTelemetry {
|
||||
return ProviderTelemetry{State: s.State().State}
|
||||
}
|
||||
|
||||
func newGatewayTransportHarness(t *testing.T) gatewayTransportHarness {
|
||||
return newGatewayTransportHarnessWithClipboard(t, true)
|
||||
}
|
||||
|
||||
func newGatewayTransportHarnessWithoutClipboard(t *testing.T) gatewayTransportHarness {
|
||||
return newGatewayTransportHarnessWithClipboard(t, false)
|
||||
}
|
||||
|
||||
func newGatewayTransportHarnessWithClipboard(t *testing.T, clipboardEnabled bool) gatewayTransportHarness {
|
||||
t.Helper()
|
||||
serverTLS, clientTLS := testTLS(t)
|
||||
fake := NewFakeApollo(FakeApolloConfig{Now: time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)})
|
||||
authority := protocol.SessionAuthority{Version: "1", SessionID: "session-transport", GatewayID: "gateway-1", Audience: "versevdi-gateway", ReconnectSequence: 0, ExpiresAt: time.Now().Add(5 * time.Second).UTC().Format(time.RFC3339Nano), Capabilities: DefaultCapabilities(), ProviderProfile: ProviderProfileApollo, ProviderIdentity: fake.config.Identity.Key()}
|
||||
admission := &oneTimeAdmission{authority: authority, released: make(chan struct{})}
|
||||
admission := &oneTimeAdmission{authority: authority, released: make(chan struct{}), disableClipboard: !clipboardEnabled}
|
||||
reporter := &recordingProviderStateReporter{}
|
||||
server, err := NewServer(ServerConfig{ListenAddress: "127.0.0.1:0", TLSConfig: serverTLS, GatewayID: "gateway-1", Capabilities: DefaultCapabilities(), ProviderCapabilities: DefaultCapabilities(), Admission: admission, ProviderStateReporter: reporter, ClipboardAuditReporter: reporter, Provider: fake})
|
||||
if err != nil {
|
||||
@@ -513,6 +693,28 @@ func (h gatewayTransportHarness) waitReleased(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h gatewayTransportHarness) drainInitialMedia(t *testing.T) {
|
||||
t.Helper()
|
||||
for range 2 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
_, err := h.client.ReceiveFrame(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
t.Fatalf("drain initial media: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h gatewayTransportHarness) assertMediaClosed(t *testing.T) {
|
||||
t.Helper()
|
||||
h.session.EmitVideo([]byte("must-not-forward"))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
if frame, err := h.client.ReceiveFrame(ctx); err == nil {
|
||||
t.Fatalf("media remained open after provider terminal state: %#v", frame)
|
||||
}
|
||||
}
|
||||
|
||||
func testTLS(t *testing.T) (*tls.Config, *tls.Config) {
|
||||
t.Helper()
|
||||
caKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
@@ -545,10 +747,12 @@ func testTLS(t *testing.T) (*tls.Config, *tls.Config) {
|
||||
}
|
||||
|
||||
type oneTimeAdmission struct {
|
||||
used atomic.Bool
|
||||
authority protocol.SessionAuthority
|
||||
releases atomic.Int64
|
||||
released chan struct{}
|
||||
used atomic.Bool
|
||||
authority protocol.SessionAuthority
|
||||
releases atomic.Int64
|
||||
released chan struct{}
|
||||
streamPolicy protocol.ProviderStreamPolicy
|
||||
disableClipboard bool
|
||||
}
|
||||
|
||||
type recordingProviderStateReporter struct {
|
||||
@@ -594,14 +798,24 @@ func (a *oneTimeAdmission) ProviderWork(_ context.Context, authority protocol.Se
|
||||
if authority != a.authority {
|
||||
return protocol.ProviderSessionWork{}, ErrAdmissionRejected
|
||||
}
|
||||
streamPolicy := a.streamPolicy
|
||||
if streamPolicy == (protocol.ProviderStreamPolicy{}) {
|
||||
streamPolicy = protocol.ProviderStreamPolicy{ResolutionWidth: 1920, ResolutionHeight: 1080, Fps: 60, Codec: "H264", BitrateKbps: 8000, AudioEnabled: true}
|
||||
}
|
||||
clipboardPolicy := protocol.ClipboardPolicy{MaxTextBytes: 65536, MaxUpdatesPerMinute: 30}
|
||||
if !a.disableClipboard {
|
||||
clipboardPolicy.ClientToProviderEnabled = true
|
||||
clipboardPolicy.ProviderToClientEnabled = true
|
||||
}
|
||||
return protocol.ProviderSessionWork{
|
||||
Version: "1", SessionID: authority.SessionID, GatewayID: authority.GatewayID,
|
||||
ReconnectSequence: authority.ReconnectSequence, ExpiresAt: authority.ExpiresAt,
|
||||
ProviderProfile: ProviderProfileApollo, ProviderIdentity: authority.ProviderIdentity,
|
||||
PolicyVersionID: "policy-1", ApplicationID: "1", ClientID: "paired-client", ManagementHost: "apollo.test", ManagementPort: 47990,
|
||||
StreamHost: "apollo.test", StreamPort: 47984, ClientCertificatePem: "certificate",
|
||||
StreamPolicy: streamPolicy,
|
||||
StreamHost: "apollo.test", StreamPort: 47984, ClientCertificatePem: "certificate",
|
||||
ClientPrivateKeyPem: "private-key", ServerCertificatePem: "server-certificate",
|
||||
ClipboardPolicy: protocol.ClipboardPolicy{ClientToProviderEnabled: true, ProviderToClientEnabled: true, MaxTextBytes: 65536, MaxUpdatesPerMinute: 30},
|
||||
ClipboardPolicy: clipboardPolicy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user