feat(gateway): relay complete encoded frames
This commit is contained in:
+271
-9
@@ -1,6 +1,7 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
@@ -32,7 +33,7 @@ const protocolTerminalReceiptVector = "VGF1\x00\x03\x00\x00"
|
||||
|
||||
func TestFrameValidationAndFragmentation(t *testing.T) {
|
||||
frames, err := FragmentPayload(ChannelVideo, 7, 11, make([]byte, 1180))
|
||||
if err != nil || len(frames) != 2 || len(frames[0].Payload) != 1179 || len(frames[1].Payload) != 1 {
|
||||
if err != nil || len(frames) != 2 || len(frames[0].Payload) != 1177 || len(frames[1].Payload) != 3 {
|
||||
t.Fatalf("fragmentation = %#v, err = %v", frames, err)
|
||||
}
|
||||
encoded, err := EncodeFrame(frames[0])
|
||||
@@ -161,11 +162,11 @@ func TestNewServerRejectsPartiallyConfiguredCapabilities(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSyntheticImpairmentPacingAndResourceBounds(t *testing.T) {
|
||||
payload := make([]byte, 1179*16+1)
|
||||
payload := make([]byte, maxCompleteFrameBytes+1)
|
||||
if _, err := FragmentPayload(ChannelVideo, 1, 0, payload); !errors.Is(err, ErrFrameFragmentedLimit) {
|
||||
t.Fatalf("oversized media payload accepted: %v", err)
|
||||
}
|
||||
frames, err := FragmentPayload(ChannelVideo, 1, 0, bytesRepeat(0x5a, 1179*4))
|
||||
frames, err := FragmentPayload(ChannelVideo, 1, 0, bytesRepeat(0x5a, frameV2PayloadSize*4))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -186,11 +187,29 @@ func TestSyntheticImpairmentPacingAndResourceBounds(t *testing.T) {
|
||||
}
|
||||
deliveredBytes += len(decoded.Payload)
|
||||
}
|
||||
if delivered != 3 || deliveredBytes != 1179*3 {
|
||||
if delivered != 3 || deliveredBytes != frameV2PayloadSize*3 {
|
||||
t.Fatalf("synthetic impairment delivered=%d bytes=%d", delivered, deliveredBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFragmentPayloadCarriesCompleteEncodedFrame(t *testing.T) {
|
||||
payload := bytesRepeat(0x5a, 256*1024)
|
||||
frames, err := FragmentPayload(ChannelVideo, 9, 11, payload)
|
||||
if errors.Is(err, ErrFrameFragmentedLimit) {
|
||||
t.Fatalf("complete encoded frame rejected at legacy fragment ceiling: %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var recovered []byte
|
||||
for _, frame := range frames {
|
||||
recovered = append(recovered, frame.Payload...)
|
||||
}
|
||||
if !bytes.Equal(recovered, payload) {
|
||||
t.Fatal("complete encoded frame payload changed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApolloFixturesAndLifecycle(t *testing.T) {
|
||||
management, err := os.ReadFile("testdata/apollo-management.xml")
|
||||
if err != nil {
|
||||
@@ -218,10 +237,10 @@ func TestApolloFixturesAndLifecycle(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := <-session.Video(); string(got.Payload) != string(video) {
|
||||
t.Fatalf("video changed: %x", got)
|
||||
t.Fatalf("video changed: %x", got.Payload)
|
||||
}
|
||||
if got := <-session.Audio(); string(got.Payload) != string(audio) {
|
||||
t.Fatalf("audio changed: %x", got)
|
||||
t.Fatalf("audio changed: %x", got.Payload)
|
||||
}
|
||||
if err := session.Input(context.Background(), InputEvent{Sequence: 1, Device: "keyboard", Code: 7, Pressed: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -401,7 +420,7 @@ func TestGatewayTelemetrySeparatesQueueProcessingAndPacing(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
receiveCtx, receiveCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
for index := byte(0); index < 2; index++ {
|
||||
for index := uint16(0); index < 2; index++ {
|
||||
frame, err := client.ReceiveFrame(receiveCtx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -838,6 +857,7 @@ func newNativeGatewayLifecycleHarness(t *testing.T, sessionID string) nativeGate
|
||||
type independentGatewayClient struct {
|
||||
connection *quic.Conn
|
||||
control *quic.Stream
|
||||
media independentMediaReassembler
|
||||
}
|
||||
|
||||
func dialIndependentGateway(ctx context.Context, address string, tlsConfig *tls.Config, request protocol.TunnelAdmissionRequest) (*independentGatewayClient, error) {
|
||||
@@ -866,7 +886,11 @@ func dialIndependentGateway(ctx context.Context, address string, tlsConfig *tls.
|
||||
_ = connection.CloseWithError(applicationError, "independent client admission failed")
|
||||
return nil, err
|
||||
}
|
||||
return &independentGatewayClient{connection: connection, control: stream}, nil
|
||||
return &independentGatewayClient{
|
||||
connection: connection,
|
||||
control: stream,
|
||||
media: independentMediaReassembler{incomplete: make(map[independentMediaKey]*independentMediaUnit)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *independentGatewayClient) ReceiveProviderEvent(ctx context.Context) (ProviderEvent, error) {
|
||||
@@ -912,7 +936,245 @@ func (c *independentGatewayClient) ReceiveFrame(ctx context.Context) (Frame, err
|
||||
if err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
return DecodeFrame(data)
|
||||
return independentDecodeFrame(data)
|
||||
}
|
||||
|
||||
func (c *independentGatewayClient) ReceiveMedia(ctx context.Context) ([]byte, error) {
|
||||
for {
|
||||
data, err := c.connection.ReceiveDatagram(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, complete, err := c.media.Add(data, time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if complete {
|
||||
return payload, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type independentMediaKey struct {
|
||||
channel byte
|
||||
sequence uint32
|
||||
}
|
||||
|
||||
type independentMediaUnit struct {
|
||||
started time.Time
|
||||
timestamp uint64
|
||||
fragments [][]byte
|
||||
received []bool
|
||||
bytes int
|
||||
}
|
||||
|
||||
type independentMediaReassembler struct {
|
||||
incomplete map[independentMediaKey]*independentMediaUnit
|
||||
}
|
||||
|
||||
func independentDecodeFrame(data []byte) (Frame, error) {
|
||||
const (
|
||||
v1Header, v2Header = 21, 23
|
||||
v1Payload, v2Payload = 1179, 1177
|
||||
)
|
||||
if len(data) < 3 {
|
||||
return Frame{}, ErrFrameTruncated
|
||||
}
|
||||
if data[0] != 'V' || data[1] != 'D' {
|
||||
return Frame{}, ErrFrameMagic
|
||||
}
|
||||
version := data[2]
|
||||
headerSize, payloadLimit := v1Header, v1Payload
|
||||
if version == 2 {
|
||||
headerSize, payloadLimit = v2Header, v2Payload
|
||||
} else if version != 1 {
|
||||
return Frame{}, ErrFrameVersion
|
||||
}
|
||||
if len(data) < headerSize || version == 2 && len(data) > 1200 {
|
||||
return Frame{}, ErrFrameSize
|
||||
}
|
||||
frame := Frame{
|
||||
Version: version,
|
||||
Channel: data[3],
|
||||
Flags: data[4],
|
||||
Sequence: binary.BigEndian.Uint32(data[5:9]),
|
||||
TimestampMS: binary.BigEndian.Uint64(data[9:17]),
|
||||
}
|
||||
payloadLengthOffset := 19
|
||||
if version == 1 {
|
||||
frame.FragmentIndex = uint16(data[17])
|
||||
frame.FragmentCount = uint16(data[18])
|
||||
} else {
|
||||
frame.FragmentIndex = binary.BigEndian.Uint16(data[17:19])
|
||||
frame.FragmentCount = binary.BigEndian.Uint16(data[19:21])
|
||||
payloadLengthOffset = 21
|
||||
}
|
||||
if (frame.Channel != ChannelVideo && frame.Channel != ChannelAudio) || frame.Flags != 0 ||
|
||||
frame.FragmentCount == 0 || version == 1 && frame.FragmentCount > 16 ||
|
||||
version == 2 && frame.FragmentCount > 891 || frame.FragmentIndex >= frame.FragmentCount {
|
||||
return Frame{}, ErrFrameFragment
|
||||
}
|
||||
payloadLength := int(binary.BigEndian.Uint16(data[payloadLengthOffset : payloadLengthOffset+2]))
|
||||
if payloadLength > payloadLimit || len(data) != headerSize+payloadLength {
|
||||
return Frame{}, ErrFrameLength
|
||||
}
|
||||
frame.Payload = append([]byte(nil), data[headerSize:]...)
|
||||
return frame, nil
|
||||
}
|
||||
|
||||
func (r *independentMediaReassembler) Add(data []byte, now time.Time) ([]byte, bool, error) {
|
||||
frame, err := independentDecodeFrame(data)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
for key, unit := range r.incomplete {
|
||||
if now.Sub(unit.started) > 250*time.Millisecond {
|
||||
delete(r.incomplete, key)
|
||||
}
|
||||
}
|
||||
key := independentMediaKey{channel: frame.Channel, sequence: frame.Sequence}
|
||||
unit := r.incomplete[key]
|
||||
if unit == nil {
|
||||
if len(r.incomplete) == 4 {
|
||||
var oldestKey independentMediaKey
|
||||
var oldest time.Time
|
||||
for candidate, current := range r.incomplete {
|
||||
if oldest.IsZero() || current.started.Before(oldest) {
|
||||
oldestKey, oldest = candidate, current.started
|
||||
}
|
||||
}
|
||||
delete(r.incomplete, oldestKey)
|
||||
}
|
||||
unit = &independentMediaUnit{
|
||||
started: now, timestamp: frame.TimestampMS,
|
||||
fragments: make([][]byte, frame.FragmentCount), received: make([]bool, frame.FragmentCount),
|
||||
}
|
||||
r.incomplete[key] = unit
|
||||
}
|
||||
if len(unit.fragments) != int(frame.FragmentCount) || unit.timestamp != frame.TimestampMS {
|
||||
return nil, false, ErrFrameFragment
|
||||
}
|
||||
index := int(frame.FragmentIndex)
|
||||
if unit.received[index] {
|
||||
if !bytes.Equal(unit.fragments[index], frame.Payload) {
|
||||
return nil, false, ErrFrameFragment
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
if unit.bytes+len(frame.Payload) > 1<<20 {
|
||||
delete(r.incomplete, key)
|
||||
return nil, false, ErrFrameSize
|
||||
}
|
||||
unit.fragments[index] = frame.Payload
|
||||
unit.received[index] = true
|
||||
unit.bytes += len(frame.Payload)
|
||||
for _, received := range unit.received {
|
||||
if !received {
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
payload := make([]byte, 0, unit.bytes)
|
||||
for _, fragment := range unit.fragments {
|
||||
payload = append(payload, fragment...)
|
||||
}
|
||||
delete(r.incomplete, key)
|
||||
return payload, true, nil
|
||||
}
|
||||
|
||||
func TestIndependentClientReassemblesProtocolDatagramV2(t *testing.T) {
|
||||
fixed, err := hex.DecodeString("5644020a00000000010000000000000002000000010003010203")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
frame, err := independentDecodeFrame(fixed)
|
||||
if err != nil || frame.Version != 2 || frame.Channel != ChannelVideo ||
|
||||
frame.Sequence != 1 || frame.TimestampMS != 2 || frame.FragmentCount != 1 ||
|
||||
!bytes.Equal(frame.Payload, []byte{1, 2, 3}) {
|
||||
t.Fatalf("fixed Protocol v2 frame = %#v, %v", frame, err)
|
||||
}
|
||||
|
||||
payload := bytes.Repeat([]byte("frame-boundary-"), 300)
|
||||
fragments, err := FragmentPayload(ChannelVideo, 7, 11, payload)
|
||||
if err != nil || len(fragments) < 3 {
|
||||
t.Fatalf("fragments = %d, %v", len(fragments), err)
|
||||
}
|
||||
encoded := make([][]byte, len(fragments))
|
||||
for index, fragment := range fragments {
|
||||
encoded[index], err = EncodeFrame(fragment)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
reassembler := independentMediaReassembler{incomplete: make(map[independentMediaKey]*independentMediaUnit)}
|
||||
now := time.Unix(0, 0)
|
||||
order := []int{2, 0, 0, 1, 3}
|
||||
var recovered []byte
|
||||
for _, index := range order {
|
||||
var complete bool
|
||||
recovered, complete, err = reassembler.Add(encoded[index], now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if index != 3 && complete {
|
||||
t.Fatalf("unit completed at fragment %d", index)
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(recovered, payload) {
|
||||
t.Fatal("independent client changed the complete encoded frame")
|
||||
}
|
||||
|
||||
if _, _, err := reassembler.Add(encoded[0], now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conflict := append([]byte(nil), encoded[0]...)
|
||||
conflict[len(conflict)-1] ^= 0xff
|
||||
if _, _, err := reassembler.Add(conflict, now); !errors.Is(err, ErrFrameFragment) {
|
||||
t.Fatalf("conflicting duplicate = %v", err)
|
||||
}
|
||||
reassembler = independentMediaReassembler{incomplete: make(map[independentMediaKey]*independentMediaUnit)}
|
||||
if _, _, err := reassembler.Add(encoded[0], now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expired := append([]byte(nil), encoded[0]...)
|
||||
binary.BigEndian.PutUint32(expired[5:9], 8)
|
||||
if _, _, err := reassembler.Add(expired, now.Add(251*time.Millisecond)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(reassembler.incomplete) != 1 {
|
||||
t.Fatalf("expired incomplete units = %d, want 1", len(reassembler.incomplete))
|
||||
}
|
||||
|
||||
reassembler = independentMediaReassembler{incomplete: make(map[independentMediaKey]*independentMediaUnit)}
|
||||
for sequence := uint32(1); sequence <= 5; sequence++ {
|
||||
partial, encodeErr := EncodeFrame(Frame{
|
||||
Version: 2, Channel: ChannelVideo, Sequence: sequence, TimestampMS: 1,
|
||||
FragmentIndex: 0, FragmentCount: 2, Payload: []byte{byte(sequence)},
|
||||
})
|
||||
if encodeErr != nil {
|
||||
t.Fatal(encodeErr)
|
||||
}
|
||||
if _, _, err := reassembler.Add(partial, now.Add(time.Duration(sequence)*time.Nanosecond)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(reassembler.incomplete) != 4 || reassembler.incomplete[independentMediaKey{channel: ChannelVideo, sequence: 1}] != nil {
|
||||
t.Fatalf("fifth unit did not evict the oldest: %#v", reassembler.incomplete)
|
||||
}
|
||||
|
||||
reassembler = independentMediaReassembler{incomplete: make(map[independentMediaKey]*independentMediaUnit)}
|
||||
for index := uint16(0); index < 891; index++ {
|
||||
fragment, encodeErr := EncodeFrame(Frame{
|
||||
Version: 2, Channel: ChannelVideo, Sequence: 99, TimestampMS: 1,
|
||||
FragmentIndex: index, FragmentCount: 891, Payload: make([]byte, 1177),
|
||||
})
|
||||
if encodeErr != nil {
|
||||
t.Fatal(encodeErr)
|
||||
}
|
||||
_, _, err = reassembler.Add(fragment, now)
|
||||
}
|
||||
if !errors.Is(err, ErrFrameSize) || len(reassembler.incomplete) != 0 {
|
||||
t.Fatalf("oversized reassembly = %v, incomplete=%d", err, len(reassembler.incomplete))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *independentGatewayClient) waitClosed(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user