diff --git a/context.go b/context.go index e5e84b8..4f7c5eb 100644 --- a/context.go +++ b/context.go @@ -88,6 +88,10 @@ const ( // access to a Context from multiple goroutines requires external // synchronization. type Context struct { + // constructed is set to true after the Context is fully initialized. + // Options can check this flag to reject updates that are only valid during construction. + constructed bool + cipher srtpCipher srtpSSRCStates map[uint32]*srtpSSRCState @@ -170,6 +174,8 @@ func CreateContext( c.mkis[string(c.sendMKI)] = c.cipher } + c.constructed = true + return c, nil } diff --git a/errors.go b/errors.go index 63f123b..015d4dd 100644 --- a/errors.go +++ b/errors.go @@ -13,6 +13,8 @@ var ( ErrFailedToVerifyAuthTag = errors.New("failed to verify auth tag") // ErrMKINotFound is returned when decryption fails due to unknown MKI value in packet. ErrMKINotFound = errors.New("MKI not found") + // ErrContextOptionNotUpdatable indicates an option cannot be updated after construction. + ErrContextOptionNotUpdatable = errors.New("option can only be set during context construction") errDuplicated = errors.New("duplicated packet") errShortSrtpMasterKey = errors.New("SRTP master key is not long enough") diff --git a/option.go b/option.go index e5a693b..59c6db2 100644 --- a/option.go +++ b/option.go @@ -7,12 +7,21 @@ import ( "github.com/pion/transport/v4/replaydetector" ) -// ContextOption represents option of Context using the functional options pattern. +// ContextOption configures a Context using the functional options pattern. +// +// Context options are primarily intended to be passed to CreateContext during construction. +// Whether an option may also be applied to an already-constructed Context depends on that +// option's documentation; options that do not support runtime updates return +// ErrContextOptionNotUpdatable. type ContextOption func(*Context) error // SRTPReplayProtection sets SRTP replay protection window size. func SRTPReplayProtection(windowSize uint) ContextOption { // nolint:revive return func(c *Context) error { + if c.constructed { + return ErrContextOptionNotUpdatable + } + c.newSRTPReplayDetector = func() replaydetector.ReplayDetector { return replaydetector.New(windowSize, maxROC<<16|maxSequenceNumber) } @@ -24,6 +33,10 @@ func SRTPReplayProtection(windowSize uint) ContextOption { // nolint:revive // SRTCPReplayProtection sets SRTCP replay protection window size. func SRTCPReplayProtection(windowSize uint) ContextOption { return func(c *Context) error { + if c.constructed { + return ErrContextOptionNotUpdatable + } + c.newSRTCPReplayDetector = func() replaydetector.ReplayDetector { return replaydetector.New(windowSize, maxSRTCPIndex) } @@ -35,6 +48,10 @@ func SRTCPReplayProtection(windowSize uint) ContextOption { // SRTPNoReplayProtection disables SRTP replay protection. func SRTPNoReplayProtection() ContextOption { // nolint:revive return func(c *Context) error { + if c.constructed { + return ErrContextOptionNotUpdatable + } + c.newSRTPReplayDetector = func() replaydetector.ReplayDetector { return &nopReplayDetector{} } @@ -46,6 +63,10 @@ func SRTPNoReplayProtection() ContextOption { // nolint:revive // SRTCPNoReplayProtection disables SRTCP replay protection. func SRTCPNoReplayProtection() ContextOption { return func(c *Context) error { + if c.constructed { + return ErrContextOptionNotUpdatable + } + c.newSRTCPReplayDetector = func() replaydetector.ReplayDetector { return &nopReplayDetector{} } @@ -57,6 +78,10 @@ func SRTCPNoReplayProtection() ContextOption { // SRTPReplayDetectorFactory sets custom SRTP replay detector. func SRTPReplayDetectorFactory(fn func() replaydetector.ReplayDetector) ContextOption { // nolint:revive return func(c *Context) error { + if c.constructed { + return ErrContextOptionNotUpdatable + } + c.newSRTPReplayDetector = fn return nil @@ -66,6 +91,10 @@ func SRTPReplayDetectorFactory(fn func() replaydetector.ReplayDetector) ContextO // SRTCPReplayDetectorFactory sets custom SRTCP replay detector. func SRTCPReplayDetectorFactory(fn func() replaydetector.ReplayDetector) ContextOption { return func(c *Context) error { + if c.constructed { + return ErrContextOptionNotUpdatable + } + c.newSRTCPReplayDetector = fn return nil @@ -83,6 +112,10 @@ func (s *nopReplayDetector) Check(uint64) (func() bool, bool) { // All MKIs added later using Context.AddCipherForMKI must have the same length as the one used here. func MasterKeyIndicator(mki []byte) ContextOption { return func(c *Context) error { + if c.constructed { + return ErrContextOptionNotUpdatable + } + if len(mki) > 0 { c.sendMKI = make([]byte, len(mki)) copy(c.sendMKI, mki) @@ -95,6 +128,10 @@ func MasterKeyIndicator(mki []byte) ContextOption { // SRTPEncryption enables SRTP encryption. func SRTPEncryption() ContextOption { // nolint:revive return func(c *Context) error { + if c.constructed { + return ErrContextOptionNotUpdatable + } + c.encryptSRTP = true return nil @@ -108,6 +145,10 @@ func SRTPEncryption() ContextOption { // nolint:revive // Note: you can also use SRTPAuthenticationTagLength(0) to disable authentication tag too. func SRTPNoEncryption() ContextOption { // nolint:revive return func(c *Context) error { + if c.constructed { + return ErrContextOptionNotUpdatable + } + c.encryptSRTP = false return nil @@ -117,6 +158,10 @@ func SRTPNoEncryption() ContextOption { // nolint:revive // SRTCPEncryption enables SRTCP encryption. func SRTCPEncryption() ContextOption { return func(c *Context) error { + if c.constructed { + return ErrContextOptionNotUpdatable + } + c.encryptSRTCP = true return nil @@ -128,6 +173,10 @@ func SRTCPEncryption() ContextOption { // It simplifies debugging and testing, but it is not recommended for production use. func SRTCPNoEncryption() ContextOption { return func(c *Context) error { + if c.constructed { + return ErrContextOptionNotUpdatable + } + c.encryptSRTCP = false return nil @@ -152,6 +201,10 @@ func SRTCPNoEncryption() ContextOption { // to mitigate this issue. func RolloverCounterCarryingTransform(mode RCCMode, rocTransmitRate uint16) ContextOption { return func(c *Context) error { + if c.constructed { + return ErrContextOptionNotUpdatable + } + c.rccMode = mode c.rocTransmitRate = rocTransmitRate @@ -168,6 +221,10 @@ func RolloverCounterCarryingTransform(mode RCCMode, rocTransmitRate uint16) Cont // This option is ignored for AEAD profiles. func SRTPAuthenticationTagLength(authTagRTPLen int) ContextOption { // nolint:revive return func(c *Context) error { + if c.constructed { + return ErrContextOptionNotUpdatable + } + c.authTagRTPLen = &authTagRTPLen return nil @@ -178,6 +235,10 @@ func SRTPAuthenticationTagLength(authTagRTPLen int) ContextOption { // nolint:re // Sources, as defined in RFC 9335. func Cryptex(cryptexMode CryptexMode) ContextOption { return func(c *Context) error { + if c.constructed { + return ErrContextOptionNotUpdatable + } + c.cryptexMode = cryptexMode return nil diff --git a/option_test.go b/option_test.go new file mode 100644 index 0000000..2b8f037 --- /dev/null +++ b/option_test.go @@ -0,0 +1,186 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package srtp + +import ( + "testing" + + "github.com/pion/transport/v4/replaydetector" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// testReplayDetector is a minimal ReplayDetector used in factory option tests. +type testReplayDetector struct{} + +func (d *testReplayDetector) Check(uint64) (func() bool, bool) { + return func() bool { return true }, true +} + +// constructedContext returns a fully constructed Context. +func constructedContext(t *testing.T) *Context { + t.Helper() + + c, err := CreateContext(make([]byte, 16), make([]byte, 14), ProtectionProfileAes128CmHmacSha1_80) + require.NoError(t, err) + + return c +} + +func TestContextOptions(t *testing.T) { + tests := []struct { + name string + option func() ContextOption + validate func(t *testing.T, c *Context) + }{ + { + name: "SRTPReplayProtection", + option: func() ContextOption { return SRTPReplayProtection(128) }, + validate: func(t *testing.T, c *Context) { + t.Helper() + assert.NotNil(t, c.newSRTPReplayDetector) + assert.NotNil(t, c.newSRTPReplayDetector()) + }, + }, + { + name: "SRTCPReplayProtection", + option: func() ContextOption { return SRTCPReplayProtection(128) }, + validate: func(t *testing.T, c *Context) { + t.Helper() + assert.NotNil(t, c.newSRTCPReplayDetector) + assert.NotNil(t, c.newSRTCPReplayDetector()) + }, + }, + { + name: "SRTPNoReplayProtection", + option: func() ContextOption { + return func(c *Context) error { return SRTPNoReplayProtection()(c) } + }, + validate: func(t *testing.T, c *Context) { + t.Helper() + require.NotNil(t, c.newSRTPReplayDetector) + _, ok := c.newSRTPReplayDetector().(*nopReplayDetector) + assert.True(t, ok) + }, + }, + { + name: "SRTCPNoReplayProtection", + option: func() ContextOption { + return func(c *Context) error { return SRTCPNoReplayProtection()(c) } + }, + validate: func(t *testing.T, c *Context) { + t.Helper() + require.NotNil(t, c.newSRTCPReplayDetector) + _, ok := c.newSRTCPReplayDetector().(*nopReplayDetector) + assert.True(t, ok) + }, + }, + { + name: "SRTPReplayDetectorFactory", + option: func() ContextOption { + return SRTPReplayDetectorFactory(func() replaydetector.ReplayDetector { return &testReplayDetector{} }) + }, + validate: func(t *testing.T, c *Context) { + t.Helper() + require.NotNil(t, c.newSRTPReplayDetector) + _, ok := c.newSRTPReplayDetector().(*testReplayDetector) + assert.True(t, ok) + }, + }, + { + name: "SRTCPReplayDetectorFactory", + option: func() ContextOption { + return SRTCPReplayDetectorFactory(func() replaydetector.ReplayDetector { return &testReplayDetector{} }) + }, + validate: func(t *testing.T, c *Context) { + t.Helper() + require.NotNil(t, c.newSRTCPReplayDetector) + _, ok := c.newSRTCPReplayDetector().(*testReplayDetector) + assert.True(t, ok) + }, + }, + { + name: "MasterKeyIndicator", + option: func() ContextOption { return MasterKeyIndicator([]byte{0x01, 0x02, 0x03, 0x04}) }, + validate: func(t *testing.T, c *Context) { + t.Helper() + assert.Equal(t, []byte{0x01, 0x02, 0x03, 0x04}, c.sendMKI) + }, + }, + { + name: "SRTPEncryption", + option: func() ContextOption { + return func(c *Context) error { return SRTPEncryption()(c) } + }, + validate: func(t *testing.T, c *Context) { + t.Helper() + assert.True(t, c.encryptSRTP) + }, + }, + { + name: "SRTPNoEncryption", + option: func() ContextOption { + return func(c *Context) error { return SRTPNoEncryption()(c) } + }, + validate: func(t *testing.T, c *Context) { + t.Helper() + assert.False(t, c.encryptSRTP) + }, + }, + { + name: "SRTCPEncryption", + option: func() ContextOption { + return func(c *Context) error { return SRTCPEncryption()(c) } + }, + validate: func(t *testing.T, c *Context) { + t.Helper() + assert.True(t, c.encryptSRTCP) + }, + }, + { + name: "SRTCPNoEncryption", + option: func() ContextOption { + return func(c *Context) error { return SRTCPNoEncryption()(c) } + }, + validate: func(t *testing.T, c *Context) { + t.Helper() + assert.False(t, c.encryptSRTCP) + }, + }, + { + name: "RolloverCounterCarryingTransform", + option: func() ContextOption { return RolloverCounterCarryingTransform(RCCMode2, 10) }, + validate: func(t *testing.T, c *Context) { + t.Helper() + assert.Equal(t, RCCMode2, c.rccMode) + assert.Equal(t, uint16(10), c.rocTransmitRate) + }, + }, + { + name: "SRTPAuthenticationTagLength", + option: func() ContextOption { return SRTPAuthenticationTagLength(8) }, + validate: func(t *testing.T, c *Context) { + t.Helper() + require.NotNil(t, c.authTagRTPLen) + assert.Equal(t, 8, *c.authTagRTPLen) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Run("sets value", func(t *testing.T) { + c := &Context{} + require.NoError(t, tt.option()(c)) + tt.validate(t, c) + }) + + t.Run("constructed error", func(t *testing.T) { + ctx := constructedContext(t) + err := tt.option()(ctx) + assert.ErrorIs(t, err, ErrContextOptionNotUpdatable) + }) + }) + } +} diff --git a/srtp_cipher_utils_test.go b/srtp_cipher_utils_test.go index 8fb274e..ae63b0a 100644 --- a/srtp_cipher_utils_test.go +++ b/srtp_cipher_utils_test.go @@ -110,6 +110,7 @@ func createContextWithCipher(profile ProtectionProfile, cipher srtpCipher) (*Con if err != nil { return nil, err } + ctx.constructed = true return ctx, nil }