Repository: bluenviron/gortsplib

Issue:
MIKEY: use proper SP parameter for authentication tag length
I have seen that you implemented support for SRTP and MIKEY, thanks. Looks that you copied this bug from GStreamer which does it wrong. For SHA-1 Authentication Key Length is always 20 bytes, and Authentication Tag Length is length of truncated Auth Key appended to SRTP packet (either 10 or 4). Length of Authentication Tag in MIKEY should be sent using `PayloadSPPolicyParamTypeAuthTagLen` parameter instead of `PayloadSPPolicyParamTypeSessionAuthKeyLen`.

Hints from issue comments:
@aler9: in #935 i've changed the content of PayloadSPPolicyParamTypeAuthTagLen and PayloadSPPolicyParamTypeSessionAuthKeyLen of outgoing MIKEY messages.

Regarding the GStreamer issue, you should report it.@sirzooro: Thanks!

I logged bug for GStreamer in the past when I noticed this issue there.

Solution patch:
diff --git a/client.go b/client.go
index 15d422eb..3a5fc2de 100644
--- a/client.go
+++ b/client.go
@@ -281,7 +281,7 @@ func supportsGetParameter(header base.Header) bool {
 		return false
 	}
 
-	for _, m := range strings.Split(pub[0], ",") {
+	for m := range strings.SplitSeq(pub[0], ",") {
 		if base.Method(strings.Trim(m, " ")) == base.GetParameter {
 			return true
 		}
@@ -1229,7 +1229,10 @@ func (c *Client) do(req *base.Request, skipResponse bool) (*base.Response, error
 		c.session = sx.Session
 
 		if sx.Timeout != nil && *sx.Timeout > 0 {
-			c.keepAlivePeriod = time.Duration(*sx.Timeout) * time.Second * 8 / 10
+			c.keepAlivePeriod = max(
+				(time.Duration(*sx.Timeout)*time.Second)-5*time.Second,
+				1*time.Second,
+			)
 		}
 	}
 
diff --git a/examples/client-play-backchannel/dummy_audio.go b/examples/client-play-backchannel/dummy_audio.go
index c56bad21..25fbd407 100644
--- a/examples/client-play-backchannel/dummy_audio.go
+++ b/examples/client-play-backchannel/dummy_audio.go
@@ -13,7 +13,7 @@ func createDummyAudio(pts int64, prevPTS int64) []byte {
 	n := 0
 	ret := make([]byte, sampleCount*2)
 
-	for i := int64(0); i < sampleCount; i++ {
+	for i := range sampleCount {
 		v := int16(amplitude * math.Sin((float64(prevPTS+i)*frequency*math.Pi*2)/sampleRate))
 		ret[n] = byte(v >> 8)
 		ret[n+1] = byte(v)
diff --git a/examples/client-record-format-g711/dummy_audio.go b/examples/client-record-format-g711/dummy_audio.go
index c56bad21..25fbd407 100644
--- a/examples/client-record-format-g711/dummy_audio.go
+++ b/examples/client-record-format-g711/dummy_audio.go
@@ -13,7 +13,7 @@ func createDummyAudio(pts int64, prevPTS int64) []byte {
 	n := 0
 	ret := make([]byte, sampleCount*2)
 
-	for i := int64(0); i < sampleCount; i++ {
+	for i := range sampleCount {
 		v := int16(amplitude * math.Sin((float64(prevPTS+i)*frequency*math.Pi*2)/sampleRate))
 		ret[n] = byte(v >> 8)
 		ret[n+1] = byte(v)
diff --git a/examples/client-record-format-lpcm/dummy_audio.go b/examples/client-record-format-lpcm/dummy_audio.go
index 267263b2..7e2b971d 100644
--- a/examples/client-record-format-lpcm/dummy_audio.go
+++ b/examples/client-record-format-lpcm/dummy_audio.go
@@ -13,7 +13,7 @@ func createDummyAudio(pts int64, prevPTS int64) []byte {
 	n := 0
 	ret := make([]byte, sampleCount*2)
 
-	for i := int64(0); i < sampleCount; i++ {
+	for i := range sampleCount {
 		v := int16(amplitude * math.Sin((float64(prevPTS+i)*frequency*math.Pi*2)/sampleRate))
 		ret[n] = byte(v >> 8)
 		ret[n+1] = byte(v)
diff --git a/examples/server-play-backchannel/dummy_audio.go b/examples/server-play-backchannel/dummy_audio.go
index c56bad21..25fbd407 100644
--- a/examples/server-play-backchannel/dummy_audio.go
+++ b/examples/server-play-backchannel/dummy_audio.go
@@ -13,7 +13,7 @@ func createDummyAudio(pts int64, prevPTS int64) []byte {
 	n := 0
 	ret := make([]byte, sampleCount*2)
 
-	for i := int64(0); i < sampleCount; i++ {
+	for i := range sampleCount {
 		v := int16(amplitude * math.Sin((float64(prevPTS+i)*frequency*math.Pi*2)/sampleRate))
 		ret[n] = byte(v >> 8)
 		ret[n+1] = byte(v)
diff --git a/pkg/conn/conn.go b/pkg/conn/conn.go
index d42d4885..fab7d534 100644
--- a/pkg/conn/conn.go
+++ b/pkg/conn/conn.go
@@ -26,7 +26,7 @@ func NewConn(br *bufio.Reader, w io.Writer) *Conn {
 }
 
 // Read reads a Request, a Response or an Interleaved frame.
-func (c *Conn) Read() (interface{}, error) {
+func (c *Conn) Read() (any, error) {
 	for {
 		byts, err := c.br.Peek(2)
 		if err != nil {
diff --git a/pkg/description/media.go b/pkg/description/media.go
index 78fe1eae..03d8d946 100644
--- a/pkg/description/media.go
+++ b/pkg/description/media.go
@@ -261,7 +261,7 @@ func (m Media) URL(contentBase *base.URL) (*base.URL, error) {
 }
 
 // FindFormat finds a certain format among all the formats in the media.
-func (m Media) FindFormat(forma interface{}) bool {
+func (m Media) FindFormat(forma any) bool {
 	for _, formak := range m.Formats {
 		if reflect.TypeOf(formak) == reflect.TypeOf(forma).Elem() {
 			reflect.ValueOf(forma).Elem().Set(reflect.ValueOf(formak))
diff --git a/pkg/description/session.go b/pkg/description/session.go
index b0d62ef8..4116ab74 100644
--- a/pkg/description/session.go
+++ b/pkg/description/session.go
@@ -65,7 +65,7 @@ type Session struct {
 
 // FindFormat finds a certain format among all the formats in all the medias of the stream.
 // If the format is found, it is inserted into forma, and its media is returned.
-func (d *Session) FindFormat(forma interface{}) *Media {
+func (d *Session) FindFormat(forma any) *Media {
 	for _, media := range d.Medias {
 		ok := media.FindFormat(forma)
 		if ok {
diff --git a/pkg/format/format.go b/pkg/format/format.go
index be344192..14a81a35 100644
--- a/pkg/format/format.go
+++ b/pkg/format/format.go
@@ -60,7 +60,7 @@ func decodeFMTP(enc string) map[string]string {
 
 	ret := make(map[string]string)
 
-	for _, kv := range strings.Split(enc, ";") {
+	for kv := range strings.SplitSeq(enc, ";") {
 		kv = strings.Trim(kv, " ")
 
 		if len(kv) == 0 {
diff --git a/pkg/headers/rtp_info.go b/pkg/headers/rtp_info.go
index 46d2f963..00b8918f 100644
--- a/pkg/headers/rtp_info.go
+++ b/pkg/headers/rtp_info.go
@@ -28,7 +28,7 @@ func (h *RTPInfo) Unmarshal(v base.HeaderValue) error {
 		return fmt.Errorf("value provided multiple times (%v)", v)
 	}
 
-	for _, part := range strings.Split(v[0], ",") {
+	for part := range strings.SplitSeq(v[0], ",") {
 		e := &RTPInfoEntry{}
 
 		// remove leading spaces
diff --git a/pkg/ringbuffer/ringbuffer.go b/pkg/ringbuffer/ringbuffer.go
index 364aea31..91cba959 100644
--- a/pkg/ringbuffer/ringbuffer.go
+++ b/pkg/ringbuffer/ringbuffer.go
@@ -11,7 +11,7 @@ type RingBuffer struct {
 	size       uint64
 	mutex      sync.Mutex
 	cond       *sync.Cond
-	buffer     []interface{}
+	buffer     []any
 	readIndex  uint64
 	writeIndex uint64
 	closed     bool
@@ -27,7 +27,7 @@ func New(size uint64) (*RingBuffer, error) {
 
 	r := &RingBuffer{
 		size:   size,
-		buffer: make([]interface{}, size),
+		buffer: make([]any, size),
 	}
 
 	r.cond = sync.NewCond(&r.mutex)
@@ -62,7 +62,7 @@ func (r *RingBuffer) Reset() {
 }
 
 // Push pushes data at the end of the buffer.
-func (r *RingBuffer) Push(data interface{}) bool {
+func (r *RingBuffer) Push(data any) bool {
 	r.mutex.Lock()
 
 	if r.buffer[r.writeIndex] != nil {
@@ -81,7 +81,7 @@ func (r *RingBuffer) Push(data interface{}) bool {
 }
 
 // Pull pulls data from the beginning of the buffer.
-func (r *RingBuffer) Pull() (interface{}, bool) {
+func (r *RingBuffer) Pull() (any, bool) {
 	for {
 		r.mutex.Lock()
 
diff --git a/pkg/sdp/sdp.go b/pkg/sdp/sdp.go
index 7b63d61c..16ba2ebc 100644
--- a/pkg/sdp/sdp.go
+++ b/pkg/sdp/sdp.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"net/url"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -41,12 +42,7 @@ func indexOf(element string, data []string) int {
 }
 
 func anyOf(element string, data ...string) bool {
-	for _, v := range data {
-		if element == v {
-			return true
-		}
-	}
-	return false
+	return slices.Contains(data, element)
 }
 
 func parsePort(value string) (int, error) {
@@ -467,7 +463,7 @@ func (s *SessionDescription) unmarshalMediaDescription(value string) error {
 	// <proto>
 	// Set according to currently registered with IANA
 	// https://tools.ietf.org/html/rfc4566#section-5.14
-	for _, proto := range strings.Split(fields[2], "/") {
+	for proto := range strings.SplitSeq(fields[2], "/") {
 		if i := indexOf(proto, []string{
 			"UDP", "RTP", "AVP", "SAVP", "SAVPF",
 			"MP2T", "TLS", "DTLS", "SCTP", "AVPF", "TCP",
@@ -691,7 +687,7 @@ func (s *SessionDescription) Unmarshal(byts []byte) error {
 
 	state := stateInitial
 
-	for _, line := range strings.Split(strings.ReplaceAll(str, "\r", ""), "\n") {
+	for line := range strings.SplitSeq(strings.ReplaceAll(str, "\r", ""), "\n") {
 		if line == "" {
 			continue
 		}
diff --git a/server.go b/server.go
index 9d59845e..f5719e37 100644
--- a/server.go
+++ b/server.go
@@ -362,6 +362,11 @@ func (s *Server) Wait() error {
 	return s.closeError
 }
 
+// NetListener returns the underlying net.Listener
+func (s *Server) NetListener() net.Listener {
+	return s.tcpListener.ln
+}
+
 func (s *Server) run() {
 	defer s.wg.Done()
 
diff --git a/server_conn.go b/server_conn.go
index 6da07beb..d92c73ed 100644
--- a/server_conn.go
+++ b/server_conn.go
@@ -3,11 +3,11 @@ package gortsplib
 import (
 	"bufio"
 	"context"
-	"crypto/rand"
 	"crypto/tls"
 	"errors"
 	"net"
 	gourl "net/url"
+	"slices"
 	"strconv"
 	"strings"
 	"sync"
@@ -21,7 +21,6 @@ import (
 	"github.com/bluenviron/gortsplib/v5/pkg/headers"
 	"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
 	"github.com/bluenviron/gortsplib/v5/pkg/mikey"
-	"github.com/bluenviron/gortsplib/v5/pkg/ntp"
 )
 
 func getSessionID(header base.Header) string {
@@ -47,99 +46,13 @@ func checkMulticastEnabled(multicastIPRange string, query string) bool {
 
 func checkBackChannelsEnabled(header base.Header) bool {
 	if vals, ok := header["Require"]; ok {
-		for _, val := range vals {
-			if val == "www.onvif.org/ver20/backchannel" {
-				return true
-			}
+		if slices.Contains(vals, "www.onvif.org/ver20/backchannel") {
+			return true
 		}
 	}
 	return false
 }
 
-func mikeyGenerate(ctx *wrappedSRTPContext) (*mikey.Message, error) {
-	csbID, err := randUint32()
-	if err != nil {
-		return nil, err
-	}
-
-	msg := &mikey.Message{
-		Header: mikey.Header{
-			Version: 1,
-			CSBID:   csbID,
-		},
-	}
-
-	msg.Header.CSIDMapInfo = make([]mikey.SRTPIDEntry, len(ctx.ssrcs))
-
-	n := 0
-	for _, ssrc := range ctx.ssrcs {
-		msg.Header.CSIDMapInfo[n] = mikey.SRTPIDEntry{
-			PolicyNo: 0,
-			SSRC:     ssrc,
-			ROC:      ctx.roc(ssrc),
-		}
-		n++
-	}
-
-	randData := make([]byte, 16)
-	_, err = rand.Read(randData)
-	if err != nil {
-		return nil, err
-	}
-
-	msg.Payloads = []mikey.Payload{
-		&mikey.PayloadT{
-			TSType:  0,
-			TSValue: ntp.Encode(time.Now()),
-		},
-		&mikey.PayloadRAND{
-			Data: randData,
-		},
-		&mikey.PayloadSP{
-			PolicyParams: []mikey.PayloadSPPolicyParam{
-				{
-					Type:  mikey.PayloadSPPolicyParamTypeEncrAlg,
-					Value: []byte{1},
-				},
-				{
-					Type:  mikey.PayloadSPPolicyParamTypeSessionEncrKeyLen,
-					Value: []byte{0x10},
-				},
-				{
-					Type:  mikey.PayloadSPPolicyParamTypeAuthAlg,
-					Value: []byte{1},
-				},
-				{
-					Type:  mikey.PayloadSPPolicyParamTypeSessionAuthKeyLen,
-					Value: []byte{0x0a},
-				},
-				{
-					Type:  mikey.PayloadSPPolicyParamTypeSRTPEncrOffOn,
-					Value: []byte{1},
-				},
-				{
-					Type:  mikey.PayloadSPPolicyParamTypeSRTCPEncrOffOn,
-					Value: []byte{1},
-				},
-				{
-					Type:  mikey.PayloadSPPolicyParamTypeSRTPAuthOffOn,
-					Value: []byte{1},
-				},
-			},
-		},
-		&mikey.PayloadKEMAC{
-			SubPayloads: []*mikey.SubPayloadKeyData{
-				{
-					Type:    mikey.SubPayloadKeyDataKeyTypeTEK,
-					KeyData: ctx.key,
-				},
-			},
-		},
-	}
-
-	return msg, nil
-}
-
 func prepareForDescribe(
 	d *description.Session,
 	multicast bool,
@@ -210,7 +123,7 @@ type ServerConn struct {
 	ctx              context.Context
 	ctxCancel        func()
 	propsMutex       sync.RWMutex
-	userData         interface{}
+	userData         any
 	remoteAddr       *net.TCPAddr
 	bc               *bytecounter.ByteCounter
 	conn             *conn.Conn
@@ -259,12 +172,12 @@ func (sc *ServerConn) NetConn() net.Conn {
 }
 
 // SetUserData sets some user data associated with the connection.
-func (sc *ServerConn) SetUserData(v interface{}) {
+func (sc *ServerConn) SetUserData(v any) {
 	sc.userData = v
 }
 
 // UserData returns some user data associated with the connection.
-func (sc *ServerConn) UserData() interface{} {
+func (sc *ServerConn) UserData() any {
 	return sc.userData
 }
 
diff --git a/server_handler.go b/server_handler.go
index 8d090b5c..1a86d278 100644
--- a/server_handler.go
+++ b/server_handler.go
@@ -6,7 +6,7 @@ import (
 )
 
 // ServerHandler is the interface implemented by all the server handlers.
-type ServerHandler interface{}
+type ServerHandler any
 
 // ServerHandlerOnConnOpenCtx is the context of OnConnOpen.
 type ServerHandlerOnConnOpenCtx struct {
diff --git a/server_session.go b/server_session.go
index c7c5fbcd..dc6ef833 100644
--- a/server_session.go
+++ b/server_session.go
@@ -1,7 +1,6 @@
 package gortsplib
 
 import (
-	"bytes"
 	"context"
 	"crypto/rand"
 	"fmt"
@@ -24,7 +23,6 @@ import (
 	"github.com/bluenviron/gortsplib/v5/pkg/headers"
 	"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
 	"github.com/bluenviron/gortsplib/v5/pkg/mikey"
-	"github.com/bluenviron/gortsplib/v5/pkg/ntp"
 	"github.com/bluenviron/gortsplib/v5/pkg/rtpreceiver"
 	"github.com/bluenviron/gortsplib/v5/pkg/rtpsender"
 	"github.com/bluenviron/gortsplib/v5/pkg/rtptime"
@@ -223,111 +221,6 @@ func pickFirstSupportedTransport(sc *ServerConn, tsh headers.Transports) *header
 	return nil
 }
 
-func mikeyGetPayload[T mikey.Payload](mikeyMsg *mikey.Message) (T, bool) {
-	var zero T
-	for _, wrapped := range mikeyMsg.Payloads {
-		if val, ok := wrapped.(T); ok {
-			return val, true
-		}
-	}
-	return zero, false
-}
-
-func mikeyGetSPPolicy(spPayload *mikey.PayloadSP, typ mikey.PayloadSPPolicyParamType) ([]byte, bool) {
-	for _, pl := range spPayload.PolicyParams {
-		if pl.Type == typ {
-			return pl.Value, true
-		}
-	}
-	return nil, false
-}
-
-func mikeyToContext(mikeyMsg *mikey.Message) (*wrappedSRTPContext, error) {
-	timePayload, ok := mikeyGetPayload[*mikey.PayloadT](mikeyMsg)
-	if !ok {
-		return nil, fmt.Errorf("time payload not present")
-	}
-
-	ts := ntp.Decode(timePayload.TSValue)
-	diff := time.Since(ts)
-	if diff < -time.Hour || diff > time.Hour {
-		return nil, fmt.Errorf("NTP difference is too high")
-	}
-
-	spPayload, ok := mikeyGetPayload[*mikey.PayloadSP](mikeyMsg)
-	if !ok {
-		return nil, fmt.Errorf("SP payload not present")
-	}
-
-	v, ok := mikeyGetSPPolicy(spPayload, mikey.PayloadSPPolicyParamTypeEncrAlg)
-	if !ok || !bytes.Equal(v, []byte{1}) {
-		return nil, fmt.Errorf("missing or unsupported policy: PayloadSPPolicyParamTypeEncrAlg")
-	}
-
-	v, ok = mikeyGetSPPolicy(spPayload, mikey.PayloadSPPolicyParamTypeSessionEncrKeyLen)
-	if !ok || !bytes.Equal(v, []byte{0x10}) {
-		return nil, fmt.Errorf("missing or unsupported policy: PayloadSPPolicyParamTypeSessionEncrKeyLen")
-	}
-
-	v, ok = mikeyGetSPPolicy(spPayload, mikey.PayloadSPPolicyParamTypeAuthAlg)
-	if !ok || !bytes.Equal(v, []byte{1}) {
-		return nil, fmt.Errorf("missing or unsupported policy: PayloadSPPolicyParamTypeAuthAlg")
-	}
-
-	v, ok = mikeyGetSPPolicy(spPayload, mikey.PayloadSPPolicyParamTypeSessionAuthKeyLen)
-	if !ok || !bytes.Equal(v, []byte{0x0a}) {
-		return nil, fmt.Errorf("missing or unsupported policy: PayloadSPPolicyParamTypeSessionAuthKeyLen")
-	}
-
-	v, ok = mikeyGetSPPolicy(spPayload, mikey.PayloadSPPolicyParamTypeSRTPEncrOffOn)
-	if !ok || !bytes.Equal(v, []byte{1}) {
-		return nil, fmt.Errorf("missing or unsupported policy: PayloadSPPolicyParamTypeSRTPEncrOffOn")
-	}
-
-	v, ok = mikeyGetSPPolicy(spPayload, mikey.PayloadSPPolicyParamTypeSRTCPEncrOffOn)
-	if !ok || !bytes.Equal(v, []byte{1}) {
-		return nil, fmt.Errorf("missing or unsupported policy: PayloadSPPolicyParamTypeSRTCPEncrOffOn")
-	}
-
-	v, ok = mikeyGetSPPolicy(spPayload, mikey.PayloadSPPolicyParamTypeSRTPAuthOffOn)
-	if !ok || !bytes.Equal(v, []byte{1}) {
-		return nil, fmt.Errorf("missing or unsupported policy: PayloadSPPolicyParamTypeSRTPAuthOffOn")
-	}
-
-	kemacPayload, ok := mikeyGetPayload[*mikey.PayloadKEMAC](mikeyMsg)
-	if !ok {
-		return nil, fmt.Errorf("KEMAC payload not present")
-	}
-
-	if len(kemacPayload.SubPayloads) != 1 {
-		return nil, fmt.Errorf("multiple keys are present")
-	}
-
-	if len(kemacPayload.SubPayloads[0].KeyData) != srtpKeyLength {
-		return nil, fmt.Errorf("unexpected key size: %d", len(kemacPayload.SubPayloads[0].KeyData))
-	}
-
-	ssrcs := make([]uint32, len(mikeyMsg.Header.CSIDMapInfo))
-	startROCs := make([]uint32, len(mikeyMsg.Header.CSIDMapInfo))
-
-	for i, entry := range mikeyMsg.Header.CSIDMapInfo {
-		ssrcs[i] = entry.SSRC
-		startROCs[i] = entry.ROC
-	}
-
-	srtpCtx := &wrappedSRTPContext{
-		key:       kemacPayload.SubPayloads[0].KeyData,
-		ssrcs:     ssrcs,
-		startROCs: startROCs,
-	}
-	err := srtpCtx.initialize()
-	if err != nil {
-		return nil, err
-	}
-
-	return srtpCtx, nil
-}
-
 func generateRTPInfoEntry(ssm *serverStreamMedia, now time.Time) *headers.RTPInfoEntry {
 	// do not generate a RTP-Info entry when
 	// there are multiple formats inside a single media stream,
@@ -436,7 +329,7 @@ type ServerSession struct {
 	ctxCancel             func()
 	propsMutex            sync.RWMutex
 	conns                 map[*ServerConn]struct{}
-	userData              interface{}
+	userData              any
 	state                 ServerSessionState
 	setuppedMedias        map[*description.Media]*serverSessionMedia
 	setuppedMediasOrdered []*serverSessionMedia
@@ -543,12 +436,12 @@ func (ss *ServerSession) Medias() []*description.Media {
 }
 
 // SetUserData sets some user data associated with the session.
-func (ss *ServerSession) SetUserData(v interface{}) {
+func (ss *ServerSession) SetUserData(v any) {
 	ss.userData = v
 }
 
 // UserData returns some user data associated with the session.
-func (ss *ServerSession) UserData() interface{} {
+func (ss *ServerSession) UserData() any {
 	return ss.userData
 }
 
@@ -882,9 +775,14 @@ func (ss *ServerSession) runInner() error {
 						res.Header = make(base.Header)
 					}
 
+					// Media Foundation-based software, like Windows Media Player,
+					// send keepalives at an interval equal the timeout value.
+					// prevent timeouts by subtracting 5 seconds from the value.
+					timeout := max(int(ss.s.IdleTimeout/time.Second)-5, 1)
+
 					res.Header["Session"] = headers.Session{
 						Session: ss.secretID,
-						Timeout: ptrOf(uint(ss.s.IdleTimeout / time.Second)),
+						Timeout: ptrOf(uint(timeout)),
 					}.Marshal()
 				}
 
diff --git a/wrapped_srtp_context.go b/wrapped_srtp_context.go
index 2a2a5238..197844d7 100644
--- a/wrapped_srtp_context.go
+++ b/wrapped_srtp_context.go
@@ -1,13 +1,222 @@
 package gortsplib
 
 import (
+	"bytes"
+	"crypto/rand"
+	"fmt"
 	"sync"
+	"time"
 
+	"github.com/bluenviron/gortsplib/v5/pkg/mikey"
+	"github.com/bluenviron/gortsplib/v5/pkg/ntp"
 	"github.com/pion/rtcp"
 	"github.com/pion/rtp"
 	"github.com/pion/srtp/v3"
 )
 
+func mikeyGetPayload[T mikey.Payload](mikeyMsg *mikey.Message) (T, bool) {
+	var zero T
+	for _, wrapped := range mikeyMsg.Payloads {
+		if val, ok := wrapped.(T); ok {
+			return val, true
+		}
+	}
+	return zero, false
+}
+
+func mikeyGetSPPolicy(spPayload *mikey.PayloadSP, typ mikey.PayloadSPPolicyParamType) ([]byte, bool) {
+	for _, pl := range spPayload.PolicyParams {
+		if pl.Type == typ {
+			return pl.Value, true
+		}
+	}
+	return nil, false
+}
+
+func mikeyToContext(mikeyMsg *mikey.Message) (*wrappedSRTPContext, error) {
+	timePayload, ok := mikeyGetPayload[*mikey.PayloadT](mikeyMsg)
+	if !ok {
+		return nil, fmt.Errorf("time payload not present")
+	}
+
+	ts := ntp.Decode(timePayload.TSValue)
+	diff := time.Since(ts)
+	if diff < -time.Hour || diff > time.Hour {
+		return nil, fmt.Errorf("NTP difference is too high")
+	}
+
+	spPayload, ok := mikeyGetPayload[*mikey.PayloadSP](mikeyMsg)
+	if !ok {
+		return nil, fmt.Errorf("SP payload not present")
+	}
+
+	v, ok := mikeyGetSPPolicy(spPayload, mikey.PayloadSPPolicyParamTypeEncrAlg)
+	if !ok || !bytes.Equal(v, []byte{1}) {
+		return nil, fmt.Errorf("missing or unsupported policy: PayloadSPPolicyParamTypeEncrAlg")
+	}
+
+	v, ok = mikeyGetSPPolicy(spPayload, mikey.PayloadSPPolicyParamTypeSessionEncrKeyLen)
+	if !ok || !bytes.Equal(v, []byte{16}) {
+		return nil, fmt.Errorf("missing or unsupported policy: PayloadSPPolicyParamTypeSessionEncrKeyLen")
+	}
+
+	v, ok = mikeyGetSPPolicy(spPayload, mikey.PayloadSPPolicyParamTypeAuthAlg)
+	if !ok || !bytes.Equal(v, []byte{1}) {
+		return nil, fmt.Errorf("missing or unsupported policy: PayloadSPPolicyParamTypeAuthAlg")
+	}
+
+	v, ok = mikeyGetSPPolicy(spPayload, mikey.PayloadSPPolicyParamTypeSRTPEncrOffOn)
+	if !ok || !bytes.Equal(v, []byte{1}) {
+		return nil, fmt.Errorf("missing or unsupported policy: PayloadSPPolicyParamTypeSRTPEncrOffOn")
+	}
+
+	v, ok = mikeyGetSPPolicy(spPayload, mikey.PayloadSPPolicyParamTypeSRTCPEncrOffOn)
+	if !ok || !bytes.Equal(v, []byte{1}) {
+		return nil, fmt.Errorf("missing or unsupported policy: PayloadSPPolicyParamTypeSRTCPEncrOffOn")
+	}
+
+	v, ok = mikeyGetSPPolicy(spPayload, mikey.PayloadSPPolicyParamTypeSRTPAuthOffOn)
+	if !ok || !bytes.Equal(v, []byte{1}) {
+		return nil, fmt.Errorf("missing or unsupported policy: PayloadSPPolicyParamTypeSRTPAuthOffOn")
+	}
+
+	kemacPayload, ok := mikeyGetPayload[*mikey.PayloadKEMAC](mikeyMsg)
+	if !ok {
+		return nil, fmt.Errorf("KEMAC payload not present")
+	}
+
+	if len(kemacPayload.SubPayloads) != 1 {
+		return nil, fmt.Errorf("multiple keys are present")
+	}
+
+	if len(kemacPayload.SubPayloads[0].KeyData) != srtpKeyLength {
+		return nil, fmt.Errorf("unexpected key size: %d", len(kemacPayload.SubPayloads[0].KeyData))
+	}
+
+	ssrcs := make([]uint32, len(mikeyMsg.Header.CSIDMapInfo))
+	startROCs := make([]uint32, len(mikeyMsg.Header.CSIDMapInfo))
+
+	for i, entry := range mikeyMsg.Header.CSIDMapInfo {
+		ssrcs[i] = entry.SSRC
+		startROCs[i] = entry.ROC
+	}
+
+	srtpCtx := &wrappedSRTPContext{
+		key:       kemacPayload.SubPayloads[0].KeyData,
+		ssrcs:     ssrcs,
+		startROCs: startROCs,
+	}
+	err := srtpCtx.initialize()
+	if err != nil {
+		return nil, err
+	}
+
+	return srtpCtx, nil
+}
+
+func mikeyGenerate(ctx *wrappedSRTPContext) (*mikey.Message, error) {
+	csbID, err := randUint32()
+	if err != nil {
+		return nil, err
+	}
+
+	msg := &mikey.Message{
+		Header: mikey.Header{
+			Version: 1,
+			CSBID:   csbID,
+		},
+	}
+
+	msg.Header.CSIDMapInfo = make([]mikey.SRTPIDEntry, len(ctx.ssrcs))
+
+	n := 0
+	for _, ssrc := range ctx.ssrcs {
+		msg.Header.CSIDMapInfo[n] = mikey.SRTPIDEntry{
+			PolicyNo: 0,
+			SSRC:     ssrc,
+			ROC:      ctx.roc(ssrc),
+		}
+		n++
+	}
+
+	randData := make([]byte, 16)
+	_, err = rand.Read(randData)
+	if err != nil {
+		return nil, err
+	}
+
+	keyLen, err := ctx.profile.KeyLen()
+	if err != nil {
+		return nil, err
+	}
+
+	authKeyLen, err := ctx.profile.AuthKeyLen()
+	if err != nil {
+		return nil, err
+	}
+
+	authTagLen, err := ctx.profile.AuthTagRTPLen()
+	if err != nil {
+		return nil, err
+	}
+
+	msg.Payloads = []mikey.Payload{
+		&mikey.PayloadT{
+			TSType:  0,
+			TSValue: ntp.Encode(time.Now()),
+		},
+		&mikey.PayloadRAND{
+			Data: randData,
+		},
+		&mikey.PayloadSP{
+			PolicyParams: []mikey.PayloadSPPolicyParam{
+				{
+					Type:  mikey.PayloadSPPolicyParamTypeEncrAlg,
+					Value: []byte{1},
+				},
+				{
+					Type:  mikey.PayloadSPPolicyParamTypeSessionEncrKeyLen,
+					Value: []byte{byte(keyLen)},
+				},
+				{
+					Type:  mikey.PayloadSPPolicyParamTypeAuthAlg,
+					Value: []byte{1},
+				},
+				{
+					Type:  mikey.PayloadSPPolicyParamTypeSessionAuthKeyLen,
+					Value: []byte{byte(authKeyLen)},
+				},
+				{
+					Type:  mikey.PayloadSPPolicyParamTypeSRTPEncrOffOn,
+					Value: []byte{1},
+				},
+				{
+					Type:  mikey.PayloadSPPolicyParamTypeSRTCPEncrOffOn,
+					Value: []byte{1},
+				},
+				{
+					Type:  mikey.PayloadSPPolicyParamTypeSRTPAuthOffOn,
+					Value: []byte{1},
+				},
+				{
+					Type:  mikey.PayloadSPPolicyParamTypeAuthTagLen,
+					Value: []byte{byte(authTagLen)},
+				},
+			},
+		},
+		&mikey.PayloadKEMAC{
+			SubPayloads: []*mikey.SubPayloadKeyData{
+				{
+					Type:    mikey.SubPayloadKeyDataKeyTypeTEK,
+					KeyData: ctx.key,
+				},
+			},
+		},
+	}
+
+	return msg, nil
+}
+
 // srtp.Context with
 // - accessible key
 // - accessible SSRCs
@@ -17,13 +226,16 @@ type wrappedSRTPContext struct {
 	ssrcs     []uint32
 	startROCs []uint32
 
-	w     *srtp.Context
-	mutex sync.RWMutex
+	profile srtp.ProtectionProfile
+	w       *srtp.Context
+	mutex   sync.RWMutex
 }
 
 func (ctx *wrappedSRTPContext) initialize() error {
+	ctx.profile = srtp.ProtectionProfileAes128CmHmacSha1_80
+
 	var err error
-	ctx.w, err = srtp.CreateContext(ctx.key[:16], ctx.key[16:], srtp.ProtectionProfileAes128CmHmacSha1_80)
+	ctx.w, err = srtp.CreateContext(ctx.key[:16], ctx.key[16:], ctx.profile)
 	if err != nil {
 		return err
 	}



Test patch:
diff --git a/client_play_test.go b/client_play_test.go
index 2e8653fd..382ca011 100644
--- a/client_play_test.go
+++ b/client_play_test.go
@@ -182,7 +182,7 @@ func TestClientPlayFormats(t *testing.T) {
 		})
 		require.NoError(t, err2)
 
-		for i := 0; i < 3; i++ {
+		for i := range 3 {
 			req, err2 = conn.ReadRequest()
 			require.NoError(t, err2)
 			require.Equal(t, base.Setup, req.Method)
@@ -375,7 +375,7 @@ func TestClientPlay(t *testing.T) {
 				var srtpInCtx [2]*wrappedSRTPContext
 				var srtpOutCtx [2]*wrappedSRTPContext
 
-				for i := 0; i < 2; i++ {
+				for i := range 2 {
 					req, err2 = conn.ReadRequest()
 					require.NoError(t, err2)
 					require.Equal(t, base.Setup, req.Method)
@@ -514,7 +514,7 @@ func TestClientPlay(t *testing.T) {
 
 				// server -> client
 
-				for i := 0; i < 2; i++ {
+				for i := range 2 {
 					buf := testRTPPacketMarshaled
 
 					if ca.secure == "secure" {
@@ -551,7 +551,7 @@ func TestClientPlay(t *testing.T) {
 				// skip firewall opening
 
 				if ca.transport == "udp" {
-					for i := 0; i < 2; i++ {
+					for i := range 2 {
 						buf := make([]byte, 2048)
 						_, _, err2 = l2s[i].ReadFrom(buf)
 						require.NoError(t, err2)
@@ -560,7 +560,7 @@ func TestClientPlay(t *testing.T) {
 
 				// client -> server
 
-				for i := 0; i < 2; i++ {
+				for i := range 2 {
 					var buf []byte
 
 					switch ca.transport {
@@ -3711,7 +3711,7 @@ func TestClientPlayBackChannel(t *testing.T) {
 				var l2s [2]net.PacketConn
 				var clientPorts [2]*[2]int
 
-				for i := 0; i < 2; i++ {
+				for i := range 2 {
 					req, err2 = conn.ReadRequest()
 					require.NoError(t, err2)
 					require.Equal(t, base.Setup, req.Method)
@@ -3908,7 +3908,7 @@ func TestClientPlayBackChannel(t *testing.T) {
 					}
 					return ptrOf(ProtocolUDP)
 				}(),
-				senderReportPeriod:   500 * time.Millisecond,
+				senderReportPeriod:   750 * time.Millisecond,
 				receiverReportPeriod: 750 * time.Millisecond,
 			}
 
diff --git a/client_record_test.go b/client_record_test.go
index 03837a8f..68077132 100644
--- a/client_record_test.go
+++ b/client_record_test.go
@@ -1349,7 +1349,7 @@ func TestClientRecordRTCPReport(t *testing.T) {
 					require.NoError(t, err2)
 					buf = buf[:n]
 				} else {
-					for i := 0; i < 2; i++ {
+					for range 2 {
 						_, err2 = conn.ReadInterleavedFrame()
 						require.NoError(t, err2)
 					}
diff --git a/internal/teste2e/server_vs_external_test.go b/internal/teste2e/server_vs_external_test.go
index bf5f88a3..b7f9dc39 100644
--- a/internal/teste2e/server_vs_external_test.go
+++ b/internal/teste2e/server_vs_external_test.go
@@ -79,12 +79,10 @@ func TestServerVsExternal(t *testing.T) {
 
 	for _, file := range files {
 		cfile := file
-		wg.Add(1)
-		go func() {
+		wg.Go(func() {
 			err := buildImage(cfile.Name())
 			require.NoError(t, err)
-			wg.Done()
-		}()
+		})
 	}
 
 	wg.Wait()
diff --git a/pkg/base/header_test.go b/pkg/base/header_test.go
index 96205bc6..a3d4bcc4 100644
--- a/pkg/base/header_test.go
+++ b/pkg/base/header_test.go
@@ -134,7 +134,7 @@ func FuzzHeaderUnmarshal(f *testing.F) {
 	}
 
 	str := ""
-	for i := 0; i < 300; i++ {
+	for range 300 {
 		str += "Key: val\r\n"
 	}
 	f.Add([]byte(str))
diff --git a/pkg/conn/conn_test.go b/pkg/conn/conn_test.go
index e287a64e..30c14058 100644
--- a/pkg/conn/conn_test.go
+++ b/pkg/conn/conn_test.go
@@ -22,7 +22,7 @@ func TestRead(t *testing.T) {
 	for _, ca := range []struct {
 		name string
 		enc  []byte
-		dec  interface{}
+		dec  any
 	}{
 		{
 			"request",
diff --git a/pkg/ringbuffer/ringbuffer_test.go b/pkg/ringbuffer/ringbuffer_test.go
index 9c747a84..a7a0e8d7 100644
--- a/pkg/ringbuffer/ringbuffer_test.go
+++ b/pkg/ringbuffer/ringbuffer_test.go
@@ -79,15 +79,15 @@ func TestOverflow(t *testing.T) {
 	r, err := New(32)
 	require.NoError(t, err)
 
-	for i := 0; i < 32; i++ {
+	for range 32 {
 		r.Push([]byte{1, 2, 3, 4})
 	}
 
 	ok := r.Push([]byte{5, 6, 7, 8})
 	require.Equal(t, false, ok)
 
-	for i := 0; i < 32; i++ {
-		var data interface{}
+	for range 32 {
+		var data any
 		data, ok = r.Pull()
 		require.Equal(t, true, ok)
 		require.Equal(t, []byte{1, 2, 3, 4}, data)
@@ -100,16 +100,16 @@ func BenchmarkPushPullContinuous(b *testing.B) {
 
 	data := make([]byte, 1024)
 
-	for n := 0; n < b.N; n++ {
+	for b.Loop() {
 		done := make(chan struct{})
 		go func() {
 			defer close(done)
-			for i := 0; i < 1024*8; i++ {
+			for range 1024 * 8 {
 				r.Push(data)
 			}
 		}()
 
-		for i := 0; i < 1024*8; i++ {
+		for range 1024 * 8 {
 			r.Pull()
 		}
 
@@ -123,17 +123,17 @@ func BenchmarkPushPullPaused5(b *testing.B) {
 
 	data := make([]byte, 1024)
 
-	for n := 0; n < b.N; n++ {
+	for b.Loop() {
 		done := make(chan struct{})
 		go func() {
 			defer close(done)
-			for i := 0; i < 128; i++ {
+			for range 128 {
 				r.Push(data)
 				time.Sleep(5 * time.Millisecond)
 			}
 		}()
 
-		for i := 0; i < 128; i++ {
+		for range 128 {
 			r.Pull()
 		}
 
@@ -147,17 +147,17 @@ func BenchmarkPushPullPaused10(b *testing.B) {
 
 	data := make([]byte, 1024)
 
-	for n := 0; n < b.N; n++ {
+	for b.Loop() {
 		done := make(chan struct{})
 		go func() {
 			defer close(done)
-			for i := 0; i < 128; i++ {
+			for range 128 {
 				r.Push(data)
 				time.Sleep(10 * time.Millisecond)
 			}
 		}()
 
-		for i := 0; i < 128; i++ {
+		for range 128 {
 			r.Pull()
 		}
 
diff --git a/server_play_test.go b/server_play_test.go
index 73622318..8633d30d 100644
--- a/server_play_test.go
+++ b/server_play_test.go
@@ -535,7 +535,7 @@ func TestServerPlaySetupErrorSameUDPPortsAndIP(t *testing.T) {
 	require.NoError(t, err)
 	defer stream.Close()
 
-	for i := 0; i < 2; i++ {
+	for i := range 2 {
 		var nconn net.Conn
 		nconn, err = net.Dial("tcp", "localhost:8554")
 		require.NoError(t, err)
@@ -2738,7 +2738,7 @@ func TestServerPlayBackChannel(t *testing.T) {
 			var l1s [2]net.PacketConn
 			var l2s [2]net.PacketConn
 
-			for i := 0; i < 2; i++ {
+			for i := range 2 {
 				inTH := &headers.Transport{
 					Mode: ptrOf(headers.TransportModePlay),
 				}
diff --git a/server_record_test.go b/server_record_test.go
index 7e615ca0..04bf6f39 100644
--- a/server_record_test.go
+++ b/server_record_test.go
@@ -652,7 +652,7 @@ func TestServerRecord(t *testing.T) {
 						err = ctx.Session.WritePacketRTCP(ctx.Session.AnnouncedDescription().Medias[1], &testRTCPPacket)
 						require.NoError(t, err)
 
-						for i := 0; i < 2; i++ {
+						for i := range 2 {
 							ctx.Session.OnPacketRTP(
 								ctx.Session.AnnouncedDescription().Medias[i],
 								ctx.Session.AnnouncedDescription().Medias[i].Formats[0],
@@ -740,7 +740,7 @@ func TestServerRecord(t *testing.T) {
 			var srtpOutCtx [2]*wrappedSRTPContext
 			var srtpInCtx [2]*wrappedSRTPContext
 
-			for i := 0; i < 2; i++ {
+			for i := range 2 {
 				inTH := &headers.Transport{
 					Delivery: ptrOf(headers.TransportDeliveryUnicast),
 					Mode:     ptrOf(headers.TransportModeRecord),
@@ -835,7 +835,7 @@ func TestServerRecord(t *testing.T) {
 			// skip firewall opening
 
 			if ca.transport == "udp" {
-				for i := 0; i < 2; i++ {
+				for i := range 2 {
 					buf := make([]byte, 2048)
 					_, _, err = l2s[i].ReadFrom(buf)
 					require.NoError(t, err)
@@ -844,7 +844,7 @@ func TestServerRecord(t *testing.T) {
 
 			// server -> client
 
-			for i := 0; i < 2; i++ {
+			for i := range 2 {
 				var buf []byte
 
 				if ca.transport == "udp" {
@@ -871,7 +871,7 @@ func TestServerRecord(t *testing.T) {
 
 			// client -> server
 
-			for i := 0; i < 2; i++ {
+			for i := range 2 {
 				buf1 := testRTPPacketMarshaled
 
 				if ca.secure == "secure" {
@@ -919,7 +919,7 @@ func TestServerRecord(t *testing.T) {
 
 			// server -> client
 
-			for i := 0; i < 2; i++ {
+			for i := range 2 {
 				var buf []byte
 
 				if ca.transport == "udp" {
diff --git a/server_test.go b/server_test.go
index 57dad2a3..b9c84bcd 100644
--- a/server_test.go
+++ b/server_test.go
@@ -240,6 +240,20 @@ func TestServerClose(t *testing.T) {
 	s.Close()
 }
 
+func TestServerNetListener(t *testing.T) {
+	s := &Server{
+		Handler:     &testServerHandler{},
+		RTSPAddress: "127.0.0.1:8554",
+	}
+
+	err := s.Start()
+	require.NoError(t, err)
+	defer s.Close()
+
+	ln := s.NetListener()
+	require.Equal(t, "127.0.0.1:8554", ln.Addr().String())
+}
+
 func TestServerErrorInvalidUDPPorts(t *testing.T) {
 	t.Run("non consecutive", func(t *testing.T) {
 		s := &Server{