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  }