diff --git a/.changeset/sip-fix-trunk-encryption.md b/.changeset/sip-fix-trunk-encryption.md new file mode 100644 index 000000000..f1838449f --- /dev/null +++ b/.changeset/sip-fix-trunk-encryption.md @@ -0,0 +1,6 @@ +--- +"@livekit/protocol": patch +"github.com/livekit/protocol": patch +--- + +Fix SIP trunk-level MediaEncryption being silently dropped on outbound and inbound calls. The early `req.Upgrade()` / `rule.Upgrade()` calls pinned `Media.Encryption` to the (legacy) request/rule field before the trunk's MediaEncryption was merged, causing INVITEs to omit SRTP when only the trunk had it configured. diff --git a/livekit/sip.go b/livekit/sip.go index e16074e1a..64e110644 100644 --- a/livekit/sip.go +++ b/livekit/sip.go @@ -1081,7 +1081,8 @@ func (p *SIPMediaConfig) UpgradeWith(enc SIPMediaEncryption) *SIPMediaConfig { if p == nil { p = new(SIPMediaConfig) } - if p.Encryption == nil { + // Ignore DISABLE as it's a zero value which means "unset". + if p.Encryption == nil && enc != SIPMediaEncryption_SIP_MEDIA_ENCRYPT_DISABLE { p.Encryption = &enc } return p diff --git a/rpc/sip_test.go b/rpc/sip_test.go index f7d6fa5a1..cb5a81078 100644 --- a/rpc/sip_test.go +++ b/rpc/sip_test.go @@ -150,3 +150,28 @@ func TestNewCreateSIPParticipantRequest(t *testing.T) { require.NoError(t, err) require.True(t, proto.Equal(exp, res), "%v\nvs\n%v", exp, res) } + +// Regression: trunk-level MediaEncryption must be honored when the request specifies +// neither MediaEncryption nor Media. A prior version called req.Upgrade() at the top of +// NewCreateSIPParticipantRequest, which pinned req.Media.Encryption to req.MediaEncryption (0) +// before the trunk was consulted, causing outbound INVITEs to omit SRTP and upstream +// providers (e.g. Twilio) to reject with 488 / 32208. +func TestNewCreateSIPParticipantRequest_TrunkOnlyEncryption(t *testing.T) { + r := &livekit.CreateSIPParticipantRequest{ + SipTrunkId: "trunk", + SipCallTo: "+3333", + RoomName: "room", + } + tr := &livekit.SIPOutboundTrunkInfo{ + SipTrunkId: "trunk", + Address: "sip.example.com", + Numbers: []string{"+1111"}, + MediaEncryption: livekit.SIPMediaEncryption_SIP_MEDIA_ENCRYPT_REQUIRE, + } + res, err := NewCreateSIPParticipantRequest("p_123", "call-id", "xyz.sip.livekit.cloud", "url", "token", r, tr) + require.NoError(t, err) + require.Equal(t, livekit.SIPMediaEncryption_SIP_MEDIA_ENCRYPT_REQUIRE, res.MediaEncryption) + require.NotNil(t, res.Media) + require.NotNil(t, res.Media.Encryption) + require.Equal(t, livekit.SIPMediaEncryption_SIP_MEDIA_ENCRYPT_REQUIRE, *res.Media.Encryption) +} diff --git a/sip/sip_test.go b/sip/sip_test.go index 37bc0f703..53adf41f6 100644 --- a/sip/sip_test.go +++ b/sip/sip_test.go @@ -933,6 +933,34 @@ func TestEvaluateDispatchRule(t *testing.T) { }) } +// Regression: trunk-level MediaEncryption must be honored when the dispatch rule specifies +// neither MediaEncryption nor Media. A prior version called rule.Upgrade() at the top of +// EvaluateDispatchRule, which pinned rule.Media.Encryption to rule.MediaEncryption (0) +// before the trunk was consulted, causing the inbound trunk's encryption setting to be +// silently dropped. +func TestEvaluateDispatchRule_TrunkOnlyEncryption(t *testing.T) { + d := &livekit.SIPDispatchRuleInfo{ + SipDispatchRuleId: "rule", + Rule: newDirectDispatch("room", ""), + } + r := &rpc.EvaluateSIPDispatchRulesRequest{ + SipCallId: "call-id", + CallingNumber: "+11112222", + CallingHost: "sip.example.com", + CalledNumber: "+3333", + } + tr := &livekit.SIPInboundTrunkInfo{ + SipTrunkId: "trunk", + MediaEncryption: livekit.SIPMediaEncryption_SIP_MEDIA_ENCRYPT_REQUIRE, + } + res, err := EvaluateDispatchRule("p_123", tr, d, r) + require.NoError(t, err) + require.Equal(t, livekit.SIPMediaEncryption_SIP_MEDIA_ENCRYPT_REQUIRE, res.MediaEncryption) + require.NotNil(t, res.Media) + require.NotNil(t, res.Media.Encryption) + require.Equal(t, livekit.SIPMediaEncryption_SIP_MEDIA_ENCRYPT_REQUIRE, *res.Media.Encryption) +} + func TestMatchIP(t *testing.T) { cases := []struct { addr string