fix(gateway): close Phase 3C audit gaps

This commit is contained in:
sechmachine
2026-07-30 01:46:00 +07:00
parent 040ca73ce9
commit d3852d15f3
23 changed files with 1619 additions and 219 deletions
+221 -7
View File
@@ -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
}