package sip
import (
"fmt"
"net/netip"
"regexp"
"strconv"
"testing"
"github.com/dennwc/iters"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/rpc"
)
func TestNormalizeNumber(t *testing.T) {
cases := []struct {
name string
num string
exp string
}{
{"empty", "", ""},
{"number", "123", "+123"},
{"plus", "+123", "+123"},
{"user", "user", "user"},
{"human", "(123) 456 7890", "+1234567890"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
require.Equal(t, c.exp, NormalizeNumber(c.num))
})
}
}
const (
sipNumber1 = "1111 1111"
sipNumber2 = "2222 2222"
sipNumber3 = "3333 3333"
sipTrunkID1 = "aaa"
sipTrunkID2 = "bbb"
)
var trunkCases = []struct {
name string
trunks []*livekit.SIPTrunkInfo
exp int
expErr bool
invalid bool
from string
to string
src string
host string
}{
{
name: "empty",
trunks: nil,
exp: -1, },
{
name: "one wildcard",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "aaa"},
},
exp: 0,
},
{
name: "matching",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "aaa", OutboundNumber: sipNumber2},
},
exp: 0,
},
{
name: "matching inbound",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbers: []string{sipNumber1}},
},
exp: 0,
},
{
name: "matching regexp",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbersRegex: []string{`^\d+ \d+$`}},
},
exp: 0,
},
{
name: "not matching",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
},
exp: -1,
},
{
name: "not matching inbound",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbers: []string{sipNumber1 + "1"}},
},
exp: -1,
},
{
name: "one match",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
{SipTrunkId: "bbb", OutboundNumber: sipNumber2},
},
exp: 1,
},
{
name: "many matches",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
{SipTrunkId: "bbb", OutboundNumber: sipNumber2},
{SipTrunkId: "ccc", OutboundNumber: sipNumber2},
},
expErr: true,
invalid: true,
},
{
name: "many matches default",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
{SipTrunkId: "bbb"},
{SipTrunkId: "ccc", OutboundNumber: sipNumber2},
{SipTrunkId: "ddd"},
},
exp: 2,
invalid: true, },
{
name: "inbound",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
{SipTrunkId: "bbb", OutboundNumber: sipNumber2},
{SipTrunkId: "ccc", OutboundNumber: sipNumber2, InboundNumbers: []string{sipNumber1 + "1"}},
},
exp: 1,
},
{
name: "multiple defaults",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
{SipTrunkId: "bbb"},
{SipTrunkId: "ccc"},
},
expErr: true,
invalid: true,
},
{
name: "inbound with ip exact",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "bbb", OutboundNumber: sipNumber2, InboundAddresses: []string{
"10.10.10.10",
"1.1.1.1",
}},
},
exp: 0,
},
{
name: "inbound with ip exact miss",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "bbb", OutboundNumber: sipNumber2, InboundAddresses: []string{
"10.10.10.10",
}},
},
exp: -1,
},
{
name: "inbound with ip mask",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "bbb", OutboundNumber: sipNumber2, InboundAddresses: []string{
"10.10.10.0/24",
"1.1.1.0/24",
}},
},
exp: 0,
},
{
name: "inbound with ip mask miss",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "bbb", OutboundNumber: sipNumber2, InboundAddresses: []string{
"10.10.10.0/24",
}},
},
exp: -1,
},
{
name: "inbound with host mask",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "bbb", OutboundNumber: sipNumber2, InboundAddresses: []string{
"10.10.10.0/24",
"sip.example.com",
}},
},
exp: 0,
},
{
name: "inbound with plus",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "aaa", OutboundNumber: "+" + sipNumber3},
{SipTrunkId: "bbb", OutboundNumber: "+" + sipNumber2},
},
exp: 1,
},
{
name: "inbound without plus",
trunks: []*livekit.SIPTrunkInfo{
{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
{SipTrunkId: "bbb", OutboundNumber: sipNumber2},
},
from: "+" + sipNumber1,
to: "+" + sipNumber2,
exp: 1,
},
}
func toInboundTrunks(trunks []*livekit.SIPTrunkInfo) []*livekit.SIPInboundTrunkInfo {
out := make([]*livekit.SIPInboundTrunkInfo, 0, len(trunks))
for _, t := range trunks {
out = append(out, t.AsInbound())
}
return out
}
func TestSIPMatchTrunk(t *testing.T) {
for _, c := range trunkCases {
c := c
t.Run(c.name, func(t *testing.T) {
from, to, src, host := c.from, c.to, c.src, c.host
if from == "" {
from = sipNumber1
}
if to == "" {
to = sipNumber2
}
if src == "" {
src = "1.1.1.1"
}
if host == "" {
host = "sip.example.com"
}
trunks := toInboundTrunks(c.trunks)
call := &rpc.SIPCall{
SipCallId: "test-call-id",
SourceIp: src,
From: &livekit.SIPUri{
User: from,
Host: host,
},
To: &livekit.SIPUri{
User: to,
},
}
call.Address = call.To
got, err := MatchTrunkIter(iters.Slice(trunks), call, WithTrunkConflict(func(t1, t2 *livekit.SIPInboundTrunkInfo, reason TrunkConflictReason) {
t.Logf("conflict: %v\n%v\nvs\n%v", reason, t1, t2)
}))
if c.expErr {
require.Error(t, err)
require.Nil(t, got)
t.Log(err)
} else {
var exp *livekit.SIPInboundTrunkInfo
if c.exp >= 0 {
exp = trunks[c.exp]
}
require.NoError(t, err)
require.Equal(t, exp, got)
}
})
}
}
func TestSIPValidateTrunks(t *testing.T) {
for _, c := range trunkCases {
c := c
t.Run(c.name, func(t *testing.T) {
for i, r := range c.trunks {
if r.SipTrunkId == "" {
r.SipTrunkId = strconv.Itoa(i)
}
}
err := ValidateTrunks(toInboundTrunks(c.trunks))
if c.invalid {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func newSIPTrunkDispatch() *livekit.SIPTrunkInfo {
return &livekit.SIPTrunkInfo{
SipTrunkId: sipTrunkID1,
OutboundNumber: sipNumber2,
}
}
func newSIPReqDispatch(pin string, noPin bool) *rpc.EvaluateSIPDispatchRulesRequest {
return &rpc.EvaluateSIPDispatchRulesRequest{
CallingNumber: sipNumber1,
CalledNumber: sipNumber2,
Pin: pin,
}
}
func newDirectDispatch(room, pin string) *livekit.SIPDispatchRule {
return &livekit.SIPDispatchRule{
Rule: &livekit.SIPDispatchRule_DispatchRuleDirect{
DispatchRuleDirect: &livekit.SIPDispatchRuleDirect{
RoomName: room, Pin: pin,
},
},
}
}
func newIndividualDispatch(roomPref, pin string, randomize bool) *livekit.SIPDispatchRule {
return &livekit.SIPDispatchRule{
Rule: &livekit.SIPDispatchRule_DispatchRuleIndividual{
DispatchRuleIndividual: &livekit.SIPDispatchRuleIndividual{
RoomPrefix: roomPref, Pin: pin, NoRandomness: !randomize,
},
},
}
}
var dispatchCases = []struct {
name string
trunk *livekit.SIPTrunkInfo
rules []*livekit.SIPDispatchRuleInfo
reqPin string
noPin bool
exp int
expErr bool
invalid bool
}{
{
name: "empty",
trunk: nil,
rules: nil,
expErr: true,
},
{
name: "only trunk",
trunk: newSIPTrunkDispatch(),
rules: nil,
expErr: true,
},
{
name: "one rule/no trunk",
trunk: nil,
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip", "")},
},
exp: 0,
},
{
name: "one rule/default trunk",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip", "")},
},
exp: 0,
},
{
name: "one rule/specific trunk",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: []string{sipTrunkID1, sipTrunkID2}, Rule: newDirectDispatch("sip", "")},
},
exp: 0,
},
{
name: "one rule/wrong trunk",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: []string{"zzz"}, Rule: newDirectDispatch("sip", "")},
},
expErr: true,
},
{
name: "direct pin/correct",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip", "123")},
{TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip", "456")},
},
reqPin: "123",
exp: 0,
},
{
name: "direct pin/wrong",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip", "123")},
{TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip", "456")},
},
reqPin: "zzz",
expErr: true,
},
{
name: "direct pin/conflict",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip1", "123")},
{TrunkIds: []string{sipTrunkID1, sipTrunkID2}, Rule: newDirectDispatch("sip2", "123")},
},
reqPin: "123",
expErr: true,
invalid: true,
},
{
name: "direct pin/no conflict on different trunk",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip1", "123")},
{TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip2", "123")},
},
reqPin: "123",
exp: 0,
},
{
name: "direct pin/default and specific",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")},
{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "123")},
},
reqPin: "123",
exp: 1,
},
{
name: "direct/default and specific",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip1", "")},
{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "")},
},
exp: 1,
},
{
name: "direct/default and specific/mixed 1",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")},
{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "")},
},
exp: 1,
},
{
name: "direct/default and specific/mixed 2",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip1", "")},
{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "123")},
},
exp: 1,
},
{
name: "direct/multiple defaults",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip1", "")},
{TrunkIds: nil, Rule: newDirectDispatch("sip2", "")},
},
expErr: true,
invalid: true,
},
{
name: "direct/inbound number specific",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip1", "")},
{TrunkIds: nil, Rule: newDirectDispatch("sip2", ""), InboundNumbers: []string{sipNumber1}},
},
exp: 1,
},
{
name: "direct/inbound number specific pin",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")},
{TrunkIds: nil, Rule: newDirectDispatch("sip2", "123"), InboundNumbers: []string{sipNumber1}},
},
exp: 1,
},
{
name: "direct/inbound number specific conflict",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip1", ""), InboundNumbers: []string{sipNumber1}},
{TrunkIds: nil, Rule: newDirectDispatch("sip2", ""), InboundNumbers: []string{sipNumber1, sipNumber2}},
},
expErr: true,
invalid: true,
},
{
name: "direct/open specific vs pin generic",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")},
{TrunkIds: nil, Rule: newDirectDispatch("sip2", ""), InboundNumbers: []string{sipNumber1}},
},
exp: 1,
},
{
name: "direct vs individual/private",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newIndividualDispatch("pref_", "123", true)},
{TrunkIds: nil, Rule: newDirectDispatch("sip", "123")},
},
expErr: true,
invalid: true,
},
{
name: "direct vs individual/open",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newIndividualDispatch("pref_", "", true)},
{TrunkIds: nil, Rule: newDirectDispatch("sip", "")},
},
expErr: true,
invalid: true,
},
{
name: "direct vs individual/priority",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newIndividualDispatch("pref_", "123", true)},
{TrunkIds: nil, Rule: newDirectDispatch("sip", "456")},
},
reqPin: "456",
exp: 1,
},
{
name: "direct/number specific",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip_1", "")},
{TrunkIds: nil, Rule: newDirectDispatch("sip_2", ""), Numbers: []string{sipNumber2}},
},
exp: 1,
},
{
name: "direct/number specific pin",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip_1", "123")},
{TrunkIds: nil, Rule: newDirectDispatch("sip_2", "123"), Numbers: []string{sipNumber2}},
},
exp: 1,
},
{
name: "direct/number specific conflict",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip_1", ""), Numbers: []string{sipNumber1}},
{TrunkIds: nil, Rule: newDirectDispatch("sip_2", ""), Numbers: []string{sipNumber1, sipNumber2}},
},
expErr: true,
invalid: true,
},
{
name: "direct/number + inbound number specific conflict",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip_1", ""), Numbers: []string{sipNumber1}, InboundNumbers: []string{sipNumber1}},
{TrunkIds: nil, Rule: newDirectDispatch("sip_2", ""), Numbers: []string{sipNumber1, sipNumber2}, InboundNumbers: []string{sipNumber1}},
},
expErr: true,
invalid: true,
},
{
name: "direct/open specific vs pin generic",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip_1", "123")},
{TrunkIds: nil, Rule: newDirectDispatch("sip_2", ""), Numbers: []string{sipNumber2}},
},
exp: 1,
},
{
name: "direct/open specific vs pin generic",
trunk: newSIPTrunkDispatch(),
rules: []*livekit.SIPDispatchRuleInfo{
{TrunkIds: nil, Rule: newDirectDispatch("sip_1", "123")},
{TrunkIds: nil, Rule: newDirectDispatch("sip_2", ""), Numbers: []string{sipNumber2}, InboundNumbers: []string{sipNumber1}},
},
exp: 1,
},
}
func TestSIPMatchDispatchRule(t *testing.T) {
for _, c := range dispatchCases {
c := c
t.Run(c.name, func(t *testing.T) {
pins := []string{c.reqPin}
if !c.expErr && c.reqPin != "" {
pins = append(pins, "")
}
for i, r := range c.rules {
if r.SipDispatchRuleId == "" {
r.SipDispatchRuleId = fmt.Sprintf("rule_%d", i)
}
}
for _, pin := range pins {
pin := pin
name := pin
if name == "" {
name = "no pin"
}
t.Run(name, func(t *testing.T) {
got, err := MatchDispatchRuleIter(c.trunk.AsInbound(), iters.Slice(c.rules), newSIPReqDispatch(pin, c.noPin), WithDispatchRuleConflict(func(r1, r2 *livekit.SIPDispatchRuleInfo, reason DispatchRuleConflictReason) {
t.Logf("conflict: %v\n%v\nvs\n%v", reason, r1, r2)
}))
if c.expErr {
require.Error(t, err)
require.Nil(t, got)
t.Log(err)
} else {
var exp *livekit.SIPDispatchRuleInfo
if c.exp >= 0 {
exp = c.rules[c.exp]
}
require.NoError(t, err)
require.Equal(t, exp, got)
}
})
}
})
}
}
func TestSIPValidateDispatchRules(t *testing.T) {
for _, c := range dispatchCases {
c := c
t.Run(c.name, func(t *testing.T) {
for i, r := range c.rules {
if r.SipDispatchRuleId == "" {
r.SipDispatchRuleId = strconv.Itoa(i)
}
}
_, err := ValidateDispatchRulesIter(iters.Slice(c.rules), WithDispatchRuleConflict(func(r1, r2 *livekit.SIPDispatchRuleInfo, reason DispatchRuleConflictReason) {
t.Logf("conflict: %v\n%v\nvs\n%v", reason, r1, r2)
}))
if c.invalid {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestEvaluateDispatchRule(t *testing.T) {
const projectID = "p_123"
const caller = "+15551234567"
const callee = "+3333"
const prefix = "testPrefix"
var quotedCaller = regexp.QuoteMeta(caller)
req := &rpc.EvaluateSIPDispatchRulesRequest{
SipCallId: "call-id",
CallingNumber: caller,
CallingHost: "sip.example.com",
CalledNumber: callee,
}
tr := &livekit.SIPInboundTrunkInfo{SipTrunkId: "trunk"}
t.Run("Direct", func(t *testing.T) {
d := &livekit.SIPDispatchRuleInfo{
SipDispatchRuleId: "rule",
Rule: newDirectDispatch("room", ""),
HidePhoneNumber: false,
InboundNumbers: nil,
Numbers: nil,
Name: "",
Metadata: "rule-meta",
Attributes: map[string]string{
"rule-attr": "1",
},
}
r := &rpc.EvaluateSIPDispatchRulesRequest{
SipCallId: "call-id",
CallingNumber: "+11112222",
CallingHost: "sip.example.com",
CalledNumber: "+3333",
ExtraAttributes: map[string]string{
"prov-attr": "1",
},
}
tr := &livekit.SIPInboundTrunkInfo{SipTrunkId: "trunk"}
res, err := EvaluateDispatchRule("p_123", tr, d, r)
require.NoError(t, err)
require.Equal(t, &rpc.EvaluateSIPDispatchRulesResponse{
ProjectId: "p_123",
Result: rpc.SIPDispatchResult_ACCEPT,
SipTrunkId: "trunk",
SipDispatchRuleId: "rule",
RoomName: "room",
ParticipantIdentity: "sip_+11112222",
ParticipantName: "Phone +11112222",
ParticipantMetadata: "rule-meta",
ParticipantAttributes: map[string]string{
"rule-attr": "1",
"prov-attr": "1",
livekit.AttrSIPCallID: "call-id",
livekit.AttrSIPTrunkID: "trunk",
livekit.AttrSIPDispatchRuleID: "rule",
livekit.AttrSIPPhoneNumber: "+11112222",
livekit.AttrSIPTrunkNumber: "+3333",
livekit.AttrSIPHostName: "sip.example.com",
},
}, res)
d.HidePhoneNumber = true
res, err = EvaluateDispatchRule("p_123", tr, d, r)
require.NoError(t, err)
require.Equal(t, &rpc.EvaluateSIPDispatchRulesResponse{
ProjectId: "p_123",
Result: rpc.SIPDispatchResult_ACCEPT,
SipTrunkId: "trunk",
SipDispatchRuleId: "rule",
RoomName: "room",
ParticipantIdentity: "sip_c15a31c71649a522",
ParticipantName: "Phone 2222",
ParticipantMetadata: "rule-meta",
ParticipantAttributes: map[string]string{
"rule-attr": "1",
"prov-attr": "1",
livekit.AttrSIPCallID: "call-id",
livekit.AttrSIPTrunkID: "trunk",
livekit.AttrSIPDispatchRuleID: "rule",
},
}, res)
})
t.Run("Individual", func(t *testing.T) {
t.Run("minimal", func(t *testing.T) {
testDR := livekit.SIPDispatchRuleInfo{
SipDispatchRuleId: "rule",
Rule: newIndividualDispatch("", "", false),
}
res, err := EvaluateDispatchRule(projectID, tr, &testDR, req)
require.NoError(t, err)
require.Equal(t, rpc.SIPDispatchResult_ACCEPT, res.Result)
require.Equal(t, caller, res.RoomName, "room name should be from")
})
t.Run("only prefix", func(t *testing.T) {
testDR := livekit.SIPDispatchRuleInfo{
SipDispatchRuleId: "rule",
Rule: newIndividualDispatch(prefix, "", false),
}
res, err := EvaluateDispatchRule(projectID, tr, &testDR, req)
require.NoError(t, err)
require.Equal(t, rpc.SIPDispatchResult_ACCEPT, res.Result)
require.Equal(t, prefix+"_"+caller, res.RoomName)
})
t.Run("only randomize", func(t *testing.T) {
testDR := livekit.SIPDispatchRuleInfo{
SipDispatchRuleId: "rule",
Rule: newIndividualDispatch("", "", true),
}
res, err := EvaluateDispatchRule(projectID, tr, &testDR, req)
require.NoError(t, err)
require.Equal(t, rpc.SIPDispatchResult_ACCEPT, res.Result)
require.Regexp(t, `^`+regexp.QuoteMeta(caller)+`_[a-zA-Z0-9]+$`, res.RoomName, "room name should be from_guid")
})
t.Run("only pin", func(t *testing.T) {
testDR := livekit.SIPDispatchRuleInfo{
SipDispatchRuleId: "rule",
Rule: newIndividualDispatch("", "123", false),
}
res, err := EvaluateDispatchRule(projectID, tr, &testDR, req)
require.NoError(t, err)
require.Equal(t, rpc.SIPDispatchResult_REQUEST_PIN, res.Result)
require.Empty(t, res.RoomName, "implementation does not set RoomName when returning REQUEST_PIN")
})
t.Run("prefix and randomize", func(t *testing.T) {
testDR := livekit.SIPDispatchRuleInfo{
SipDispatchRuleId: "rule",
Rule: newIndividualDispatch(prefix, "", true),
}
res, err := EvaluateDispatchRule(projectID, tr, &testDR, req)
require.NoError(t, err)
require.Equal(t, rpc.SIPDispatchResult_ACCEPT, res.Result)
require.Regexp(t, `^`+prefix+`_`+quotedCaller+`_[a-zA-Z0-9]+$`, res.RoomName, "room name should be prefix_from_guid")
})
t.Run("prefix and pin", func(t *testing.T) {
testDR := livekit.SIPDispatchRuleInfo{
SipDispatchRuleId: "rule",
Rule: newIndividualDispatch(prefix, "123", false),
}
res, err := EvaluateDispatchRule(projectID, tr, &testDR, req)
require.NoError(t, err)
require.Equal(t, rpc.SIPDispatchResult_REQUEST_PIN, res.Result)
require.Empty(t, res.RoomName, "implementation does not set RoomName when returning REQUEST_PIN")
})
t.Run("randomize and pin", func(t *testing.T) {
testDR := livekit.SIPDispatchRuleInfo{
SipDispatchRuleId: "rule",
Rule: newIndividualDispatch("", "123", true),
}
res, err := EvaluateDispatchRule(projectID, tr, &testDR, req)
require.NoError(t, err)
require.Equal(t, rpc.SIPDispatchResult_REQUEST_PIN, res.Result)
require.Empty(t, res.RoomName, "implementation does not set RoomName when returning REQUEST_PIN")
})
t.Run("all options", func(t *testing.T) {
testDR := livekit.SIPDispatchRuleInfo{
SipDispatchRuleId: "rule",
Rule: newIndividualDispatch(prefix, "123", true),
}
res, err := EvaluateDispatchRule(projectID, tr, &testDR, req)
require.NoError(t, err)
require.Equal(t, rpc.SIPDispatchResult_REQUEST_PIN, res.Result)
require.Empty(t, res.RoomName, "implementation does not set RoomName when returning REQUEST_PIN")
})
})
}
func TestMatchIP(t *testing.T) {
cases := []struct {
addr string
mask string
valid bool
exp bool
}{
{addr: "192.168.0.10", mask: "192.168.0.10", valid: true, exp: true},
{addr: "192.168.0.10", mask: "192.168.0.11", valid: true, exp: false},
{addr: "192.168.0.10", mask: "192.168.0.0/24", valid: true, exp: true},
{addr: "192.168.0.10", mask: "192.168.0.10/0", valid: true, exp: true},
{addr: "192.168.0.10", mask: "192.170.0.0/24", valid: true, exp: false},
}
for _, c := range cases {
t.Run(c.mask, func(t *testing.T) {
ip, err := netip.ParseAddr(c.addr)
require.NoError(t, err)
got := isValidMask(c.mask)
require.Equal(t, c.valid, got)
got = matchAddrMask(ip, c.mask)
require.Equal(t, c.exp, got)
})
}
}
func TestMatchMasks(t *testing.T) {
cases := []struct {
name string
addr string
host string
masks []string
exp bool
}{
{
name: "no masks",
addr: "192.168.0.10",
masks: nil,
exp: true,
},
{
name: "single ip",
addr: "192.168.0.10",
masks: []string{
"192.168.0.10",
},
exp: true,
},
{
name: "wrong ip",
addr: "192.168.0.10",
masks: []string{
"192.168.0.11",
},
exp: false,
},
{
name: "ip mask",
addr: "192.168.0.10",
masks: []string{
"192.168.0.0/24",
},
exp: true,
},
{
name: "wrong mask",
addr: "192.168.0.10",
masks: []string{
"192.168.1.0/24",
},
exp: false,
},
{
name: "hostname",
addr: "192.168.0.10",
host: "sip.example.com",
masks: []string{
"sip.example.com",
},
exp: true,
},
{
name: "invalid hostname",
addr: "192.168.0.10",
host: "sip.example.com",
masks: []string{
"some.domain",
},
exp: false,
},
{
name: "invalid and valid range",
addr: "192.168.0.10",
masks: []string{
"some.domain,192.168.0.10/24",
"192.168.0.0/24",
},
exp: true,
},
{
name: "invalid and wrong range",
addr: "192.168.0.10",
masks: []string{
"some.domain",
"192.168.1.0/24",
},
exp: false,
},
{
name: "domain name",
addr: "192.168.0.10",
host: "sip.example.com",
masks: []string{
"some.domain",
"192.168.1.0/24",
"sip.example.com",
},
exp: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := matchAddrMasks(c.addr, c.host, c.masks)
require.Equal(t, c.exp, got)
})
}
}
func TestMatchTrunkDetailed(t *testing.T) {
for _, c := range []struct {
name string
trunks []*livekit.SIPInboundTrunkInfo
expMatchType TrunkMatchType
expTrunkID string
expDefaultCount int
expErr bool
from string
to string
src string
host string
}{
{
name: "empty",
trunks: nil,
expMatchType: TrunkMatchEmpty,
expTrunkID: "",
expErr: false,
},
{
name: "one wildcard",
trunks: []*livekit.SIPInboundTrunkInfo{
{SipTrunkId: "aaa"},
},
expMatchType: TrunkMatchDefault,
expTrunkID: "aaa",
expDefaultCount: 1,
expErr: false,
},
{
name: "specific match",
trunks: []*livekit.SIPInboundTrunkInfo{
{SipTrunkId: "aaa", Numbers: []string{sipNumber2}},
},
expMatchType: TrunkMatchSpecific,
expTrunkID: "aaa",
expDefaultCount: 0,
expErr: false,
},
{
name: "no match with trunks",
trunks: []*livekit.SIPInboundTrunkInfo{
{SipTrunkId: "aaa", Numbers: []string{sipNumber3}},
},
expMatchType: TrunkMatchNone,
expTrunkID: "",
expDefaultCount: 0,
expErr: false,
},
{
name: "multiple defaults",
trunks: []*livekit.SIPInboundTrunkInfo{
{SipTrunkId: "aaa"},
{SipTrunkId: "bbb"},
},
expMatchType: TrunkMatchDefault,
expTrunkID: "aaa",
expDefaultCount: 2,
expErr: true,
},
{
name: "specific over default",
trunks: []*livekit.SIPInboundTrunkInfo{
{SipTrunkId: "aaa"},
{SipTrunkId: "bbb", Numbers: []string{sipNumber2}},
},
expMatchType: TrunkMatchSpecific,
expTrunkID: "bbb",
expDefaultCount: 1,
expErr: false,
},
{
name: "multiple specific",
trunks: []*livekit.SIPInboundTrunkInfo{
{SipTrunkId: "aaa", Numbers: []string{sipNumber2}},
{SipTrunkId: "bbb", Numbers: []string{sipNumber2}},
},
expMatchType: TrunkMatchSpecific,
expTrunkID: "aaa",
expDefaultCount: 0,
expErr: true,
},
} {
c := c
t.Run(c.name, func(t *testing.T) {
from, to, src, host := c.from, c.to, c.src, c.host
if from == "" {
from = sipNumber1
}
if to == "" {
to = sipNumber2
}
if src == "" {
src = "1.1.1.1"
}
if host == "" {
host = "sip.example.com"
}
call := &rpc.SIPCall{
SipCallId: "test-call-id",
SourceIp: src,
From: &livekit.SIPUri{
User: from,
Host: host,
},
To: &livekit.SIPUri{
User: to,
},
}
call.Address = call.To
var conflicts []string
result, err := MatchTrunkDetailed(iters.Slice(c.trunks), call, WithTrunkConflict(func(t1, t2 *livekit.SIPInboundTrunkInfo, reason TrunkConflictReason) {
conflicts = append(conflicts, fmt.Sprintf("%v: %v vs %v", reason, t1.SipTrunkId, t2.SipTrunkId))
}))
if c.expErr {
require.Error(t, err)
require.NotEmpty(t, conflicts, "expected conflicts but got none")
} else {
require.NoError(t, err)
require.Empty(t, conflicts, "unexpected conflicts: %v", conflicts)
if c.expTrunkID == "" {
require.Nil(t, result.Trunk)
} else {
require.NotNil(t, result.Trunk)
require.Equal(t, c.expTrunkID, result.Trunk.SipTrunkId)
}
require.Equal(t, c.expMatchType, result.MatchType)
require.Equal(t, c.expDefaultCount, result.DefaultTrunkCount)
}
})
}
}