Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -170,6 +174,8 @@ func CreateContext(
c.mkis[string(c.sendMKI)] = c.cipher
}

c.constructed = true

return c, nil
}

Expand Down
2 changes: 2 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
63 changes: 62 additions & 1 deletion option.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment thread
sirzooro marked this conversation as resolved.

c.newSRTPReplayDetector = func() replaydetector.ReplayDetector {
return replaydetector.New(windowSize, maxROC<<16|maxSequenceNumber)
}
Expand All @@ -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)
}
Expand All @@ -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{}
}
Expand All @@ -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{}
}
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down
186 changes: 186 additions & 0 deletions option_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// 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)
})
})
}
}
1 change: 1 addition & 0 deletions srtp_cipher_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func createContextWithCipher(profile ProtectionProfile, cipher srtpCipher) (*Con
if err != nil {
return nil, err
}
ctx.constructed = true

return ctx, nil
}
Loading