github.com/livekit/protocol@v1.39.3/sip/sip_test.go (about) 1 // Copyright 2023 LiveKit, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package sip 16 17 import ( 18 "fmt" 19 "net/netip" 20 "strconv" 21 "testing" 22 23 "github.com/dennwc/iters" 24 25 "github.com/stretchr/testify/require" 26 27 "github.com/livekit/protocol/livekit" 28 "github.com/livekit/protocol/rpc" 29 ) 30 31 func TestNormalizeNumber(t *testing.T) { 32 cases := []struct { 33 name string 34 num string 35 exp string 36 }{ 37 {"empty", "", ""}, 38 {"number", "123", "+123"}, 39 {"plus", "+123", "+123"}, 40 {"user", "user", "user"}, 41 {"human", "(123) 456 7890", "+1234567890"}, 42 } 43 for _, c := range cases { 44 t.Run(c.name, func(t *testing.T) { 45 require.Equal(t, c.exp, NormalizeNumber(c.num)) 46 }) 47 } 48 } 49 50 const ( 51 sipNumber1 = "1111 1111" 52 sipNumber2 = "2222 2222" 53 sipNumber3 = "3333 3333" 54 sipTrunkID1 = "aaa" 55 sipTrunkID2 = "bbb" 56 ) 57 58 var trunkCases = []struct { 59 name string 60 trunks []*livekit.SIPTrunkInfo 61 exp int 62 expErr bool 63 invalid bool 64 from string 65 to string 66 src string 67 host string 68 }{ 69 { 70 name: "empty", 71 trunks: nil, 72 exp: -1, // no error; nil result 73 }, 74 { 75 name: "one wildcard", 76 trunks: []*livekit.SIPTrunkInfo{ 77 {SipTrunkId: "aaa"}, 78 }, 79 exp: 0, 80 }, 81 { 82 name: "matching", 83 trunks: []*livekit.SIPTrunkInfo{ 84 {SipTrunkId: "aaa", OutboundNumber: sipNumber2}, 85 }, 86 exp: 0, 87 }, 88 { 89 name: "matching inbound", 90 trunks: []*livekit.SIPTrunkInfo{ 91 {SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbers: []string{sipNumber1}}, 92 }, 93 exp: 0, 94 }, 95 { 96 name: "matching regexp", 97 trunks: []*livekit.SIPTrunkInfo{ 98 {SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbersRegex: []string{`^\d+ \d+$`}}, 99 }, 100 exp: 0, 101 }, 102 { 103 name: "not matching", 104 trunks: []*livekit.SIPTrunkInfo{ 105 {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, 106 }, 107 exp: -1, 108 }, 109 { 110 name: "not matching inbound", 111 trunks: []*livekit.SIPTrunkInfo{ 112 {SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbers: []string{sipNumber1 + "1"}}, 113 }, 114 exp: -1, 115 }, 116 { 117 name: "one match", 118 trunks: []*livekit.SIPTrunkInfo{ 119 {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, 120 {SipTrunkId: "bbb", OutboundNumber: sipNumber2}, 121 }, 122 exp: 1, 123 }, 124 { 125 name: "many matches", 126 trunks: []*livekit.SIPTrunkInfo{ 127 {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, 128 {SipTrunkId: "bbb", OutboundNumber: sipNumber2}, 129 {SipTrunkId: "ccc", OutboundNumber: sipNumber2}, 130 }, 131 expErr: true, 132 invalid: true, 133 }, 134 { 135 name: "many matches default", 136 trunks: []*livekit.SIPTrunkInfo{ 137 {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, 138 {SipTrunkId: "bbb"}, 139 {SipTrunkId: "ccc", OutboundNumber: sipNumber2}, 140 {SipTrunkId: "ddd"}, 141 }, 142 exp: 2, 143 invalid: true, // it can successfully select "ccc", but the overall configuration is invalid 144 }, 145 { 146 name: "inbound", 147 trunks: []*livekit.SIPTrunkInfo{ 148 {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, 149 {SipTrunkId: "bbb", OutboundNumber: sipNumber2}, 150 {SipTrunkId: "ccc", OutboundNumber: sipNumber2, InboundNumbers: []string{sipNumber1 + "1"}}, 151 }, 152 exp: 1, 153 }, 154 { 155 name: "multiple defaults", 156 trunks: []*livekit.SIPTrunkInfo{ 157 {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, 158 {SipTrunkId: "bbb"}, 159 {SipTrunkId: "ccc"}, 160 }, 161 expErr: true, 162 invalid: true, 163 }, 164 { 165 name: "inbound with ip exact", 166 trunks: []*livekit.SIPTrunkInfo{ 167 {SipTrunkId: "bbb", OutboundNumber: sipNumber2, InboundAddresses: []string{ 168 "10.10.10.10", 169 "1.1.1.1", 170 }}, 171 }, 172 exp: 0, 173 }, 174 { 175 name: "inbound with ip exact miss", 176 trunks: []*livekit.SIPTrunkInfo{ 177 {SipTrunkId: "bbb", OutboundNumber: sipNumber2, InboundAddresses: []string{ 178 "10.10.10.10", 179 }}, 180 }, 181 exp: -1, 182 }, 183 { 184 name: "inbound with ip mask", 185 trunks: []*livekit.SIPTrunkInfo{ 186 {SipTrunkId: "bbb", OutboundNumber: sipNumber2, InboundAddresses: []string{ 187 "10.10.10.0/24", 188 "1.1.1.0/24", 189 }}, 190 }, 191 exp: 0, 192 }, 193 { 194 name: "inbound with ip mask miss", 195 trunks: []*livekit.SIPTrunkInfo{ 196 {SipTrunkId: "bbb", OutboundNumber: sipNumber2, InboundAddresses: []string{ 197 "10.10.10.0/24", 198 }}, 199 }, 200 exp: -1, 201 }, 202 { 203 name: "inbound with host mask", 204 trunks: []*livekit.SIPTrunkInfo{ 205 {SipTrunkId: "bbb", OutboundNumber: sipNumber2, InboundAddresses: []string{ 206 "10.10.10.0/24", 207 "sip.example.com", 208 }}, 209 }, 210 exp: 0, 211 }, 212 { 213 name: "inbound with plus", 214 trunks: []*livekit.SIPTrunkInfo{ 215 {SipTrunkId: "aaa", OutboundNumber: "+" + sipNumber3}, 216 {SipTrunkId: "bbb", OutboundNumber: "+" + sipNumber2}, 217 }, 218 exp: 1, 219 }, 220 { 221 name: "inbound without plus", 222 trunks: []*livekit.SIPTrunkInfo{ 223 {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, 224 {SipTrunkId: "bbb", OutboundNumber: sipNumber2}, 225 }, 226 from: "+" + sipNumber1, 227 to: "+" + sipNumber2, 228 exp: 1, 229 }, 230 } 231 232 func toInboundTrunks(trunks []*livekit.SIPTrunkInfo) []*livekit.SIPInboundTrunkInfo { 233 out := make([]*livekit.SIPInboundTrunkInfo, 0, len(trunks)) 234 for _, t := range trunks { 235 out = append(out, t.AsInbound()) 236 } 237 return out 238 } 239 240 func TestSIPMatchTrunk(t *testing.T) { 241 for _, c := range trunkCases { 242 c := c 243 t.Run(c.name, func(t *testing.T) { 244 from, to, src, host := c.from, c.to, c.src, c.host 245 if from == "" { 246 from = sipNumber1 247 } 248 if to == "" { 249 to = sipNumber2 250 } 251 if src == "" { 252 src = "1.1.1.1" 253 } 254 if host == "" { 255 host = "sip.example.com" 256 } 257 trunks := toInboundTrunks(c.trunks) 258 call := &rpc.SIPCall{ 259 SourceIp: src, 260 From: &livekit.SIPUri{ 261 User: from, 262 Host: host, 263 }, 264 To: &livekit.SIPUri{ 265 User: to, 266 }, 267 } 268 call.Address = call.To 269 got, err := MatchTrunkIter(iters.Slice(trunks), call, WithTrunkConflict(func(t1, t2 *livekit.SIPInboundTrunkInfo, reason TrunkConflictReason) { 270 t.Logf("conflict: %v\n%v\nvs\n%v", reason, t1, t2) 271 })) 272 if c.expErr { 273 require.Error(t, err) 274 require.Nil(t, got) 275 t.Log(err) 276 } else { 277 var exp *livekit.SIPInboundTrunkInfo 278 if c.exp >= 0 { 279 exp = trunks[c.exp] 280 } 281 require.NoError(t, err) 282 require.Equal(t, exp, got) 283 } 284 }) 285 } 286 } 287 288 func TestSIPValidateTrunks(t *testing.T) { 289 for _, c := range trunkCases { 290 c := c 291 t.Run(c.name, func(t *testing.T) { 292 for i, r := range c.trunks { 293 if r.SipTrunkId == "" { 294 r.SipTrunkId = strconv.Itoa(i) 295 } 296 } 297 err := ValidateTrunks(toInboundTrunks(c.trunks)) 298 if c.invalid { 299 require.Error(t, err) 300 } else { 301 require.NoError(t, err) 302 } 303 }) 304 } 305 } 306 307 func newSIPTrunkDispatch() *livekit.SIPTrunkInfo { 308 return &livekit.SIPTrunkInfo{ 309 SipTrunkId: sipTrunkID1, 310 OutboundNumber: sipNumber2, 311 } 312 } 313 314 func newSIPReqDispatch(pin string, noPin bool) *rpc.EvaluateSIPDispatchRulesRequest { 315 return &rpc.EvaluateSIPDispatchRulesRequest{ 316 CallingNumber: sipNumber1, 317 CalledNumber: sipNumber2, 318 Pin: pin, 319 //NoPin: noPin, // TODO 320 } 321 } 322 323 func newDirectDispatch(room, pin string) *livekit.SIPDispatchRule { 324 return &livekit.SIPDispatchRule{ 325 Rule: &livekit.SIPDispatchRule_DispatchRuleDirect{ 326 DispatchRuleDirect: &livekit.SIPDispatchRuleDirect{ 327 RoomName: room, Pin: pin, 328 }, 329 }, 330 } 331 } 332 333 func newIndividualDispatch(roomPref, pin string) *livekit.SIPDispatchRule { 334 return &livekit.SIPDispatchRule{ 335 Rule: &livekit.SIPDispatchRule_DispatchRuleIndividual{ 336 DispatchRuleIndividual: &livekit.SIPDispatchRuleIndividual{ 337 RoomPrefix: roomPref, Pin: pin, 338 }, 339 }, 340 } 341 } 342 343 var dispatchCases = []struct { 344 name string 345 trunk *livekit.SIPTrunkInfo 346 rules []*livekit.SIPDispatchRuleInfo 347 reqPin string 348 noPin bool 349 exp int 350 expErr bool 351 invalid bool 352 }{ 353 // These cases just validate that no rules produce an error. 354 { 355 name: "empty", 356 trunk: nil, 357 rules: nil, 358 expErr: true, 359 }, 360 { 361 name: "only trunk", 362 trunk: newSIPTrunkDispatch(), 363 rules: nil, 364 expErr: true, 365 }, 366 // Default rules should work even if no trunk is defined. 367 { 368 name: "one rule/no trunk", 369 trunk: nil, 370 rules: []*livekit.SIPDispatchRuleInfo{ 371 {TrunkIds: nil, Rule: newDirectDispatch("sip", "")}, 372 }, 373 exp: 0, 374 }, 375 // Default rule should work with a trunk too. 376 { 377 name: "one rule/default trunk", 378 trunk: newSIPTrunkDispatch(), 379 rules: []*livekit.SIPDispatchRuleInfo{ 380 {TrunkIds: nil, Rule: newDirectDispatch("sip", "")}, 381 }, 382 exp: 0, 383 }, 384 // Rule matching the trunk should be selected. 385 { 386 name: "one rule/specific trunk", 387 trunk: newSIPTrunkDispatch(), 388 rules: []*livekit.SIPDispatchRuleInfo{ 389 {TrunkIds: []string{sipTrunkID1, sipTrunkID2}, Rule: newDirectDispatch("sip", "")}, 390 }, 391 exp: 0, 392 }, 393 // Rule NOT matching the trunk should NOT be selected. 394 { 395 name: "one rule/wrong trunk", 396 trunk: newSIPTrunkDispatch(), 397 rules: []*livekit.SIPDispatchRuleInfo{ 398 {TrunkIds: []string{"zzz"}, Rule: newDirectDispatch("sip", "")}, 399 }, 400 expErr: true, 401 }, 402 // Direct rule with a pin should be selected, even if no pin is provided. 403 { 404 name: "direct pin/correct", 405 trunk: newSIPTrunkDispatch(), 406 rules: []*livekit.SIPDispatchRuleInfo{ 407 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip", "123")}, 408 {TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip", "456")}, 409 }, 410 reqPin: "123", 411 exp: 0, 412 }, 413 // Direct rule with a pin should reject wrong pin. 414 { 415 name: "direct pin/wrong", 416 trunk: newSIPTrunkDispatch(), 417 rules: []*livekit.SIPDispatchRuleInfo{ 418 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip", "123")}, 419 {TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip", "456")}, 420 }, 421 reqPin: "zzz", 422 expErr: true, 423 }, 424 // Multiple direct rules with the same pin should result in an error. 425 { 426 name: "direct pin/conflict", 427 trunk: newSIPTrunkDispatch(), 428 rules: []*livekit.SIPDispatchRuleInfo{ 429 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip1", "123")}, 430 {TrunkIds: []string{sipTrunkID1, sipTrunkID2}, Rule: newDirectDispatch("sip2", "123")}, 431 }, 432 reqPin: "123", 433 expErr: true, 434 invalid: true, 435 }, 436 // Multiple direct rules with the same pin on different trunks are ok. 437 { 438 name: "direct pin/no conflict on different trunk", 439 trunk: newSIPTrunkDispatch(), 440 rules: []*livekit.SIPDispatchRuleInfo{ 441 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip1", "123")}, 442 {TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip2", "123")}, 443 }, 444 reqPin: "123", 445 exp: 0, 446 }, 447 // Specific direct rules should take priority over default direct rules. 448 { 449 name: "direct pin/default and specific", 450 trunk: newSIPTrunkDispatch(), 451 rules: []*livekit.SIPDispatchRuleInfo{ 452 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")}, 453 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "123")}, 454 }, 455 reqPin: "123", 456 exp: 1, 457 }, 458 // Specific direct rules should take priority over default direct rules. No pin. 459 { 460 name: "direct/default and specific", 461 trunk: newSIPTrunkDispatch(), 462 rules: []*livekit.SIPDispatchRuleInfo{ 463 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "")}, 464 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "")}, 465 }, 466 exp: 1, 467 }, 468 // Specific direct rules should take priority over default direct rules. One with pin, other without. 469 { 470 name: "direct/default and specific/mixed 1", 471 trunk: newSIPTrunkDispatch(), 472 rules: []*livekit.SIPDispatchRuleInfo{ 473 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")}, 474 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "")}, 475 }, 476 exp: 1, 477 }, 478 { 479 name: "direct/default and specific/mixed 2", 480 trunk: newSIPTrunkDispatch(), 481 rules: []*livekit.SIPDispatchRuleInfo{ 482 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "")}, 483 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "123")}, 484 }, 485 exp: 1, 486 }, 487 // Multiple default direct rules are not allowed. 488 { 489 name: "direct/multiple defaults", 490 trunk: newSIPTrunkDispatch(), 491 rules: []*livekit.SIPDispatchRuleInfo{ 492 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "")}, 493 {TrunkIds: nil, Rule: newDirectDispatch("sip2", "")}, 494 }, 495 expErr: true, 496 invalid: true, 497 }, 498 // Rules for specific numbers take priority. 499 { 500 name: "direct/number specific", 501 trunk: newSIPTrunkDispatch(), 502 rules: []*livekit.SIPDispatchRuleInfo{ 503 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "")}, 504 {TrunkIds: nil, Rule: newDirectDispatch("sip2", ""), InboundNumbers: []string{sipNumber1}}, 505 }, 506 exp: 1, 507 }, 508 { 509 name: "direct/number specific pin", 510 trunk: newSIPTrunkDispatch(), 511 rules: []*livekit.SIPDispatchRuleInfo{ 512 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")}, 513 {TrunkIds: nil, Rule: newDirectDispatch("sip2", "123"), InboundNumbers: []string{sipNumber1}}, 514 }, 515 exp: 1, 516 }, 517 { 518 name: "direct/number specific conflict", 519 trunk: newSIPTrunkDispatch(), 520 rules: []*livekit.SIPDispatchRuleInfo{ 521 {TrunkIds: nil, Rule: newDirectDispatch("sip1", ""), InboundNumbers: []string{sipNumber1}}, 522 {TrunkIds: nil, Rule: newDirectDispatch("sip2", ""), InboundNumbers: []string{sipNumber1, sipNumber2}}, 523 }, 524 expErr: true, 525 invalid: true, 526 }, 527 // Check the "personal room" use case. Rule that accepts a number without a pin and requires pin for everyone else. 528 { 529 name: "direct/open specific vs pin generic", 530 trunk: newSIPTrunkDispatch(), 531 rules: []*livekit.SIPDispatchRuleInfo{ 532 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")}, 533 {TrunkIds: nil, Rule: newDirectDispatch("sip2", ""), InboundNumbers: []string{sipNumber1}}, 534 }, 535 exp: 1, 536 }, 537 // Cannot use both direct and individual rules with the same pin setup. 538 { 539 name: "direct vs individual/private", 540 trunk: newSIPTrunkDispatch(), 541 rules: []*livekit.SIPDispatchRuleInfo{ 542 {TrunkIds: nil, Rule: newIndividualDispatch("pref_", "123")}, 543 {TrunkIds: nil, Rule: newDirectDispatch("sip", "123")}, 544 }, 545 expErr: true, 546 invalid: true, 547 }, 548 { 549 name: "direct vs individual/open", 550 trunk: newSIPTrunkDispatch(), 551 rules: []*livekit.SIPDispatchRuleInfo{ 552 {TrunkIds: nil, Rule: newIndividualDispatch("pref_", "")}, 553 {TrunkIds: nil, Rule: newDirectDispatch("sip", "")}, 554 }, 555 expErr: true, 556 invalid: true, 557 }, 558 // Direct rules take priority over individual rules. 559 { 560 name: "direct vs individual/priority", 561 trunk: newSIPTrunkDispatch(), 562 rules: []*livekit.SIPDispatchRuleInfo{ 563 {TrunkIds: nil, Rule: newIndividualDispatch("pref_", "123")}, 564 {TrunkIds: nil, Rule: newDirectDispatch("sip", "456")}, 565 }, 566 reqPin: "456", 567 exp: 1, 568 }, 569 } 570 571 func TestSIPMatchDispatchRule(t *testing.T) { 572 for _, c := range dispatchCases { 573 c := c 574 t.Run(c.name, func(t *testing.T) { 575 pins := []string{c.reqPin} 576 if !c.expErr && c.reqPin != "" { 577 // Should match the same rule, even if no pin is set (so that it can be requested). 578 pins = append(pins, "") 579 } 580 for i, r := range c.rules { 581 if r.SipDispatchRuleId == "" { 582 r.SipDispatchRuleId = fmt.Sprintf("rule_%d", i) 583 } 584 } 585 for _, pin := range pins { 586 pin := pin 587 name := pin 588 if name == "" { 589 name = "no pin" 590 } 591 t.Run(name, func(t *testing.T) { 592 got, err := MatchDispatchRuleIter(c.trunk.AsInbound(), iters.Slice(c.rules), newSIPReqDispatch(pin, c.noPin), WithDispatchRuleConflict(func(r1, r2 *livekit.SIPDispatchRuleInfo, reason DispatchRuleConflictReason) { 593 t.Logf("conflict: %v\n%v\nvs\n%v", reason, r1, r2) 594 })) 595 if c.expErr { 596 require.Error(t, err) 597 require.Nil(t, got) 598 t.Log(err) 599 } else { 600 var exp *livekit.SIPDispatchRuleInfo 601 if c.exp >= 0 { 602 exp = c.rules[c.exp] 603 } 604 require.NoError(t, err) 605 require.Equal(t, exp, got) 606 } 607 }) 608 } 609 }) 610 } 611 } 612 613 func TestSIPValidateDispatchRules(t *testing.T) { 614 for _, c := range dispatchCases { 615 c := c 616 t.Run(c.name, func(t *testing.T) { 617 for i, r := range c.rules { 618 if r.SipDispatchRuleId == "" { 619 r.SipDispatchRuleId = strconv.Itoa(i) 620 } 621 } 622 _, err := ValidateDispatchRulesIter(iters.Slice(c.rules), WithDispatchRuleConflict(func(r1, r2 *livekit.SIPDispatchRuleInfo, reason DispatchRuleConflictReason) { 623 t.Logf("conflict: %v\n%v\nvs\n%v", reason, r1, r2) 624 })) 625 if c.invalid { 626 require.Error(t, err) 627 } else { 628 require.NoError(t, err) 629 } 630 }) 631 } 632 } 633 634 func TestEvaluateDispatchRule(t *testing.T) { 635 d := &livekit.SIPDispatchRuleInfo{ 636 SipDispatchRuleId: "rule", 637 Rule: newDirectDispatch("room", ""), 638 HidePhoneNumber: false, 639 InboundNumbers: nil, 640 Name: "", 641 Metadata: "rule-meta", 642 Attributes: map[string]string{ 643 "rule-attr": "1", 644 }, 645 } 646 r := &rpc.EvaluateSIPDispatchRulesRequest{ 647 SipCallId: "call-id", 648 CallingNumber: "+11112222", 649 CallingHost: "sip.example.com", 650 CalledNumber: "+3333", 651 ExtraAttributes: map[string]string{ 652 "prov-attr": "1", 653 }, 654 } 655 tr := &livekit.SIPInboundTrunkInfo{SipTrunkId: "trunk"} 656 res, err := EvaluateDispatchRule("p_123", tr, d, r) 657 require.NoError(t, err) 658 require.Equal(t, &rpc.EvaluateSIPDispatchRulesResponse{ 659 ProjectId: "p_123", 660 Result: rpc.SIPDispatchResult_ACCEPT, 661 SipTrunkId: "trunk", 662 SipDispatchRuleId: "rule", 663 RoomName: "room", 664 ParticipantIdentity: "sip_+11112222", 665 ParticipantName: "Phone +11112222", 666 ParticipantMetadata: "rule-meta", 667 ParticipantAttributes: map[string]string{ 668 "rule-attr": "1", 669 "prov-attr": "1", 670 livekit.AttrSIPCallID: "call-id", 671 livekit.AttrSIPTrunkID: "trunk", 672 livekit.AttrSIPDispatchRuleID: "rule", 673 livekit.AttrSIPPhoneNumber: "+11112222", 674 livekit.AttrSIPTrunkNumber: "+3333", 675 livekit.AttrSIPHostName: "sip.example.com", 676 }, 677 }, res) 678 679 d.HidePhoneNumber = true 680 res, err = EvaluateDispatchRule("p_123", tr, d, r) 681 require.NoError(t, err) 682 require.Equal(t, &rpc.EvaluateSIPDispatchRulesResponse{ 683 ProjectId: "p_123", 684 Result: rpc.SIPDispatchResult_ACCEPT, 685 SipTrunkId: "trunk", 686 SipDispatchRuleId: "rule", 687 RoomName: "room", 688 ParticipantIdentity: "sip_c15a31c71649a522", 689 ParticipantName: "Phone 2222", 690 ParticipantMetadata: "rule-meta", 691 ParticipantAttributes: map[string]string{ 692 "rule-attr": "1", 693 "prov-attr": "1", 694 livekit.AttrSIPCallID: "call-id", 695 livekit.AttrSIPTrunkID: "trunk", 696 livekit.AttrSIPDispatchRuleID: "rule", 697 }, 698 }, res) 699 } 700 701 func TestMatchIP(t *testing.T) { 702 cases := []struct { 703 addr string 704 mask string 705 valid bool 706 exp bool 707 }{ 708 {addr: "192.168.0.10", mask: "192.168.0.10", valid: true, exp: true}, 709 {addr: "192.168.0.10", mask: "192.168.0.11", valid: true, exp: false}, 710 {addr: "192.168.0.10", mask: "192.168.0.0/24", valid: true, exp: true}, 711 {addr: "192.168.0.10", mask: "192.168.0.10/0", valid: true, exp: true}, 712 {addr: "192.168.0.10", mask: "192.170.0.0/24", valid: true, exp: false}, 713 } 714 for _, c := range cases { 715 t.Run(c.mask, func(t *testing.T) { 716 ip, err := netip.ParseAddr(c.addr) 717 require.NoError(t, err) 718 got := isValidMask(c.mask) 719 require.Equal(t, c.valid, got) 720 got = matchAddrMask(ip, c.mask) 721 require.Equal(t, c.exp, got) 722 }) 723 } 724 } 725 726 func TestMatchMasks(t *testing.T) { 727 cases := []struct { 728 name string 729 addr string 730 host string 731 masks []string 732 exp bool 733 }{ 734 { 735 name: "no masks", 736 addr: "192.168.0.10", 737 masks: nil, 738 exp: true, 739 }, 740 { 741 name: "single ip", 742 addr: "192.168.0.10", 743 masks: []string{ 744 "192.168.0.10", 745 }, 746 exp: true, 747 }, 748 { 749 name: "wrong ip", 750 addr: "192.168.0.10", 751 masks: []string{ 752 "192.168.0.11", 753 }, 754 exp: false, 755 }, 756 { 757 name: "ip mask", 758 addr: "192.168.0.10", 759 masks: []string{ 760 "192.168.0.0/24", 761 }, 762 exp: true, 763 }, 764 { 765 name: "wrong mask", 766 addr: "192.168.0.10", 767 masks: []string{ 768 "192.168.1.0/24", 769 }, 770 exp: false, 771 }, 772 { 773 name: "hostname", 774 addr: "192.168.0.10", 775 host: "sip.example.com", 776 masks: []string{ 777 "sip.example.com", 778 }, 779 exp: true, 780 }, 781 { 782 name: "invalid hostname", 783 addr: "192.168.0.10", 784 host: "sip.example.com", 785 masks: []string{ 786 "some.domain", 787 }, 788 exp: false, 789 }, 790 { 791 name: "invalid and valid range", 792 addr: "192.168.0.10", 793 masks: []string{ 794 "some.domain,192.168.0.10/24", 795 "192.168.0.0/24", 796 }, 797 exp: true, 798 }, 799 { 800 name: "invalid and wrong range", 801 addr: "192.168.0.10", 802 masks: []string{ 803 "some.domain", 804 "192.168.1.0/24", 805 }, 806 exp: false, 807 }, 808 { 809 name: "domain name", 810 addr: "192.168.0.10", 811 host: "sip.example.com", 812 masks: []string{ 813 "some.domain", 814 "192.168.1.0/24", 815 "sip.example.com", 816 }, 817 exp: true, 818 }, 819 } 820 for _, c := range cases { 821 t.Run(c.name, func(t *testing.T) { 822 got := matchAddrMasks(c.addr, c.host, c.masks) 823 require.Equal(t, c.exp, got) 824 }) 825 } 826 } 827 828 func TestMatchTrunkDetailed(t *testing.T) { 829 for _, c := range []struct { 830 name string 831 trunks []*livekit.SIPInboundTrunkInfo 832 expMatchType TrunkMatchType 833 expTrunkID string 834 expDefaultCount int 835 expErr bool 836 from string 837 to string 838 src string 839 host string 840 }{ 841 { 842 name: "empty", 843 trunks: nil, 844 expMatchType: TrunkMatchEmpty, 845 expTrunkID: "", 846 expErr: false, 847 }, 848 { 849 name: "one wildcard", 850 trunks: []*livekit.SIPInboundTrunkInfo{ 851 {SipTrunkId: "aaa"}, 852 }, 853 expMatchType: TrunkMatchDefault, 854 expTrunkID: "aaa", 855 expDefaultCount: 1, 856 expErr: false, 857 }, 858 { 859 name: "specific match", 860 trunks: []*livekit.SIPInboundTrunkInfo{ 861 {SipTrunkId: "aaa", Numbers: []string{sipNumber2}}, 862 }, 863 expMatchType: TrunkMatchSpecific, 864 expTrunkID: "aaa", 865 expDefaultCount: 0, 866 expErr: false, 867 }, 868 { 869 name: "no match with trunks", 870 trunks: []*livekit.SIPInboundTrunkInfo{ 871 {SipTrunkId: "aaa", Numbers: []string{sipNumber3}}, 872 }, 873 expMatchType: TrunkMatchNone, 874 expTrunkID: "", 875 expDefaultCount: 0, 876 expErr: false, 877 }, 878 { 879 name: "multiple defaults", 880 trunks: []*livekit.SIPInboundTrunkInfo{ 881 {SipTrunkId: "aaa"}, 882 {SipTrunkId: "bbb"}, 883 }, 884 expMatchType: TrunkMatchDefault, 885 expTrunkID: "aaa", 886 expDefaultCount: 2, 887 expErr: true, 888 }, 889 { 890 name: "specific over default", 891 trunks: []*livekit.SIPInboundTrunkInfo{ 892 {SipTrunkId: "aaa"}, 893 {SipTrunkId: "bbb", Numbers: []string{sipNumber2}}, 894 }, 895 expMatchType: TrunkMatchSpecific, 896 expTrunkID: "bbb", 897 expDefaultCount: 1, 898 expErr: false, 899 }, 900 { 901 name: "multiple specific", 902 trunks: []*livekit.SIPInboundTrunkInfo{ 903 {SipTrunkId: "aaa", Numbers: []string{sipNumber2}}, 904 {SipTrunkId: "bbb", Numbers: []string{sipNumber2}}, 905 }, 906 expMatchType: TrunkMatchSpecific, 907 expTrunkID: "aaa", 908 expDefaultCount: 0, 909 expErr: true, 910 }, 911 } { 912 c := c 913 t.Run(c.name, func(t *testing.T) { 914 from, to, src, host := c.from, c.to, c.src, c.host 915 if from == "" { 916 from = sipNumber1 917 } 918 if to == "" { 919 to = sipNumber2 920 } 921 if src == "" { 922 src = "1.1.1.1" 923 } 924 if host == "" { 925 host = "sip.example.com" 926 } 927 call := &rpc.SIPCall{ 928 SourceIp: src, 929 From: &livekit.SIPUri{ 930 User: from, 931 Host: host, 932 }, 933 To: &livekit.SIPUri{ 934 User: to, 935 }, 936 } 937 call.Address = call.To 938 939 var conflicts []string 940 result, err := MatchTrunkDetailed(iters.Slice(c.trunks), call, WithTrunkConflict(func(t1, t2 *livekit.SIPInboundTrunkInfo, reason TrunkConflictReason) { 941 conflicts = append(conflicts, fmt.Sprintf("%v: %v vs %v", reason, t1.SipTrunkId, t2.SipTrunkId)) 942 })) 943 944 if c.expErr { 945 require.Error(t, err) 946 require.NotEmpty(t, conflicts, "expected conflicts but got none") 947 } else { 948 require.NoError(t, err) 949 require.Empty(t, conflicts, "unexpected conflicts: %v", conflicts) 950 951 if c.expTrunkID == "" { 952 require.Nil(t, result.Trunk) 953 } else { 954 require.NotNil(t, result.Trunk) 955 require.Equal(t, c.expTrunkID, result.Trunk.SipTrunkId) 956 } 957 958 require.Equal(t, c.expMatchType, result.MatchType) 959 require.Equal(t, c.expDefaultCount, result.DefaultTrunkCount) 960 } 961 }) 962 } 963 }