fix(gateway): close Phase 3C audit gaps
This commit is contained in:
+37
-21
@@ -22,21 +22,46 @@ type apolloAudioAssembler struct {
|
||||
blocks map[uint16]*apolloAudioFECBlock
|
||||
}
|
||||
|
||||
func (a *apolloAudioAssembler) Add(codec *apolloMediaCodec, shard apolloAudioShard) ([][]byte, error) {
|
||||
func (a *apolloAudioAssembler) Add(codec *apolloMediaCodec, shard apolloAudioShard) ([][]byte, bool, error) {
|
||||
if codec == nil || len(shard.payload) == 0 || len(shard.payload) > 1408 || len(shard.payload)%16 != 0 {
|
||||
return nil, errApolloMedia
|
||||
return nil, false, errApolloMedia
|
||||
}
|
||||
if a.blocks == nil {
|
||||
a.blocks = make(map[uint16]*apolloAudioFECBlock)
|
||||
}
|
||||
base := shard.base
|
||||
if base&3 != 0 {
|
||||
return nil, errApolloMedia
|
||||
return nil, false, errApolloMedia
|
||||
}
|
||||
index := 0
|
||||
if shard.parity {
|
||||
if shard.parityIndex >= apolloAudioParityShards {
|
||||
return nil, false, errApolloMedia
|
||||
}
|
||||
index = apolloAudioDataShards + int(shard.parityIndex)
|
||||
} else {
|
||||
index = int(uint16(shard.sequence - base))
|
||||
if index >= apolloAudioDataShards {
|
||||
return nil, false, errApolloMedia
|
||||
}
|
||||
}
|
||||
evicted := false
|
||||
block := a.blocks[base]
|
||||
if block == nil {
|
||||
if len(a.blocks) >= apolloAudioMaximumBlocks {
|
||||
return nil, errApolloMedia
|
||||
var oldest uint16
|
||||
var maximumAge uint16
|
||||
for candidate := range a.blocks {
|
||||
age := base - candidate
|
||||
if age > maximumAge && age < 1<<15 {
|
||||
oldest, maximumAge = candidate, age
|
||||
}
|
||||
}
|
||||
if maximumAge == 0 {
|
||||
return nil, false, errApolloMedia
|
||||
}
|
||||
delete(a.blocks, oldest)
|
||||
evicted = true
|
||||
}
|
||||
block = &apolloAudioFECBlock{base: base}
|
||||
a.blocks[base] = block
|
||||
@@ -44,49 +69,40 @@ func (a *apolloAudioAssembler) Add(codec *apolloMediaCodec, shard apolloAudioSha
|
||||
if block.size == 0 {
|
||||
block.size = len(shard.payload)
|
||||
} else if block.size != len(shard.payload) {
|
||||
return nil, errApolloMedia
|
||||
return nil, evicted, errApolloMedia
|
||||
}
|
||||
index := 0
|
||||
if shard.parity {
|
||||
if shard.parityIndex >= apolloAudioParityShards {
|
||||
return nil, errApolloMedia
|
||||
}
|
||||
index = apolloAudioDataShards + int(shard.parityIndex)
|
||||
if block.haveFEC && (block.timestamp != shard.timestamp || block.ssrc != shard.ssrc) {
|
||||
return nil, errApolloMedia
|
||||
return nil, evicted, errApolloMedia
|
||||
}
|
||||
block.timestamp, block.ssrc, block.haveFEC = shard.timestamp, shard.ssrc, true
|
||||
} else {
|
||||
index = int(uint16(shard.sequence - base))
|
||||
if index >= apolloAudioDataShards {
|
||||
return nil, errApolloMedia
|
||||
}
|
||||
if block.haveFEC && (shard.timestamp != block.timestamp+uint32(index*5) || shard.ssrc != block.ssrc) {
|
||||
return nil, errApolloMedia
|
||||
return nil, evicted, errApolloMedia
|
||||
}
|
||||
}
|
||||
if block.received[index] {
|
||||
return nil, errApolloMedia
|
||||
return nil, evicted, errApolloMedia
|
||||
}
|
||||
block.shards[index] = append([]byte(nil), shard.payload...)
|
||||
block.received[index] = true
|
||||
block.count++
|
||||
if block.count < apolloAudioDataShards {
|
||||
return nil, nil
|
||||
return nil, evicted, nil
|
||||
}
|
||||
if err := reconstructApolloAudioBlock(block); err != nil {
|
||||
return nil, err
|
||||
return nil, evicted, err
|
||||
}
|
||||
output := make([][]byte, apolloAudioDataShards)
|
||||
for index := range output {
|
||||
payload, err := codec.openApolloAudioCipher(base+uint16(index), block.shards[index])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, evicted, err
|
||||
}
|
||||
output[index] = payload
|
||||
}
|
||||
delete(a.blocks, base)
|
||||
return output, nil
|
||||
return output, evicted, nil
|
||||
}
|
||||
|
||||
func reconstructApolloAudioBlock(block *apolloAudioFECBlock) error {
|
||||
|
||||
@@ -43,7 +43,8 @@ func NewNativeApolloBackend() *NativeApolloBackend {
|
||||
|
||||
func (b *NativeApolloBackend) Management(ctx context.Context, request LaunchRequest) ([]byte, error) {
|
||||
work := request.ProviderWork
|
||||
if err := work.Validate(); err != nil || request.SessionID == "" || request.SessionID != work.SessionID || work.ProviderProfile != ProviderProfileApollo {
|
||||
if err := work.Validate(); err != nil || validateApolloStreamPolicy(work.StreamPolicy) != nil ||
|
||||
request.SessionID == "" || request.SessionID != work.SessionID || work.ProviderProfile != ProviderProfileApollo {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
client, err := newPinnedApolloHTTPClient(work)
|
||||
@@ -199,7 +200,8 @@ func pinnedApolloTLSConfig(work protocol.ProviderSessionWork) (*tls.Config, erro
|
||||
|
||||
func (b *NativeApolloBackend) Setup(ctx context.Context, request LaunchRequest) ([]byte, error) {
|
||||
work := request.ProviderWork
|
||||
if err := work.Validate(); err != nil || request.SessionID == "" || request.SessionID != work.SessionID || work.ProviderProfile != ProviderProfileApollo {
|
||||
if err := work.Validate(); err != nil || validateApolloStreamPolicy(work.StreamPolicy) != nil ||
|
||||
request.SessionID == "" || request.SessionID != work.SessionID || work.ProviderProfile != ProviderProfileApollo {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
client, err := newPinnedApolloHTTPClient(work)
|
||||
@@ -287,6 +289,7 @@ type nativeApolloSession struct {
|
||||
state protocol.ProviderState
|
||||
pressed map[string]InputEvent
|
||||
closeOnce sync.Once
|
||||
disconnectOnce sync.Once
|
||||
channelsOnce sync.Once
|
||||
done chan struct{}
|
||||
readDone chan struct{}
|
||||
@@ -493,6 +496,9 @@ func (s *nativeApolloSession) ReleaseAll(ctx context.Context) error {
|
||||
|
||||
func (s *nativeApolloSession) Terminate(ctx context.Context) error {
|
||||
var cleanupErr error
|
||||
s.mu.Lock()
|
||||
disconnected := s.state.State == ProviderStateDisconnected
|
||||
s.mu.Unlock()
|
||||
s.closeOnce.Do(func() {
|
||||
if err := s.ReleaseAll(ctx); err != nil {
|
||||
cleanupErr = err
|
||||
@@ -518,7 +524,7 @@ func (s *nativeApolloSession) Terminate(ctx context.Context) error {
|
||||
}
|
||||
if cleanupErr == nil {
|
||||
s.closeMediaChannels()
|
||||
if s.allowApplicationTermination {
|
||||
if s.allowApplicationTermination && !disconnected {
|
||||
if err := apolloCancelRequest(ctx, s.managementClient, s.managementHost, s.managementPort); err != nil {
|
||||
cleanupErr = err
|
||||
}
|
||||
@@ -536,6 +542,8 @@ func (s *nativeApolloSession) Terminate(ctx context.Context) error {
|
||||
if cleanupErr != nil {
|
||||
s.state.State = ProviderStateCleanup
|
||||
s.state.CleanupPending = true
|
||||
} else if disconnected {
|
||||
s.state.State = ProviderStateDisconnected
|
||||
} else {
|
||||
s.state.State = ProviderStateTerminated
|
||||
}
|
||||
@@ -678,11 +686,19 @@ func (s *nativeApolloSession) handleApolloDisconnect(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
if s.state.State != ProviderStateTerminated {
|
||||
s.disconnectOnce.Do(func() {
|
||||
s.mu.Lock()
|
||||
if s.state.State == ProviderStateTerminated {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.state.State = ProviderStateDisconnected
|
||||
}
|
||||
s.mu.Unlock()
|
||||
s.mu.Unlock()
|
||||
select {
|
||||
case s.events <- ProviderEvent{Kind: ProviderEventDisconnected}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) closeMediaChannels() {
|
||||
@@ -738,7 +754,11 @@ func (s *nativeApolloSession) readUDPMedia() {
|
||||
if openErr != nil {
|
||||
continue
|
||||
}
|
||||
payloads, err = s.audioFEC.Add(s.media, shard)
|
||||
var evicted bool
|
||||
payloads, evicted, err = s.audioFEC.Add(s.media, shard)
|
||||
if evicted {
|
||||
s.mediaDrops.Add(1)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
|
||||
@@ -55,6 +55,7 @@ func TestNativeApolloManagementUsesSessionScopedMTLS(t *testing.T) {
|
||||
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 0,
|
||||
ExpiresAt: "2099-01-01T00:00:00Z", ProviderProfile: ProviderProfileApollo,
|
||||
ProviderIdentity: "apollo-server#sha256:" + hex.EncodeToString(pinned[:]), PolicyVersionID: "policy-1", ApplicationID: "1", ClientID: "paired-client",
|
||||
StreamPolicy: protocol.ProviderStreamPolicy{ResolutionWidth: 1920, ResolutionHeight: 1080, Fps: 60, Codec: "H264", BitrateKbps: 8000, AudioEnabled: true},
|
||||
ManagementHost: host, ManagementPort: port, StreamHost: host, StreamPort: 47984,
|
||||
ClientCertificatePem: certificatePEM(t, clientTLS.Certificates[0]),
|
||||
ClientPrivateKeyPem: privateKeyPEM(t, clientTLS.Certificates[0]),
|
||||
@@ -70,6 +71,32 @@ func TestNativeApolloManagementUsesSessionScopedMTLS(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeApolloSetupRejectsUnsupportedStreamPolicyBeforeProviderReadiness(t *testing.T) {
|
||||
work := protocol.ProviderSessionWork{
|
||||
Version: "1", SessionID: "session-1", GatewayID: "gateway-1",
|
||||
ExpiresAt: "2099-01-01T00:00:00Z", ProviderProfile: ProviderProfileApollo,
|
||||
ProviderIdentity: "provider#sha256:00", PolicyVersionID: "policy-1",
|
||||
ApplicationID: "1", ClientID: "client-1", ManagementHost: "127.0.0.1", ManagementPort: 1,
|
||||
StreamHost: "127.0.0.1", StreamPort: 1, ClientCertificatePem: "invalid",
|
||||
ClientPrivateKeyPem: "invalid", ServerCertificatePem: "invalid",
|
||||
ClipboardPolicy: protocol.ClipboardPolicy{MaxTextBytes: 65536, MaxUpdatesPerMinute: 30},
|
||||
}
|
||||
for name, policy := range map[string]protocol.ProviderStreamPolicy{
|
||||
"audio-disabled": {ResolutionWidth: 1920, ResolutionHeight: 1080, Fps: 60, Codec: "H264", BitrateKbps: 8000, AudioEnabled: false},
|
||||
"av1": {ResolutionWidth: 3840, ResolutionHeight: 2160, Fps: 60, Codec: "AV1", BitrateKbps: 50000, AudioEnabled: true},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
work.StreamPolicy = policy
|
||||
_, err := NewNativeApolloBackend().Setup(context.Background(), LaunchRequest{
|
||||
SessionID: "session-1", ProviderProfile: ProviderProfileApollo, ProviderWork: work,
|
||||
})
|
||||
if !errors.Is(err, ErrProviderMalformed) {
|
||||
t.Fatalf("Setup() error = %v, want ErrProviderMalformed before provider readiness", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeApolloSetupRequiresModernEncryptedRTSPOrder(t *testing.T) {
|
||||
serverTLS, clientTLS := testTLS(t)
|
||||
streamListener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
@@ -158,8 +185,9 @@ func TestNativeApolloSetupRequiresModernEncryptedRTSPOrder(t *testing.T) {
|
||||
}
|
||||
if method == "ANNOUNCE" {
|
||||
for _, required := range []string{
|
||||
"a=x-nv-video[0].clientViewportWd:1920", "a=x-nv-video[0].clientViewportHt:1080", "a=x-nv-video[0].maxFPS:60",
|
||||
"a=x-nv-video[0].packetSize:1024", "a=x-nv-vqos[0].bw.maximumBitrateKbps:8000", "a=x-nv-audio.surround.numChannels:2",
|
||||
"a=x-nv-video[0].clientViewportWd:2560", "a=x-nv-video[0].clientViewportHt:1440", "a=x-nv-video[0].maxFPS:120",
|
||||
"a=x-nv-video[0].packetSize:1024", "a=x-nv-clientSupportHevc:1", "a=x-nv-vqos[0].bitStreamFormat:1",
|
||||
"a=x-nv-vqos[0].bw.maximumBitrateKbps:32000", "a=x-ml-video.configuredBitrateKbps:40000", "a=x-nv-audio.surround.numChannels:2",
|
||||
"a=x-nv-general.useReliableUdp:13", "a=x-ss-general.encryptionEnabled:7",
|
||||
} {
|
||||
if !strings.Contains(string(plaintext), required+"\r\n") {
|
||||
@@ -255,6 +283,7 @@ func TestNativeApolloSetupRequiresModernEncryptedRTSPOrder(t *testing.T) {
|
||||
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 0,
|
||||
ExpiresAt: "2099-01-01T00:00:00Z", ProviderProfile: ProviderProfileApollo,
|
||||
ProviderIdentity: "apollo-server#sha256:" + hex.EncodeToString(pinned[:]), PolicyVersionID: "policy-1", ApplicationID: "42", ClientID: "paired-client",
|
||||
StreamPolicy: protocol.ProviderStreamPolicy{ResolutionWidth: 2560, ResolutionHeight: 1440, Fps: 120, Codec: "HEVC", BitrateKbps: 40000, AudioEnabled: true},
|
||||
ManagementHost: managementHost, ManagementPort: managementPort, StreamHost: streamHost, StreamPort: streamPort,
|
||||
ClientCertificatePem: certificatePEM(t, clientTLS.Certificates[0]), ClientPrivateKeyPem: privateKeyPEM(t, clientTLS.Certificates[0]),
|
||||
ServerCertificatePem: certificatePEM(t, tls.Certificate{Certificate: [][]byte{serverTLS.Certificates[0].Certificate[1]}}),
|
||||
@@ -625,6 +654,33 @@ func TestNativeApolloSessionRelaysOnlyAuthenticatedEncodedUDPMedia(t *testing.T)
|
||||
t.Fatal("encrypted FEC audio was not recovered")
|
||||
}
|
||||
}
|
||||
for block := 0; block < apolloAudioMaximumBlocks+1; block++ {
|
||||
sequence := uint16(100 + block*apolloAudioDataShards)
|
||||
packet := sourceShapedEncryptedAudioPacketWithHeaders(t, key, keyID, sequence, uint32(sequence)*5, 1, []byte{byte(block)})
|
||||
if _, err := audioServer.WriteToUDP(packet, audioClient.LocalAddr().(*net.UDPAddr)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for index, want := range [][]byte{{0xa0}, {0xa1}, {0xa2}, {0xa3}} {
|
||||
sequence := uint16(124 + index)
|
||||
packet := sourceShapedEncryptedAudioPacketWithHeaders(t, key, keyID, sequence, uint32(sequence)*5, 1, want)
|
||||
if _, err := audioServer.WriteToUDP(packet, audioClient.LocalAddr().(*net.UDPAddr)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, want := range [][]byte{{0xa0}, {0xa1}, {0xa2}, {0xa3}} {
|
||||
select {
|
||||
case payload := <-session.Audio():
|
||||
if string(payload) != string(want) {
|
||||
t.Fatalf("post-loss audio relay = %x, want %x", payload, want)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("sustained loss permanently stalled newer audio")
|
||||
}
|
||||
}
|
||||
if drops := session.Telemetry().MediaDrops; drops < 2 {
|
||||
t.Fatalf("stale FEC eviction drops = %d, want at least 2", drops)
|
||||
}
|
||||
terminateCtx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if err := session.Terminate(terminateCtx); err != nil {
|
||||
|
||||
@@ -112,7 +112,10 @@ func (b *NativeApolloBackend) performRTSPHandshake(ctx context.Context, work pro
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
announceBody := apolloAnnounceProfile()
|
||||
announceBody, err := apolloAnnounceProfile(work.StreamPolicy)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
announce, err := request("ANNOUNCE", "streamid=control/13/0", sessionID, []apolloRTSPHeader{{"Content-Type", "application/sdp"}}, announceBody, 6)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -468,18 +471,33 @@ func apolloRTSPConnectData(message apolloRTSPMessage) (uint32, error) {
|
||||
return uint32(parsed), nil
|
||||
}
|
||||
|
||||
func apolloAnnounceProfile() []byte {
|
||||
func apolloAnnounceProfile(policy protocol.ProviderStreamPolicy) ([]byte, error) {
|
||||
if err := validateApolloStreamPolicy(policy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
format, supportsHEVC := int64(0), int64(0)
|
||||
if policy.Codec == "HEVC" {
|
||||
format, supportsHEVC = 1, 1
|
||||
}
|
||||
maximumBitrate := policy.BitrateKbps * 80 / 100
|
||||
if maximumBitrate > 100000 {
|
||||
maximumBitrate = 100000
|
||||
}
|
||||
return []byte("v=0\r\n" +
|
||||
"o=android 0 0 IN IP4 0.0.0.0\r\n" +
|
||||
"s=NVIDIA Streaming Client\r\n" +
|
||||
"a=x-nv-video[0].clientViewportWd:1920\r\n" +
|
||||
"a=x-nv-video[0].clientViewportHt:1080\r\n" +
|
||||
"a=x-nv-video[0].maxFPS:60\r\n" +
|
||||
fmt.Sprintf("a=x-nv-video[0].clientViewportWd:%d\r\n", policy.ResolutionWidth) +
|
||||
fmt.Sprintf("a=x-nv-video[0].clientViewportHt:%d\r\n", policy.ResolutionHeight) +
|
||||
fmt.Sprintf("a=x-nv-video[0].maxFPS:%d\r\n", policy.Fps) +
|
||||
"a=x-nv-video[0].packetSize:1024\r\n" +
|
||||
"a=x-nv-video[0].videoEncoderSlicesPerFrame:1\r\n" +
|
||||
"a=x-nv-video[0].maxNumReferenceFrames:0\r\n" +
|
||||
"a=x-nv-vqos[0].bitStreamFormat:0\r\n" +
|
||||
"a=x-nv-vqos[0].bw.maximumBitrateKbps:8000\r\n" +
|
||||
fmt.Sprintf("a=x-nv-clientSupportHevc:%d\r\n", supportsHEVC) +
|
||||
fmt.Sprintf("a=x-nv-vqos[0].bitStreamFormat:%d\r\n", format) +
|
||||
fmt.Sprintf("a=x-nv-video[0].initialBitrateKbps:%d\r\n", maximumBitrate) +
|
||||
fmt.Sprintf("a=x-nv-video[0].initialPeakBitrateKbps:%d\r\n", maximumBitrate) +
|
||||
fmt.Sprintf("a=x-nv-vqos[0].bw.minimumBitrateKbps:%d\r\n", maximumBitrate) +
|
||||
fmt.Sprintf("a=x-nv-vqos[0].bw.maximumBitrateKbps:%d\r\n", maximumBitrate) +
|
||||
"a=x-nv-vqos[0].fec.minRequiredFecPackets:2\r\n" +
|
||||
"a=x-nv-vqos[0].qosTrafficType:5\r\n" +
|
||||
"a=x-nv-audio.surround.numChannels:2\r\n" +
|
||||
@@ -490,10 +508,17 @@ func apolloAnnounceProfile() []byte {
|
||||
"a=x-nv-general.useReliableUdp:13\r\n" +
|
||||
"a=x-nv-general.featureFlags:167\r\n" +
|
||||
"a=x-ml-general.featureFlags:0\r\n" +
|
||||
"a=x-ml-video.configuredBitrateKbps:8000\r\n" +
|
||||
fmt.Sprintf("a=x-ml-video.configuredBitrateKbps:%d\r\n", policy.BitrateKbps) +
|
||||
"a=x-ss-general.encryptionEnabled:7\r\n" +
|
||||
"a=x-ss-video[0].chromaSamplingType:0\r\n" +
|
||||
"a=x-ss-video[0].intraRefresh:0\r\n")
|
||||
"a=x-ss-video[0].intraRefresh:0\r\n"), nil
|
||||
}
|
||||
|
||||
func validateApolloStreamPolicy(policy protocol.ProviderStreamPolicy) error {
|
||||
if err := policy.Validate(); err != nil || !policy.AudioEnabled || (policy.Codec != "H264" && policy.Codec != "HEVC") {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validApolloRTSPToken(value string) bool {
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
|
||||
var ErrNoCapabilityOverlap = errors.New("no capability overlap")
|
||||
|
||||
const defaultClientDecode = "h264-hevc-opus"
|
||||
|
||||
func DefaultCapabilities() protocol.CapabilityProfile {
|
||||
return protocol.CapabilityProfile{
|
||||
Transport: "quic-tls13",
|
||||
@@ -15,7 +17,7 @@ func DefaultCapabilities() protocol.CapabilityProfile {
|
||||
Media: "encoded",
|
||||
Audio: "encoded",
|
||||
SourceRateControl: "server",
|
||||
ClientDecode: "h264-opus",
|
||||
ClientDecode: defaultClientDecode,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,17 @@ func TestFairPacerEightFlowSharesAndCapacitySteps(t *testing.T) {
|
||||
assertSyntheticCap(t, half, 500_000)
|
||||
}
|
||||
|
||||
func TestFairPacerBoundsCatchupAfterHostStall(t *testing.T) {
|
||||
start := time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
pacer := newFairPacer(8000)
|
||||
_ = pacer.reserveAt(start, "one", 1000)
|
||||
resumed := start.Add(100 * time.Millisecond)
|
||||
next := pacer.reserveAt(resumed, "one", 1000)
|
||||
if next.Before(resumed.Add(-fairPacerMaximumCatchup)) || next.After(resumed.Add(10*time.Millisecond)) {
|
||||
t.Fatalf("post-stall reservation = %s, want bounded catchup near %s", next, resumed)
|
||||
}
|
||||
}
|
||||
|
||||
func runSyntheticPacer(pacer *fairPacer, start, end time.Time, flows []string, next map[string]time.Time) []syntheticPacerDelivery {
|
||||
const packetBytes = 1000
|
||||
for _, flow := range flows {
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -186,6 +186,7 @@ const (
|
||||
ProviderEventTerminated ProviderEventKind = iota + 1
|
||||
ProviderEventRumble
|
||||
ProviderEventHDR
|
||||
ProviderEventDisconnected
|
||||
)
|
||||
|
||||
type ProviderEvent struct {
|
||||
@@ -563,12 +564,17 @@ func (s *fakeSession) Terminate(ctx context.Context) error {
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
disconnected := s.state.State == ProviderStateDisconnected
|
||||
s.state.State = ProviderStateTerminating
|
||||
s.closeOnce.Do(func() {
|
||||
close(s.video)
|
||||
close(s.audio)
|
||||
})
|
||||
s.state.State = ProviderStateTerminated
|
||||
if disconnected {
|
||||
s.state.State = ProviderStateDisconnected
|
||||
} else {
|
||||
s.state.State = ProviderStateTerminated
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
@@ -587,6 +593,7 @@ func (s *fakeSession) Disconnect() {
|
||||
s.mu.Lock()
|
||||
s.state.State = ProviderStateDisconnected
|
||||
s.mu.Unlock()
|
||||
s.EmitEvent(ProviderEvent{Kind: ProviderEventDisconnected})
|
||||
}
|
||||
|
||||
func (s *fakeSession) ReleaseCount() int {
|
||||
|
||||
@@ -54,6 +54,14 @@ func TestQualificationProtocolVersionIsExplicitAndImmutable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestQualificationRecordsLinkedToolVersions(t *testing.T) {
|
||||
versions, err := qualificationToolVersions()
|
||||
if err != nil || versions["qualification"] != qualificationToolVersion ||
|
||||
versions["go"] == "" || versions["quic-go"] == "" {
|
||||
t.Fatalf("qualification tool versions = %#v, %v", versions, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQualificationOutputAndStatisticsFailClosed(t *testing.T) {
|
||||
if err := validateQualificationOutputDir("relative/evidence"); err == nil {
|
||||
t.Fatal("relative evidence directory was accepted")
|
||||
@@ -86,15 +94,16 @@ func TestQualificationOutputAndStatisticsFailClosed(t *testing.T) {
|
||||
|
||||
func TestQualificationShortProcessingWritesRawArtifact(t *testing.T) {
|
||||
profile := qualificationMediaProfile{
|
||||
Name: "smoke", Codec: "h264", BitrateKbps: 1000,
|
||||
Duration: 200 * time.Millisecond, Warmup: time.Millisecond, PacketBytes: 100,
|
||||
Name: "smoke", Codec: "h264", BitrateKbps: 20000,
|
||||
Duration: time.Second, Warmup: time.Millisecond, PacketBytes: 1000,
|
||||
}
|
||||
rawPath := filepath.Join(t.TempDir(), "processing.csv.gz")
|
||||
summary, err := runQualificationProcessing(profile, rawPath)
|
||||
summary, err := runQualificationProcessing(t, profile, rawPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if summary.Count < 1 || summary.RawSamplesSHA256 == "" || summary.RawSamplesBytes < 1 {
|
||||
if summary.Count < 1 || summary.RawSamplesSHA256 == "" || summary.RawSamplesBytes < 1 ||
|
||||
summary.ResourceSamples < 2 || summary.RawResourcesSHA256 == "" || summary.RawResourcesBytes < 1 {
|
||||
t.Fatalf("processing summary = %#v", summary)
|
||||
}
|
||||
file, err := os.Open(rawPath)
|
||||
@@ -119,13 +128,14 @@ func TestQualificationShortProcessingWritesRawArtifact(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestQualificationProcessingPreservesPayload(t *testing.T) {
|
||||
payload := qualificationPayload(qualificationMediaProfiles()[0])
|
||||
processed, elapsed, err := processQualificationPayload(7, payload)
|
||||
profile := qualificationMediaProfiles()[0]
|
||||
payload := qualificationPayload(profile)
|
||||
trace, elapsed, err := newQualificationPath(t, profile.BitrateKbps).traverse(t, payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(processed, payload) {
|
||||
t.Fatal("encoded payload mutated")
|
||||
if !trace.PayloadPreserved || !trace.ApolloRecovered || !trace.VerseQUIC {
|
||||
t.Fatalf("production path trace = %#v", trace)
|
||||
}
|
||||
if elapsed <= 0 {
|
||||
t.Fatalf("processing duration = %s", elapsed)
|
||||
@@ -134,34 +144,66 @@ func TestQualificationProcessingPreservesPayload(t *testing.T) {
|
||||
|
||||
func TestQualificationImpairmentIsDeterministicAndBounded(t *testing.T) {
|
||||
profile := qualificationImpairmentProfiles()[3]
|
||||
first, err := runQualificationImpairment(profile, qualificationMediaProfiles()[0], 10_000)
|
||||
first, err := runQualificationImpairment(t, profile, qualificationMediaProfiles()[0], 1000, filepath.Join(t.TempDir(), "first.csv.gz"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := runQualificationImpairment(profile, qualificationMediaProfiles()[0], 10_000)
|
||||
second, err := runQualificationImpairment(t, profile, qualificationMediaProfiles()[0], 1000, filepath.Join(t.TempDir(), "second.csv.gz"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(first, second) {
|
||||
t.Fatalf("impairment run is not deterministic:\n%#v\n%#v", first, second)
|
||||
if first.Dropped != second.Dropped || first.InjectedReordered != second.InjectedReordered {
|
||||
t.Fatalf("deterministic impairment selection differs:\n%#v\n%#v", first, second)
|
||||
}
|
||||
if first.Sent != 10_000 || first.Delivered+first.Dropped != first.Sent ||
|
||||
first.ObservedLossPercent < 4.8 || first.ObservedLossPercent > 5.2 ||
|
||||
first.MaxQueuePackets > qualificationImpairmentQueuePackets {
|
||||
if first.Sent != 1000 || first.Delivered+first.Dropped != first.Sent ||
|
||||
first.ObservedLossPercent < 3.5 || first.ObservedLossPercent > 6.5 ||
|
||||
first.MaxQueuePackets > qualificationImpairmentQueuePackets || first.RawSamplesSHA256 == "" {
|
||||
t.Fatalf("impairment observation = %#v", first)
|
||||
}
|
||||
if _, err := runQualificationImpairment(profile, qualificationMediaProfiles()[0], qualificationImpairmentMaxPackets+1); err == nil {
|
||||
if _, err := runQualificationImpairment(t, profile, qualificationMediaProfiles()[0], qualificationImpairmentMaxPackets+1, filepath.Join(t.TempDir(), "invalid.csv.gz")); err == nil {
|
||||
t.Fatal("unbounded impairment packet count was accepted")
|
||||
}
|
||||
unknown := profile
|
||||
unknown.Name = "private-simulator"
|
||||
if _, err := runQualificationImpairment(t, unknown, qualificationMediaProfiles()[0], 1, filepath.Join(t.TempDir(), "unknown.csv.gz")); err == nil {
|
||||
t.Fatal("unregistered impairment profile was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQualificationSixImpairmentProfilesTraverseProductionPath(t *testing.T) {
|
||||
profiles := qualificationImpairmentProfiles()
|
||||
if len(profiles) != 6 {
|
||||
t.Fatalf("impairment profile count = %d, want exactly 6", len(profiles))
|
||||
}
|
||||
for _, profile := range profiles {
|
||||
observation, err := runQualificationImpairment(t, profile, qualificationMediaProfiles()[0], 40,
|
||||
filepath.Join(t.TempDir(), profile.Name+".csv.gz"))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", profile.Name, err)
|
||||
}
|
||||
if observation.Profile != profile.Name || observation.Delivered+observation.Dropped != 40 ||
|
||||
observation.RawSamplesSHA256 == "" || observation.MaxQueuePackets > qualificationImpairmentQueuePackets {
|
||||
t.Fatalf("%s observation = %#v", profile.Name, observation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQualificationUsesPublicQUICAndProductionPacer(t *testing.T) {
|
||||
qualificationTraverseProfiles(t, qualificationMediaProfiles())
|
||||
evidence, err := qualificationPacerEvidence()
|
||||
evidence, err := qualificationPacerEvidence(filepath.Join(t.TempDir(), "fairness.csv.gz"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(evidence.PerFlowBytes) != 8 || len(evidence.CapacitySteps) != 2 || evidence.JainIndex < 0.99 {
|
||||
if len(evidence.PerFlowBytes) != 8 || len(evidence.CapacitySteps) != 2 ||
|
||||
len(evidence.Series) != 80 || evidence.RawSamplesSHA256 == "" || evidence.JainIndex < 0.99 {
|
||||
t.Fatalf("pacer evidence = %#v", evidence)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQualificationSmokeTraversesNativeApolloRecoveryQueuePacerAndQUIC(t *testing.T) {
|
||||
trace := qualificationProductionPathSmoke(t, qualificationMediaProfiles()[0])
|
||||
if !trace.ApolloRecovered || !trace.ProductionQueue || !trace.ProductionPacer ||
|
||||
!trace.VerseQUIC || !trace.PayloadPreserved {
|
||||
t.Fatalf("qualification production-path trace = %#v", trace)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -132,6 +132,8 @@ type fairPacerFlow struct {
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
const fairPacerMaximumCatchup = 5 * time.Millisecond
|
||||
|
||||
func newFairPacer(kbps int64) *fairPacer {
|
||||
pacer := &fairPacer{flows: make(map[string]fairPacerFlow)}
|
||||
pacer.setKbps(kbps)
|
||||
@@ -177,9 +179,11 @@ func (p *fairPacer) reserveAt(now time.Time, flow string, bytes int) time.Time {
|
||||
state := p.flows[flow]
|
||||
state.lastSeen = now
|
||||
p.flows[flow] = state
|
||||
base := now
|
||||
if state.next.After(base) {
|
||||
base = state.next
|
||||
base := state.next
|
||||
if base.IsZero() {
|
||||
base = now
|
||||
} else if lag := now.Sub(base); lag > fairPacerMaximumCatchup {
|
||||
base = now.Add(-fairPacerMaximumCatchup)
|
||||
}
|
||||
numerator := int64(bytes) * int64(len(p.flows)) * int64(time.Second)
|
||||
delay := time.Duration((numerator + p.bytesPerSecond - 1) / p.bytesPerSecond)
|
||||
|
||||
+44
-9
@@ -19,13 +19,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHelloLimit = 16 * 1024
|
||||
defaultControlLimit = 128 * 1024
|
||||
clientControlBacklog = 64
|
||||
applicationError = quic.ApplicationErrorCode(0x100)
|
||||
controlFlowID = "control.ack.v1"
|
||||
inputFlowID = "input.sequenced.v1"
|
||||
clipboardFlowID = "clipboard.text.v1"
|
||||
defaultHelloLimit = 16 * 1024
|
||||
defaultControlLimit = 128 * 1024
|
||||
clientControlBacklog = 64
|
||||
applicationError = quic.ApplicationErrorCode(0x100)
|
||||
terminalFeedbackDrain = 100 * time.Millisecond
|
||||
controlFlowID = "control.ack.v1"
|
||||
inputFlowID = "input.sequenced.v1"
|
||||
clipboardFlowID = "clipboard.text.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -324,12 +325,26 @@ func (s *Server) validateProviderWork(work protocol.ProviderSessionWork, authori
|
||||
}
|
||||
if work.SessionID != authority.SessionID || work.GatewayID != authority.GatewayID ||
|
||||
work.ReconnectSequence != authority.ReconnectSequence || work.ExpiresAt != authority.ExpiresAt ||
|
||||
work.ProviderProfile != authority.ProviderProfile {
|
||||
work.ProviderProfile != authority.ProviderProfile || !apolloPolicyMatchesCapabilities(work.StreamPolicy, authority.Capabilities) {
|
||||
return ErrAdmissionRejected
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func apolloPolicyMatchesCapabilities(policy protocol.ProviderStreamPolicy, capabilities protocol.CapabilityProfile) bool {
|
||||
if validateApolloStreamPolicy(policy) != nil || capabilities.Audio != "encoded" {
|
||||
return false
|
||||
}
|
||||
switch policy.Codec {
|
||||
case "H264":
|
||||
return capabilities.ClientDecode == "h264-opus" || capabilities.ClientDecode == defaultClientDecode
|
||||
case "HEVC":
|
||||
return capabilities.ClientDecode == "hevc-opus" || capabilities.ClientDecode == defaultClientDecode
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) addSession(session *gatewaySession) {
|
||||
s.mu.Lock()
|
||||
s.sessions[session] = struct{}{}
|
||||
@@ -358,6 +373,7 @@ type gatewaySession struct {
|
||||
pressed map[string]struct{}
|
||||
sequence atomic.Uint32
|
||||
mediaDrops uint64
|
||||
endReason error
|
||||
result chan error
|
||||
}
|
||||
|
||||
@@ -389,7 +405,7 @@ func (s *gatewaySession) run() {
|
||||
case <-timer.C:
|
||||
s.server.metrics.InputRejected.Add(1)
|
||||
case <-s.ctx.Done():
|
||||
case <-s.result:
|
||||
case s.endReason = <-s.result:
|
||||
}
|
||||
s.cancel()
|
||||
}
|
||||
@@ -404,6 +420,10 @@ func (s *gatewaySession) providerEventLoop() {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if event.Kind == ProviderEventDisconnected {
|
||||
s.result <- ErrProviderDisconnected
|
||||
return
|
||||
}
|
||||
payload, err := EncodeProviderEvent(event)
|
||||
if err == nil {
|
||||
err = s.sendControl(s.sequence.Add(1), payload)
|
||||
@@ -412,6 +432,17 @@ func (s *gatewaySession) providerEventLoop() {
|
||||
s.result <- err
|
||||
return
|
||||
}
|
||||
if event.Kind == ProviderEventTerminated {
|
||||
timer := time.NewTimer(terminalFeedbackDrain)
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
s.result <- ErrProviderTerminated
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -771,6 +802,10 @@ func (s *gatewaySession) cleanup() {
|
||||
_ = s.connection.CloseWithError(applicationError, "session closed")
|
||||
return
|
||||
}
|
||||
if errors.Is(s.endReason, ErrProviderDisconnected) {
|
||||
state.State = ProviderStateDisconnected
|
||||
state.CleanupPending = false
|
||||
}
|
||||
if err := s.server.config.Admission.Release(cleanupCtx, s.authority); err != nil {
|
||||
s.server.metrics.ProviderErrors.Add(1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user