fix(gateway): enforce audited production traversal

This commit is contained in:
sechmachine
2026-07-30 04:48:02 +07:00
parent d3852d15f3
commit 70667d5aee
17 changed files with 1368 additions and 317 deletions
+61 -13
View File
@@ -198,12 +198,16 @@ func pinnedApolloTLSConfig(work protocol.ProviderSessionWork) (*tls.Config, erro
}, nil
}
func (b *NativeApolloBackend) Setup(ctx context.Context, request LaunchRequest) ([]byte, error) {
func (b *NativeApolloBackend) Setup(ctx context.Context, request LaunchRequest, management []byte) ([]byte, error) {
work := request.ProviderWork
if err := work.Validate(); err != nil || validateApolloStreamPolicy(work.StreamPolicy) != nil ||
request.SessionID == "" || request.SessionID != work.SessionID || work.ProviderProfile != ProviderProfileApollo {
return nil, ErrProviderMalformed
}
info, err := ParseManagementXML(management)
if err != nil || validateApolloProviderStreamPolicy(info, work.StreamPolicy) != nil {
return nil, ErrProviderMalformed
}
client, err := newPinnedApolloHTTPClient(work)
if err != nil {
return nil, err
@@ -281,10 +285,11 @@ type nativeApolloSession struct {
audioPing []byte
videoPing []byte
sessionID string
video chan []byte
audio chan []byte
video chan ProviderMedia
audio chan ProviderMedia
events chan ProviderEvent
mu sync.Mutex
mediaMu sync.Mutex
controlMu sync.Mutex
state protocol.ProviderState
pressed map[string]InputEvent
@@ -299,10 +304,15 @@ type nativeApolloSession struct {
allowApplicationTermination bool
terminationErr error
mediaDrops atomic.Uint64
mediaQuiesced atomic.Bool
mediaIngress atomic.Uint64
mediaRecovered atomic.Uint64
mediaEnqueued atomic.Uint64
mediaQueueMaximum atomic.Uint64
}
func newNativeApolloSession(sessionID string) *nativeApolloSession {
return &nativeApolloSession{sessionID: sessionID, video: make(chan []byte, 16), audio: make(chan []byte, 16), events: make(chan ProviderEvent, 16), state: protocol.ProviderState{Version: "1", SessionID: sessionID, State: ProviderStateStarting, Channels: []string{"video", "audio", "input", "feedback"}}, pressed: make(map[string]InputEvent), done: make(chan struct{}), readDone: make(chan struct{})}
return &nativeApolloSession{sessionID: sessionID, video: make(chan ProviderMedia, 16), audio: make(chan ProviderMedia, 16), events: make(chan ProviderEvent, 16), state: protocol.ProviderState{Version: "1", SessionID: sessionID, State: ProviderStateStarting, Channels: []string{"video", "audio", "input", "feedback"}}, pressed: make(map[string]InputEvent), done: make(chan struct{}), readDone: make(chan struct{})}
}
func newNativeApolloProviderSession(ctx context.Context, setup *apolloRTSPSetup) (*nativeApolloSession, error) {
@@ -405,8 +415,8 @@ func (s *nativeApolloSession) Ready(context.Context) error {
return nil
}
func (s *nativeApolloSession) Video() <-chan []byte { return s.video }
func (s *nativeApolloSession) Audio() <-chan []byte { return s.audio }
func (s *nativeApolloSession) Video() <-chan ProviderMedia { return s.video }
func (s *nativeApolloSession) Audio() <-chan ProviderMedia { return s.audio }
func (s *nativeApolloSession) Events() <-chan ProviderEvent { return s.events }
func (s *nativeApolloSession) Input(ctx context.Context, event InputEvent) error {
@@ -646,6 +656,7 @@ func (s *nativeApolloSession) handleApolloControlPayload(_ uint8, _ bool, payloa
s.handleApolloDisconnect(ErrProviderMalformed)
return
}
s.quiesceMedia()
s.mu.Lock()
s.state.State = ProviderStateTerminated
s.mu.Unlock()
@@ -686,6 +697,7 @@ func (s *nativeApolloSession) handleApolloDisconnect(err error) {
if err == nil {
return
}
s.quiesceMedia()
s.disconnectOnce.Do(func() {
s.mu.Lock()
if s.state.State == ProviderStateTerminated {
@@ -701,13 +713,45 @@ func (s *nativeApolloSession) handleApolloDisconnect(err error) {
})
}
func (s *nativeApolloSession) quiesceMedia() {
if !s.mediaQuiesced.CompareAndSwap(false, true) {
return
}
if s.audioConn != nil {
_ = s.audioConn.Close()
}
if s.videoConn != nil {
_ = s.videoConn.Close()
}
}
func (s *nativeApolloSession) closeMediaChannels() {
s.channelsOnce.Do(func() {
s.mediaMu.Lock()
defer s.mediaMu.Unlock()
close(s.video)
close(s.audio)
})
}
func (s *nativeApolloSession) enqueueMedia(output chan ProviderMedia, payload []byte, receivedAt time.Time) bool {
s.mediaMu.Lock()
defer s.mediaMu.Unlock()
if len(payload) == 0 || s.mediaQuiesced.Load() {
return false
}
s.mediaRecovered.Add(1)
media := ProviderMedia{Payload: payload, ReceivedAt: receivedAt, EnqueuedAt: time.Now()}
if pushLatest(output, media) {
s.mediaDrops.Add(1)
}
s.mediaEnqueued.Add(1)
depth := uint64(len(output))
for maximum := s.mediaQueueMaximum.Load(); depth > maximum && !s.mediaQueueMaximum.CompareAndSwap(maximum, depth); maximum = s.mediaQueueMaximum.Load() {
}
return true
}
func (s *nativeApolloSession) readUDPMedia() {
if s.media == nil {
close(s.readDone)
@@ -716,14 +760,18 @@ func (s *nativeApolloSession) readUDPMedia() {
}
var readers sync.WaitGroup
readers.Add(2)
read := func(conn *net.UDPConn, output chan []byte, video bool) {
read := func(conn *net.UDPConn, output chan ProviderMedia, video bool) {
defer readers.Done()
buffer := make([]byte, apolloMediaMaximumPacket+1)
for {
if s.mediaQuiesced.Load() {
return
}
if err := conn.SetReadDeadline(time.Now().Add(250 * time.Millisecond)); err != nil {
return
}
count, err := conn.Read(buffer)
receivedAt := time.Now()
if err != nil {
if networkErr, ok := err.(net.Error); ok && networkErr.Timeout() {
select {
@@ -738,6 +786,10 @@ func (s *nativeApolloSession) readUDPMedia() {
if count > apolloMediaMaximumPacket {
continue
}
if s.mediaQuiesced.Load() {
return
}
s.mediaIngress.Add(1)
var payloads [][]byte
if video {
shard, openErr := s.media.OpenVideo(buffer[:count])
@@ -764,11 +816,7 @@ func (s *nativeApolloSession) readUDPMedia() {
continue
}
for _, payload := range payloads {
if len(payload) != 0 {
if pushLatest(output, payload) {
s.mediaDrops.Add(1)
}
}
s.enqueueMedia(output, payload, receivedAt)
}
}
}
@@ -781,7 +829,7 @@ func (s *nativeApolloSession) readUDPMedia() {
}()
}
func pushLatest(channel chan []byte, payload []byte) bool {
func pushLatest[T any](channel chan T, payload T) bool {
select {
case channel <- payload:
return false
+90 -12
View File
@@ -82,14 +82,18 @@ func TestNativeApolloSetupRejectsUnsupportedStreamPolicyBeforeProviderReadiness(
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},
"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},
"h264-resolution": {ResolutionWidth: 4097, ResolutionHeight: 2160, Fps: 60, Codec: "H264", BitrateKbps: 50000, AudioEnabled: true},
"hevc-resolution": {ResolutionWidth: 8193, ResolutionHeight: 4320, Fps: 60, Codec: "HEVC", BitrateKbps: 80000, AudioEnabled: true},
"fps": {ResolutionWidth: 1920, ResolutionHeight: 1080, Fps: 241, Codec: "H264", BitrateKbps: 8000, AudioEnabled: true},
"bitrate-cap": {ResolutionWidth: 1920, ResolutionHeight: 1080, Fps: 60, Codec: "H264", BitrateKbps: 125001, 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,
})
}, nil)
if !errors.Is(err, ErrProviderMalformed) {
t.Fatalf("Setup() error = %v, want ErrProviderMalformed before provider readiness", err)
}
@@ -97,6 +101,73 @@ func TestNativeApolloSetupRejectsUnsupportedStreamPolicyBeforeProviderReadiness(
}
}
func TestNativeApolloRejectsProviderCapabilityMismatchBeforeInventoryOrLaunch(t *testing.T) {
serverTLS, clientTLS := testTLS(t)
var paths []string
management := httptest.NewUnstartedServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
paths = append(paths, request.URL.Path)
switch request.URL.Path {
case "/serverinfo":
_, _ = response.Write([]byte("<root><uniqueid>apollo-server</uniqueid><ServerCodecModeSupport>1</ServerCodecModeSupport><MaxLumaPixelsHEVC>0</MaxLumaPixelsHEVC></root>"))
case "/applist":
_, _ = response.Write([]byte("<root><App><ID>42</ID></App></root>"))
default:
http.Error(response, "unexpected provider request", http.StatusBadRequest)
}
}))
management.TLS = serverTLS
management.StartTLS()
defer management.Close()
host, portText, err := net.SplitHostPort(management.Listener.Addr().String())
if err != nil {
t.Fatal(err)
}
port, err := strconv.ParseInt(portText, 10, 64)
if err != nil {
t.Fatal(err)
}
pinned := sha256.Sum256(serverTLS.Certificates[0].Certificate[0])
work := protocol.ProviderSessionWork{
Version: "1", SessionID: "session-source-policy", GatewayID: "gateway-1",
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: host, ManagementPort: port, StreamHost: host, StreamPort: 47984,
ClientCertificatePem: certificatePEM(t, clientTLS.Certificates[0]),
ClientPrivateKeyPem: privateKeyPEM(t, clientTLS.Certificates[0]),
ServerCertificatePem: certificatePEM(t, tls.Certificate{Certificate: [][]byte{serverTLS.Certificates[0].Certificate[1]}}),
ClipboardPolicy: protocol.ClipboardPolicy{MaxTextBytes: 65536, MaxUpdatesPerMinute: 30},
}
adapter := NewApolloAdapter(NewNativeApolloBackend(), ProviderIdentity{UniqueID: "apollo-server", Fingerprint: "sha256:" + hex.EncodeToString(pinned[:])})
if _, err := adapter.Start(context.Background(), LaunchRequest{
SessionID: work.SessionID, ProviderProfile: ProviderProfileApollo,
ProviderIdentity: work.ProviderIdentity, ProviderWork: work,
}); err == nil {
t.Fatal("Start() accepted a provider/source policy mismatch")
}
if got := strings.Join(paths, ","); got != "/serverinfo" {
t.Fatalf("provider requests before capability rejection = %s", got)
}
}
func TestApolloProviderCapabilityRejectsHEVCLumaDowngrade(t *testing.T) {
info := ManagementInfo{
ServerCodecModeSupport: 0x101, HasServerCodecModeSupport: true,
MaxLumaPixelsHEVC: 1920 * 1080, HasMaxLumaPixelsHEVC: true,
}
policy := protocol.ProviderStreamPolicy{
ResolutionWidth: 3840, ResolutionHeight: 2160, Fps: 60,
Codec: "HEVC", BitrateKbps: 80000, AudioEnabled: true,
}
if err := validateApolloProviderStreamPolicy(info, policy); !errors.Is(err, ErrProviderMalformed) {
t.Fatalf("provider HEVC luma downgrade error = %v", err)
}
}
func TestNativeApolloSetupRequiresModernEncryptedRTSPOrder(t *testing.T) {
serverTLS, clientTLS := testTLS(t)
streamListener, err := net.Listen("tcp", "127.0.0.1:0")
@@ -213,7 +284,7 @@ func TestNativeApolloSetupRequiresModernEncryptedRTSPOrder(t *testing.T) {
}
switch request.URL.Path {
case "/serverinfo":
_, _ = response.Write([]byte("<root><uniqueid>apollo-server</uniqueid></root>"))
_, _ = response.Write([]byte("<root><uniqueid>apollo-server</uniqueid><ServerCodecModeSupport>257</ServerCodecModeSupport><MaxLumaPixelsHEVC>1869449984</MaxLumaPixelsHEVC></root>"))
case "/applist":
if request.URL.Query().Get("uniqueid") != "paired-client" {
http.Error(response, "wrong client", http.StatusBadRequest)
@@ -291,7 +362,7 @@ func TestNativeApolloSetupRequiresModernEncryptedRTSPOrder(t *testing.T) {
ProviderApplicationTerminationAllowed: true,
}
backend := NewNativeApolloBackend()
response, err := backend.Setup(context.Background(), LaunchRequest{SessionID: "session-1", ProviderProfile: ProviderProfileApollo, ProviderWork: work})
response, err := backend.Setup(context.Background(), LaunchRequest{SessionID: "session-1", ProviderProfile: ProviderProfileApollo, ProviderWork: work}, []byte("<root><uniqueid>apollo-server</uniqueid><ServerCodecModeSupport>257</ServerCodecModeSupport><MaxLumaPixelsHEVC>1869449984</MaxLumaPixelsHEVC></root>"))
if err != nil {
t.Fatalf("Setup() error = %v", err)
}
@@ -474,7 +545,8 @@ func TestNativeApolloSetupRequiresModernEncryptedRTSPOrder(t *testing.T) {
t.Fatal("encrypted host termination was not forwarded")
}
select {
case payload := <-session.Video():
case media := <-session.Video():
payload := media.Payload
if len(payload) != 1001 || payload[0] != 'A' || payload[1000] != 'B' {
t.Fatalf("source-shaped video relay = %x", payload)
}
@@ -482,7 +554,8 @@ func TestNativeApolloSetupRequiresModernEncryptedRTSPOrder(t *testing.T) {
t.Fatal("source-shaped video was not relayed")
}
select {
case payload := <-session.Audio():
case media := <-session.Audio():
payload := media.Payload
if string(payload) != "A" {
t.Fatalf("source-shaped audio relay = %x", payload)
}
@@ -608,7 +681,8 @@ func TestNativeApolloSessionRelaysOnlyAuthenticatedEncodedUDPMedia(t *testing.T)
}
select {
case payload := <-session.Video():
case media := <-session.Video():
payload := media.Payload
if string(payload) != string([]byte{0x01, 0x02, 0x03}) {
t.Fatalf("video relay = %x, want encoded payload", payload)
}
@@ -617,7 +691,8 @@ func TestNativeApolloSessionRelaysOnlyAuthenticatedEncodedUDPMedia(t *testing.T)
}
for _, want := range wantAudio {
select {
case payload := <-session.Audio():
case media := <-session.Audio():
payload := media.Payload
if string(payload) != string(want) {
t.Fatalf("audio relay = %x, want %x", payload, want)
}
@@ -632,7 +707,8 @@ func TestNativeApolloSessionRelaysOnlyAuthenticatedEncodedUDPMedia(t *testing.T)
}
}
select {
case payload := <-session.Video():
case media := <-session.Video():
payload := media.Payload
if len(payload) != 1001 || payload[0] != 'A' || payload[999] != 'A' || payload[1000] != 'B' {
t.Fatalf("FEC video relay = %x", payload)
}
@@ -646,7 +722,8 @@ func TestNativeApolloSessionRelaysOnlyAuthenticatedEncodedUDPMedia(t *testing.T)
}
for _, want := range [][]byte{{'A'}, {'B'}, {'C'}, {'D'}} {
select {
case payload := <-session.Audio():
case media := <-session.Audio():
payload := media.Payload
if string(payload) != string(want) {
t.Fatalf("FEC audio relay = %x, want %x", payload, want)
}
@@ -670,7 +747,8 @@ func TestNativeApolloSessionRelaysOnlyAuthenticatedEncodedUDPMedia(t *testing.T)
}
for _, want := range [][]byte{{0xa0}, {0xa1}, {0xa2}, {0xa3}} {
select {
case payload := <-session.Audio():
case media := <-session.Audio():
payload := media.Payload
if string(payload) != string(want) {
t.Fatalf("post-loss audio relay = %x, want %x", payload, want)
}
+26 -1
View File
@@ -515,7 +515,32 @@ func apolloAnnounceProfile(policy protocol.ProviderStreamPolicy) ([]byte, error)
}
func validateApolloStreamPolicy(policy protocol.ProviderStreamPolicy) error {
if err := policy.Validate(); err != nil || !policy.AudioEnabled || (policy.Codec != "H264" && policy.Codec != "HEVC") {
if err := policy.Validate(); err != nil || !policy.AudioEnabled || policy.BitrateKbps > 125000 ||
(policy.Codec != "H264" && policy.Codec != "HEVC") {
return ErrProviderMalformed
}
if (policy.Codec == "H264" && (policy.ResolutionWidth > 4096 || policy.ResolutionHeight > 4096)) ||
(policy.Codec == "HEVC" && (policy.ResolutionWidth > 8192 || policy.ResolutionHeight > 8192)) {
return ErrProviderMalformed
}
return nil
}
func validateApolloProviderStreamPolicy(info ManagementInfo, policy protocol.ProviderStreamPolicy) error {
if validateApolloStreamPolicy(policy) != nil || !info.HasServerCodecModeSupport || !info.HasMaxLumaPixelsHEVC {
return ErrProviderMalformed
}
switch policy.Codec {
case "H264":
if info.ServerCodecModeSupport&0x1 == 0 {
return ErrProviderMalformed
}
case "HEVC":
luma := uint64(policy.ResolutionWidth) * uint64(policy.ResolutionHeight)
if info.ServerCodecModeSupport&0x100 == 0 || info.MaxLumaPixelsHEVC == 0 || luma > info.MaxLumaPixelsHEVC {
return ErrProviderMalformed
}
default:
return ErrProviderMalformed
}
return nil
+8 -20
View File
@@ -8,8 +8,6 @@ import (
var ErrNoCapabilityOverlap = errors.New("no capability overlap")
const defaultClientDecode = "h264-hevc-opus"
func DefaultCapabilities() protocol.CapabilityProfile {
return protocol.CapabilityProfile{
Transport: "quic-tls13",
@@ -17,29 +15,19 @@ func DefaultCapabilities() protocol.CapabilityProfile {
Media: "encoded",
Audio: "encoded",
SourceRateControl: "server",
ClientDecode: defaultClientDecode,
ClientDecode: []string{"hevc-opus", "h264-opus"},
}
}
func capabilityProfileUnset(profile protocol.CapabilityProfile) bool {
return profile.Transport == "" && profile.Framing == "" && profile.Media == "" &&
profile.Audio == "" && profile.SourceRateControl == "" && len(profile.ClientDecode) == 0
}
func IntersectCapabilities(profiles ...protocol.CapabilityProfile) (protocol.CapabilityProfile, error) {
if len(profiles) == 0 {
selected, err := protocol.IntersectCapabilityProfiles(profiles...)
if err != nil {
return protocol.CapabilityProfile{}, ErrNoCapabilityOverlap
}
for _, profile := range profiles {
if err := profile.Validate(); err != nil {
return protocol.CapabilityProfile{}, ErrNoCapabilityOverlap
}
}
selected := profiles[0]
for _, profile := range profiles[1:] {
if selected.Transport != profile.Transport ||
selected.Framing != profile.Framing ||
selected.Media != profile.Media ||
selected.Audio != profile.Audio ||
selected.SourceRateControl != profile.SourceRateControl ||
selected.ClientDecode != profile.ClientDecode {
return protocol.CapabilityProfile{}, ErrNoCapabilityOverlap
}
}
return selected, nil
}
+19 -8
View File
@@ -10,14 +10,15 @@ const (
)
const (
gatewayFeedbackHeaderSize = 8
gatewayFeedbackClient = 0
gatewayFeedbackGateway = 1
gatewayFeedbackIDR = 1
gatewayFeedbackFEC = 2
gatewayFeedbackTerminated = 0x10
gatewayFeedbackRumble = 0x11
gatewayFeedbackHDR = 0x12
gatewayFeedbackHeaderSize = 8
gatewayFeedbackClient = 0
gatewayFeedbackGateway = 1
gatewayFeedbackIDR = 1
gatewayFeedbackFEC = 2
gatewayFeedbackTerminated = 0x10
gatewayFeedbackRumble = 0x11
gatewayFeedbackHDR = 0x12
gatewayFeedbackDisconnected = 0x13
)
type gatewayFeedbackMessage struct {
@@ -43,6 +44,11 @@ func EncodeProviderEvent(event ProviderEvent) ([]byte, error) {
return nil, ErrProviderMalformed
}
return encodeGatewayFeedback(gatewayFeedbackGateway, gatewayFeedbackHDR, event.Payload)
case ProviderEventDisconnected:
if len(event.Payload) != 0 {
return nil, ErrProviderMalformed
}
return encodeGatewayFeedback(gatewayFeedbackGateway, gatewayFeedbackDisconnected, nil)
default:
return nil, ErrProviderMalformed
}
@@ -120,6 +126,11 @@ func DecodeProviderEvent(data []byte) (ProviderEvent, error) {
return ProviderEvent{}, ErrProviderMalformed
}
return ProviderEvent{Kind: ProviderEventHDR, Payload: message.payload}, nil
case gatewayFeedbackDisconnected:
if len(message.payload) != 0 {
return ProviderEvent{}, ErrProviderMalformed
}
return ProviderEvent{Kind: ProviderEventDisconnected}, nil
default:
return ProviderEvent{}, ErrProviderMalformed
}
+267 -22
View File
@@ -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}
+52 -18
View File
@@ -5,6 +5,7 @@ import (
"encoding/xml"
"errors"
"fmt"
"strconv"
"strings"
"sync"
"time"
@@ -66,8 +67,12 @@ func (i ProviderIdentity) Validate(now time.Time, expected ProviderIdentity) err
}
type ManagementInfo struct {
Identity ProviderIdentity
Name string
Identity ProviderIdentity
Name string
ServerCodecModeSupport uint32
MaxLumaPixelsHEVC uint64
HasServerCodecModeSupport bool
HasMaxLumaPixelsHEVC bool
}
func ParseManagementXML(data []byte) (ManagementInfo, error) {
@@ -82,6 +87,8 @@ func ParseManagementXML(data []byte) (ManagementInfo, error) {
NotBefore string `xml:"not_before"`
NotAfter string `xml:"not_after"`
Name string `xml:"name"`
CodecModes string `xml:"ServerCodecModeSupport"`
MaxHEVCLuma string `xml:"MaxLumaPixelsHEVC"`
}
decoder := xml.NewDecoder(strings.NewReader(string(data)))
decoder.Strict = true
@@ -108,7 +115,24 @@ func ParseManagementXML(data []byte) (ManagementInfo, error) {
if identity.UniqueID == "" || len(identity.UniqueID) > 128 || len(identity.Fingerprint) > 256 {
return ManagementInfo{}, ErrProviderMalformed
}
return ManagementInfo{Identity: identity, Name: document.Name}, nil
info := ManagementInfo{Identity: identity, Name: document.Name}
if document.CodecModes != "" {
value, parseErr := strconv.ParseUint(document.CodecModes, 10, 32)
if parseErr != nil {
return ManagementInfo{}, ErrProviderMalformed
}
info.ServerCodecModeSupport = uint32(value)
info.HasServerCodecModeSupport = true
}
if document.MaxHEVCLuma != "" {
value, parseErr := strconv.ParseUint(document.MaxHEVCLuma, 10, 64)
if parseErr != nil {
return ManagementInfo{}, ErrProviderMalformed
}
info.MaxLumaPixelsHEVC = value
info.HasMaxLumaPixelsHEVC = true
}
return info, nil
}
type RTSPResponse struct {
@@ -206,14 +230,20 @@ type ProviderTelemetry struct {
MediaDrops uint64
}
type ProviderMedia struct {
Payload []byte
ReceivedAt time.Time
EnqueuedAt time.Time
}
type Provider interface {
Start(context.Context, LaunchRequest) (ProviderSession, error)
}
type ProviderSession interface {
Ready(context.Context) error
Video() <-chan []byte
Audio() <-chan []byte
Video() <-chan ProviderMedia
Audio() <-chan ProviderMedia
Events() <-chan ProviderEvent
Input(context.Context, InputEvent) error
Feedback(context.Context, Feedback) error
@@ -227,7 +257,7 @@ type ProviderSession interface {
type ApolloBackend interface {
Management(context.Context, LaunchRequest) ([]byte, error)
Setup(context.Context, LaunchRequest) ([]byte, error)
Setup(context.Context, LaunchRequest, []byte) ([]byte, error)
Open(context.Context, LaunchRequest, RTSPResponse) (ProviderSession, error)
}
@@ -268,7 +298,7 @@ func (a *ApolloAdapter) Start(ctx context.Context, request LaunchRequest) (Provi
if request.ProviderIdentity != "" && info.Identity.UniqueID != expected.UniqueID {
return nil, ErrProviderIdentity
}
rawRTSP, err := a.backend.Setup(ctx, request)
rawRTSP, err := a.backend.Setup(ctx, request, management)
if err != nil {
return nil, err
}
@@ -352,7 +382,7 @@ func (f *FakeApollo) Management(context.Context, LaunchRequest) ([]byte, error)
return []byte(fmt.Sprintf("<root><unique_id>%s</unique_id><fingerprint>%s</fingerprint><not_before>%s</not_before><not_after>%s</not_after><name>fixture-apollo</name></root>", identity.UniqueID, identity.Fingerprint, f.config.Now.Add(-time.Hour).Format(time.RFC3339), f.config.Now.Add(time.Hour).Format(time.RFC3339))), nil
}
func (f *FakeApollo) Setup(context.Context, LaunchRequest) ([]byte, error) {
func (f *FakeApollo) Setup(context.Context, LaunchRequest, []byte) ([]byte, error) {
if f.config.Failure == FakeFailureMalformed {
return []byte("RTSP/1.0 200 OK\r\n\r\n"), nil
}
@@ -362,8 +392,8 @@ func (f *FakeApollo) Setup(context.Context, LaunchRequest) ([]byte, error) {
func (f *FakeApollo) Open(_ context.Context, request LaunchRequest, _ RTSPResponse) (ProviderSession, error) {
session := &fakeSession{
failure: f.config.Failure,
video: make(chan []byte, 16),
audio: make(chan []byte, 16),
video: make(chan ProviderMedia, 16),
audio: make(chan ProviderMedia, 16),
events: make(chan ProviderEvent, 16),
clipboardWrites: make(chan string, 1),
state: protocol.ProviderState{Version: "1", SessionID: request.SessionID, State: ProviderStateStarting, Channels: []string{"video", "audio", "input", "feedback"}},
@@ -405,8 +435,8 @@ func (f *FakeApollo) DisconnectProvider() {
type fakeSession struct {
mu sync.Mutex
failure FakeFailure
video chan []byte
audio chan []byte
video chan ProviderMedia
audio chan ProviderMedia
events chan ProviderEvent
state protocol.ProviderState
pressed map[string]struct{}
@@ -432,8 +462,8 @@ func (s *fakeSession) Ready(ctx context.Context) error {
return nil
}
func (s *fakeSession) Video() <-chan []byte { return s.video }
func (s *fakeSession) Audio() <-chan []byte { return s.audio }
func (s *fakeSession) Video() <-chan ProviderMedia { return s.video }
func (s *fakeSession) Audio() <-chan ProviderMedia { return s.audio }
func (s *fakeSession) Events() <-chan ProviderEvent { return s.events }
func (s *fakeSession) EmitEvent(event ProviderEvent) {
@@ -449,15 +479,17 @@ func (s *fakeSession) EmitVideo(payload []byte) {
if s.state.State == ProviderStateTerminating || s.state.State == ProviderStateTerminated || s.state.State == ProviderStateDisconnected {
return
}
now := time.Now()
media := ProviderMedia{Payload: append([]byte(nil), payload...), ReceivedAt: now, EnqueuedAt: now}
select {
case s.video <- append([]byte(nil), payload...):
case s.video <- media:
default:
select {
case <-s.video:
default:
}
select {
case s.video <- append([]byte(nil), payload...):
case s.video <- media:
default:
}
}
@@ -469,15 +501,17 @@ func (s *fakeSession) EmitAudio(payload []byte) {
if s.state.State == ProviderStateTerminating || s.state.State == ProviderStateTerminated || s.state.State == ProviderStateDisconnected {
return
}
now := time.Now()
media := ProviderMedia{Payload: append([]byte(nil), payload...), ReceivedAt: now, EnqueuedAt: now}
select {
case s.audio <- append([]byte(nil), payload...):
case s.audio <- media:
default:
select {
case <-s.audio:
default:
}
select {
case s.audio <- append([]byte(nil), payload...):
case s.audio <- media:
default:
}
}
+3 -3
View File
@@ -130,7 +130,7 @@ func TestQualificationShortProcessingWritesRawArtifact(t *testing.T) {
func TestQualificationProcessingPreservesPayload(t *testing.T) {
profile := qualificationMediaProfiles()[0]
payload := qualificationPayload(profile)
trace, elapsed, err := newQualificationPath(t, profile.BitrateKbps).traverse(t, payload)
trace, elapsed, err := newQualificationPath(t, profile, profile.BitrateKbps).traverse(t, payload)
if err != nil {
t.Fatal(err)
}
@@ -190,12 +190,12 @@ func TestQualificationSixImpairmentProfilesTraverseProductionPath(t *testing.T)
func TestQualificationUsesPublicQUICAndProductionPacer(t *testing.T) {
qualificationTraverseProfiles(t, qualificationMediaProfiles())
evidence, err := qualificationPacerEvidence(filepath.Join(t.TempDir(), "fairness.csv.gz"))
evidence, err := qualificationPacerEvidence(t, filepath.Join(t.TempDir(), "fairness.csv.gz"), 2*time.Second, 2*time.Second)
if err != nil {
t.Fatal(err)
}
if len(evidence.PerFlowBytes) != 8 || len(evidence.CapacitySteps) != 2 ||
len(evidence.Series) != 80 || evidence.RawSamplesSHA256 == "" || evidence.JainIndex < 0.99 {
len(evidence.Series) != 6 || evidence.RawSamplesSHA256 == "" || evidence.JainIndex < 0.99 {
t.Fatalf("pacer evidence = %#v", evidence)
}
}
File diff suppressed because it is too large Load Diff
+103 -37
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"net"
"slices"
"sync"
"sync/atomic"
"time"
@@ -19,14 +20,13 @@ import (
)
const (
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"
defaultHelloLimit = 16 * 1024
defaultControlLimit = 128 * 1024
clientControlBacklog = 64
applicationError = quic.ApplicationErrorCode(0x100)
controlFlowID = "control.ack.v1"
inputFlowID = "input.sequenced.v1"
clipboardFlowID = "clipboard.text.v1"
)
var (
@@ -101,12 +101,15 @@ func NewServer(config ServerConfig) (*Server, error) {
if err := validateServerTLS(config.TLSConfig); err != nil {
return nil, err
}
if config.Capabilities == (protocol.CapabilityProfile{}) {
if capabilityProfileUnset(config.Capabilities) {
config.Capabilities = DefaultCapabilities()
}
if config.ProviderCapabilities == (protocol.CapabilityProfile{}) {
if capabilityProfileUnset(config.ProviderCapabilities) {
config.ProviderCapabilities = DefaultCapabilities()
}
if config.Capabilities.Validate() != nil || config.ProviderCapabilities.Validate() != nil {
return nil, ErrNoCapabilityOverlap
}
if config.ProviderProfile == "" {
config.ProviderProfile = ProviderProfileApollo
}
@@ -241,6 +244,13 @@ func (s *Server) handleConnection(parent context.Context, connection *quic.Conn)
_ = writeStableError(stream, "no_capability_overlap", err, false)
return
}
selected, err = selectApolloPolicyCapabilities(work.StreamPolicy, selected)
if err != nil {
_ = s.config.Admission.Release(context.Background(), authority)
s.metrics.AdmissionRejects.Add(1)
_ = writeStableError(stream, "no_capability_overlap", err, false)
return
}
clipboard, err := newClipboardGate(work.ClipboardPolicy, time.Now)
if err != nil {
_ = s.config.Admission.Release(context.Background(), authority)
@@ -335,13 +345,26 @@ func apolloPolicyMatchesCapabilities(policy protocol.ProviderStreamPolicy, capab
if validateApolloStreamPolicy(policy) != nil || capabilities.Audio != "encoded" {
return false
}
required := apolloPolicyProfile(policy)
return required != "" && slices.Contains(capabilities.ClientDecode, required)
}
func selectApolloPolicyCapabilities(policy protocol.ProviderStreamPolicy, capabilities protocol.CapabilityProfile) (protocol.CapabilityProfile, error) {
if !apolloPolicyMatchesCapabilities(policy, capabilities) {
return protocol.CapabilityProfile{}, ErrNoCapabilityOverlap
}
capabilities.ClientDecode = []string{apolloPolicyProfile(policy)}
return capabilities, nil
}
func apolloPolicyProfile(policy protocol.ProviderStreamPolicy) string {
switch policy.Codec {
case "H264":
return capabilities.ClientDecode == "h264-opus" || capabilities.ClientDecode == defaultClientDecode
return "h264-opus"
case "HEVC":
return capabilities.ClientDecode == "hevc-opus" || capabilities.ClientDecode == defaultClientDecode
return "hevc-opus"
default:
return false
return ""
}
}
@@ -370,9 +393,12 @@ type gatewaySession struct {
cleanupOnce sync.Once
inputMu sync.Mutex
controlWriteMu sync.Mutex
outputMu sync.Mutex
pressed map[string]struct{}
sequence atomic.Uint32
mediaDrops uint64
mediaQuiesced bool
terminalSent atomic.Bool
endReason error
result chan error
}
@@ -408,6 +434,13 @@ func (s *gatewaySession) run() {
case s.endReason = <-s.result:
}
s.cancel()
if s.terminalSent.Load() {
s.cleanup()
select {
case <-s.connection.Context().Done():
case <-timer.C:
}
}
}
func (s *gatewaySession) providerEventLoop() {
@@ -420,28 +453,32 @@ func (s *gatewaySession) providerEventLoop() {
if !ok {
return
}
if event.Kind == ProviderEventDisconnected {
s.result <- ErrProviderDisconnected
return
terminal := event.Kind == ProviderEventTerminated || event.Kind == ProviderEventDisconnected
if terminal {
s.outputMu.Lock()
s.mediaQuiesced = true
}
payload, err := EncodeProviderEvent(event)
if err == nil {
err = s.sendControl(s.sequence.Add(1), payload)
}
if terminal {
s.outputMu.Unlock()
}
if err != nil {
s.result <- err
return
}
if event.Kind == ProviderEventTerminated {
timer := time.NewTimer(terminalFeedbackDrain)
select {
case <-s.ctx.Done():
timer.Stop()
return
case <-timer.C:
}
if terminal {
s.terminalSent.Store(true)
}
switch event.Kind {
case ProviderEventTerminated:
s.result <- ErrProviderTerminated
return
case ProviderEventDisconnected:
s.result <- ErrProviderDisconnected
return
}
}
}
@@ -604,21 +641,21 @@ func (s *gatewaySession) mediaLoop() {
select {
case <-s.ctx.Done():
return
case payload, ok := <-video:
case media, ok := <-video:
if !ok {
video = nil
continue
}
if err := s.sendMedia(ChannelVideo, payload); err != nil {
if err := s.forwardMedia(ChannelVideo, media); err != nil {
s.result <- err
return
}
case payload, ok := <-audio:
case media, ok := <-audio:
if !ok {
audio = nil
continue
}
if err := s.sendMedia(ChannelAudio, payload); err != nil {
if err := s.forwardMedia(ChannelAudio, media); err != nil {
s.result <- err
return
}
@@ -627,12 +664,31 @@ func (s *gatewaySession) mediaLoop() {
s.result <- ErrProviderDisconnected
}
func (s *gatewaySession) sendMedia(channel byte, payload []byte) error {
func (s *gatewaySession) forwardMedia(channel byte, media ProviderMedia) error {
s.outputMu.Lock()
defer s.outputMu.Unlock()
state := s.provider.State().State
if s.mediaQuiesced || state == ProviderStateTerminated || state == ProviderStateDisconnected {
s.mediaQuiesced = true
return nil
}
return s.sendMedia(channel, media)
}
func (s *gatewaySession) sendMedia(channel byte, media ProviderMedia) error {
dequeuedAt := time.Now()
if media.EnqueuedAt.IsZero() || media.EnqueuedAt.After(dequeuedAt) {
media.EnqueuedAt = dequeuedAt
}
if media.ReceivedAt.IsZero() || media.ReceivedAt.After(media.EnqueuedAt) {
media.ReceivedAt = media.EnqueuedAt
}
processingStarted := time.Now()
frames, err := FragmentPayload(channel, s.sequence.Add(1), uint64(time.Now().UnixMilli()), payload)
frames, err := FragmentPayload(channel, s.sequence.Add(1), uint64(time.Now().UnixMilli()), media.Payload)
if err != nil {
return err
}
var pacingDelay time.Duration
for _, frame := range frames {
encoded, err := EncodeFrame(frame)
if err != nil {
@@ -642,16 +698,18 @@ func (s *gatewaySession) sendMedia(channel byte, payload []byte) error {
if err := s.server.pacer.wait(s.ctx, s.authority.SessionID, len(encoded)); err != nil {
return err
}
s.server.metrics.PacingDelayNanos.Add(uint64(time.Since(pacingStarted)))
s.server.metrics.QueueDelayNanos.Add(uint64(time.Since(pacingStarted)))
pacingDelay += time.Since(pacingStarted)
if err := s.connection.SendDatagram(encoded); err != nil {
return err
}
s.server.metrics.MediaPackets.Add(1)
s.server.metrics.MediaBytes.Add(uint64(len(encoded)))
s.server.metrics.ProcessingDelayNanos.Add(uint64(time.Since(processingStarted)))
s.server.metrics.ProcessingSamples.Add(1)
}
processingDelay := media.EnqueuedAt.Sub(media.ReceivedAt) + time.Since(processingStarted) - pacingDelay
s.server.metrics.QueueDelayNanos.Add(uint64(dequeuedAt.Sub(media.EnqueuedAt)))
s.server.metrics.ProcessingDelayNanos.Add(uint64(max(processingDelay, 0)))
s.server.metrics.PacingDelayNanos.Add(uint64(pacingDelay))
s.server.metrics.ProcessingSamples.Add(1)
return nil
}
@@ -799,7 +857,9 @@ func (s *gatewaySession) cleanup() {
} else if err := s.server.config.Admission.Release(cleanupCtx, s.authority); err != nil {
s.server.metrics.ProviderErrors.Add(1)
}
_ = s.connection.CloseWithError(applicationError, "session closed")
if !s.terminalSent.Load() {
_ = s.connection.CloseWithError(applicationError, "session closed")
}
return
}
if errors.Is(s.endReason, ErrProviderDisconnected) {
@@ -812,7 +872,9 @@ func (s *gatewaySession) cleanup() {
if err := s.server.reportProviderState(cleanupCtx, state); err != nil {
s.server.metrics.ProviderErrors.Add(1)
}
_ = s.connection.CloseWithError(applicationError, "session closed")
if !s.terminalSent.Load() {
_ = s.connection.CloseWithError(applicationError, "session closed")
}
})
}
@@ -1001,7 +1063,11 @@ func (c *Client) ReceiveProviderEvent(ctx context.Context) (ProviderEvent, error
if len(payload) > 1024 {
return ProviderEvent{}, ErrProviderMalformed
}
return DecodeProviderEvent(payload)
event, err := DecodeProviderEvent(payload)
if err == nil && (event.Kind == ProviderEventTerminated || event.Kind == ProviderEventDisconnected) {
_ = c.Close()
}
return event, err
}
func (c *Client) ReceiveClipboard(ctx context.Context) (protocol.GatewayClipboardText, error) {