Skip to content

Commit 0411da6

Browse files
authored
Simplify number matching rules. Make '+' optional. (#737)
1 parent 114aac1 commit 0411da6

File tree

4 files changed

+136
-18
lines changed

4 files changed

+136
-18
lines changed

.changeset/clean-comics-cheat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"github.com/livekit/protocol": minor
3+
---
4+
5+
Simplify number matching rules for SIP.

rpc/sip.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package rpc
22

3-
import "github.com/livekit/protocol/livekit"
3+
import (
4+
"strings"
5+
6+
"github.com/livekit/protocol/livekit"
7+
)
48

59
// NewCreateSIPParticipantRequest fills InternalCreateSIPParticipantRequest from
610
// livekit.CreateSIPParticipantRequest and livekit.SIPTrunkInfo.
@@ -9,11 +13,23 @@ func NewCreateSIPParticipantRequest(
913
req *livekit.CreateSIPParticipantRequest,
1014
trunk *livekit.SIPTrunkInfo,
1115
) *InternalCreateSIPParticipantRequest {
16+
// A sanity check for the number format for well-known providers.
17+
outboundNumber := trunk.OutboundNumber
18+
switch {
19+
case strings.HasSuffix(trunk.OutboundAddress, "telnyx.com"):
20+
// Telnyx omits leading '+' by default.
21+
outboundNumber = strings.TrimPrefix(outboundNumber, "+")
22+
case strings.HasSuffix(trunk.OutboundAddress, "twilio.com"):
23+
// Twilio requires leading '+'.
24+
if !strings.HasPrefix(outboundNumber, "+") {
25+
outboundNumber = "+" + outboundNumber
26+
}
27+
}
1228
return &InternalCreateSIPParticipantRequest{
1329
SipCallId: callID,
1430
Address: trunk.OutboundAddress,
1531
Transport: trunk.Transport,
16-
Number: trunk.OutboundNumber,
32+
Number: outboundNumber,
1733
Username: trunk.OutboundUsername,
1834
Password: trunk.OutboundPassword,
1935
CallTo: req.SipCallTo,

sip/sip.go

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ func ValidateDispatchRules(rules []*livekit.SIPDispatchRuleInfo) error {
139139
}
140140
for _, trunk := range trunks {
141141
for _, number := range numbers {
142-
key := ruleKey{Pin: pin, Trunk: trunk, Number: number}
142+
key := ruleKey{Pin: pin, Trunk: trunk, Number: normalizeNumber(number)}
143143
r2 := byRuleKey[key]
144144
if r2 != nil {
145145
return fmt.Errorf("Conflicting SIP Dispatch Rules: same Trunk+Number+PIN combination for for %q and %q",
@@ -190,6 +190,18 @@ func printNumber(s string) string {
190190
return strconv.Quote(s)
191191
}
192192

193+
func normalizeNumber(num string) string {
194+
if num == "" {
195+
return ""
196+
}
197+
// TODO: Always keep "number" as-is if it's not E.164.
198+
// This will only matter for native SIP clients which have '+' in the username.
199+
if !strings.HasPrefix(num, `+`) {
200+
num = "+" + num
201+
}
202+
return num
203+
}
204+
193205
// ValidateTrunks checks a set of trunks for conflicts.
194206
func ValidateTrunks(trunks []*livekit.SIPTrunkInfo) error {
195207
if len(trunks) == 0 {
@@ -200,10 +212,11 @@ func ValidateTrunks(trunks []*livekit.SIPTrunkInfo) error {
200212
if len(t.InboundNumbersRegex) != 0 {
201213
continue // can't effectively validate these
202214
}
203-
byInbound := byOutboundAndInbound[t.OutboundNumber]
215+
outboundKey := normalizeNumber(t.OutboundNumber)
216+
byInbound := byOutboundAndInbound[outboundKey]
204217
if byInbound == nil {
205218
byInbound = make(map[string]*livekit.SIPTrunkInfo)
206-
byOutboundAndInbound[t.OutboundNumber] = byInbound
219+
byOutboundAndInbound[outboundKey] = byInbound
207220
}
208221
if len(t.InboundNumbers) == 0 {
209222
if t2 := byInbound[""]; t2 != nil {
@@ -213,19 +226,20 @@ func ValidateTrunks(trunks []*livekit.SIPTrunkInfo) error {
213226
byInbound[""] = t
214227
} else {
215228
for _, num := range t.InboundNumbers {
216-
t2 := byInbound[num]
229+
inboundKey := normalizeNumber(num)
230+
t2 := byInbound[inboundKey]
217231
if t2 != nil {
218232
return fmt.Errorf("Conflicting SIP Trunks: %q and %q, using the same OutboundNumber %s and InboundNumber %q",
219233
printID(t.SipTrunkId), printID(t2.SipTrunkId), printNumber(t.OutboundNumber), num)
220234
}
221-
byInbound[num] = t
235+
byInbound[inboundKey] = t
222236
}
223237
}
224238
}
225239
return nil
226240
}
227241

228-
func matchAddrs(addr string, mask string) bool {
242+
func matchAddrMask(addr string, mask string) bool {
229243
if !strings.Contains(mask, "/") {
230244
return addr == mask
231245
}
@@ -240,16 +254,29 @@ func matchAddrs(addr string, mask string) bool {
240254
return pref.Contains(ip)
241255
}
242256

243-
func matchAddr(addr string, masks []string) bool {
244-
if addr == "" {
257+
func matchAddrMasks(addr string, masks []string) bool {
258+
if addr == "" || len(masks) == 0 {
245259
return true
246260
}
247261
for _, mask := range masks {
248-
if !matchAddrs(addr, mask) {
249-
return false
262+
if matchAddrMask(addr, mask) {
263+
return true
264+
}
265+
}
266+
return false
267+
}
268+
269+
func matchNumbers(num string, allowed []string) bool {
270+
if len(allowed) == 0 {
271+
return true
272+
}
273+
num = normalizeNumber(num)
274+
for _, allow := range allowed {
275+
if num == normalizeNumber(allow) {
276+
return true
250277
}
251278
}
252-
return true
279+
return false
253280
}
254281

255282
// MatchTrunk finds a SIP Trunk definition matching the request.
@@ -260,12 +287,13 @@ func MatchTrunk(trunks []*livekit.SIPTrunkInfo, srcIP, calling, called string) (
260287
defaultTrunk *livekit.SIPTrunkInfo
261288
defaultTrunkCnt int // to error in case there are multiple ones
262289
)
290+
calledNorm := normalizeNumber(called)
263291
for _, tr := range trunks {
264292
// Do not consider it if number doesn't match.
265-
if len(tr.InboundNumbers) != 0 && !slices.Contains(tr.InboundNumbers, calling) {
293+
if !matchNumbers(calling, tr.InboundNumbers) {
266294
continue
267295
}
268-
if !matchAddr(srcIP, tr.InboundAddresses) {
296+
if !matchAddrMasks(srcIP, tr.InboundAddresses) {
269297
continue
270298
}
271299
// Deprecated, but we still check it for backward compatibility.
@@ -289,7 +317,7 @@ func MatchTrunk(trunks []*livekit.SIPTrunkInfo, srcIP, calling, called string) (
289317
// Default/wildcard trunk.
290318
defaultTrunk = tr
291319
defaultTrunkCnt++
292-
} else if tr.OutboundNumber == called {
320+
} else if normalizeNumber(tr.OutboundNumber) == calledNorm {
293321
// Trunk specific to the number.
294322
if selectedTrunk != nil {
295323
return nil, fmt.Errorf("Multiple SIP Trunks matched for %q", called)

sip/sip_test.go

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ var trunkCases = []struct {
3939
exp int
4040
expErr bool
4141
invalid bool
42+
from string
43+
to string
44+
src string
4245
}{
4346
{
4447
name: "empty",
@@ -151,13 +154,79 @@ var trunkCases = []struct {
151154
expErr: true,
152155
invalid: true,
153156
},
157+
{
158+
name: "inbound with ip exact",
159+
trunks: []*livekit.SIPTrunkInfo{
160+
{SipTrunkId: "bbb", OutboundNumber: sipNumber2, InboundAddresses: []string{
161+
"10.10.10.10",
162+
"1.1.1.1",
163+
}},
164+
},
165+
exp: 0,
166+
},
167+
{
168+
name: "inbound with ip exact miss",
169+
trunks: []*livekit.SIPTrunkInfo{
170+
{SipTrunkId: "bbb", OutboundNumber: sipNumber2, InboundAddresses: []string{
171+
"10.10.10.10",
172+
}},
173+
},
174+
exp: -1,
175+
},
176+
{
177+
name: "inbound with ip mask",
178+
trunks: []*livekit.SIPTrunkInfo{
179+
{SipTrunkId: "bbb", OutboundNumber: sipNumber2, InboundAddresses: []string{
180+
"10.10.10.0/24",
181+
"1.1.1.0/24",
182+
}},
183+
},
184+
exp: 0,
185+
},
186+
{
187+
name: "inbound with ip mask miss",
188+
trunks: []*livekit.SIPTrunkInfo{
189+
{SipTrunkId: "bbb", OutboundNumber: sipNumber2, InboundAddresses: []string{
190+
"10.10.10.0/24",
191+
}},
192+
},
193+
exp: -1,
194+
},
195+
{
196+
name: "inbound with plus",
197+
trunks: []*livekit.SIPTrunkInfo{
198+
{SipTrunkId: "aaa", OutboundNumber: "+" + sipNumber3},
199+
{SipTrunkId: "bbb", OutboundNumber: "+" + sipNumber2},
200+
},
201+
exp: 1,
202+
},
203+
{
204+
name: "inbound without plus",
205+
trunks: []*livekit.SIPTrunkInfo{
206+
{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
207+
{SipTrunkId: "bbb", OutboundNumber: sipNumber2},
208+
},
209+
from: "+" + sipNumber1,
210+
to: "+" + sipNumber2,
211+
exp: 1,
212+
},
154213
}
155214

156215
func TestSIPMatchTrunk(t *testing.T) {
157216
for _, c := range trunkCases {
158217
c := c
159218
t.Run(c.name, func(t *testing.T) {
160-
got, err := MatchTrunk(c.trunks, "", sipNumber1, sipNumber2)
219+
from, to, src := c.from, c.to, c.src
220+
if from == "" {
221+
from = sipNumber1
222+
}
223+
if to == "" {
224+
to = sipNumber2
225+
}
226+
if src == "" {
227+
src = "1.1.1.1"
228+
}
229+
got, err := MatchTrunk(c.trunks, src, from, to)
161230
if c.expErr {
162231
require.Error(t, err)
163232
require.Nil(t, got)
@@ -530,7 +599,7 @@ func TestMatchIP(t *testing.T) {
530599
}
531600
for _, c := range cases {
532601
t.Run(c.mask, func(t *testing.T) {
533-
got := matchAddrs(c.addr, c.mask)
602+
got := matchAddrMask(c.addr, c.mask)
534603
require.Equal(t, c.exp, got)
535604
})
536605
}

0 commit comments

Comments
 (0)