fix(gateway): enforce audited production traversal
This commit is contained in:
+103
-37
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user