diff --git a/pkg/service/ioservice_sip.go b/pkg/service/ioservice_sip.go index 32bc6219b..b1c0cded9 100644 --- a/pkg/service/ioservice_sip.go +++ b/pkg/service/ioservice_sip.go @@ -38,14 +38,26 @@ func (s *IOInfoService) matchSIPTrunk(ctx context.Context, trunkID string, call if trunkID != "" { // This is a best-effort optimization. Fallthrough to listing trunks if it doesn't work. if tr, err := s.ss.LoadSIPInboundTrunk(ctx, trunkID); err == nil { - tr, err = sip.MatchTrunkIter(iters.Slice([]*livekit.SIPInboundTrunkInfo{tr}), call) - if err == nil { - return tr, nil + result, err := sip.MatchTrunkDetailed(iters.Slice([]*livekit.SIPInboundTrunkInfo{tr}), call) + if err == nil && result.MatchType != sip.TrunkMatchNone { + return result.Trunk, nil } } } it := s.SelectSIPInboundTrunk(ctx, call.To.User) - return sip.MatchTrunkIter(it, call) + result, err := sip.MatchTrunkDetailed(it, call) + if err != nil { + return nil, err + } + // If trunks exist but none matched, return a specific error + if result.MatchType == sip.TrunkMatchNone { + return nil, ErrSIPTrunkNotFound + } + // If no trunks were defined at all, return nil (this is different from TrunkMatchNone) + if result.MatchType == sip.TrunkMatchEmpty { + return nil, nil + } + return result.Trunk, nil } func (s *IOInfoService) SelectSIPInboundTrunk(ctx context.Context, called string) iters.Iter[*livekit.SIPInboundTrunkInfo] { @@ -143,12 +155,18 @@ func (s *IOInfoService) GetSIPTrunkAuthentication(ctx context.Context, req *rpc. } trunk, err := s.matchSIPTrunk(ctx, "", call) if err != nil { + // Check if this is the specific "no SIP trunk matched" error + if err == ErrSIPTrunkNotFound { + err := twirp.NotFoundError(fmt.Sprintf("sip trunk not found for destination %q", req.Call.To)) + log.Errorw("No SIP trunk matched for auth", err, "sipTrunk", "", "to", req.Call.To) + return nil, err + } return nil, err } if trunk == nil { - err := twirp.NotFoundError(fmt.Sprintf("sip trunk not found for destination %q", req.Call.To)) - log.Errorw("No SIP trunk matched for auth", err, "sipTrunk", "", "to", req.Call.To) - return nil, err + // This case is for TrunkMatchEmpty (no trunks defined at all) + // We don't return an error in this case + return nil, nil } log.Debugw("SIP trunk matched for auth", "sipTrunk", trunk.SipTrunkId) return &rpc.GetSIPTrunkAuthenticationResponse{ diff --git a/pkg/service/ioservice_sip_test.go b/pkg/service/ioservice_sip_test.go index 4aca9c5be..450839f0f 100644 --- a/pkg/service/ioservice_sip_test.go +++ b/pkg/service/ioservice_sip_test.go @@ -141,6 +141,13 @@ func TestGetSIPTrunkAuthentication(t *testing.T) { AuthUsername: "user2", AuthPassword: "pass2", }, + { + SipTrunkId: "restricted-trunk", + Numbers: []string{"9999"}, + AllowedAddresses: []string{"10.0.0.0/8"}, // Only allow calls from 10.x.x.x + AuthUsername: "restricted-user", + AuthPassword: "restricted-pass", + }, } for _, tr := range trunks { @@ -182,11 +189,11 @@ func TestGetSIPTrunkAuthentication(t *testing.T) { wantErr: false, }, { - name: "no matching trunk", + name: "trunk exists but source IP not allowed (TrunkMatchNone)", call: &rpc.SIPCall{ To: &livekit.SIPUri{User: "9999"}, From: &livekit.SIPUri{User: "8888"}, - SourceIp: "192.168.1.1", + SourceIp: "192.168.1.1", // Not in 10.0.0.0/8 range }, wantErr: true, wantErrMsg: `sip trunk not found for destination "user:\"9999\""`, @@ -225,3 +232,50 @@ func TestGetSIPTrunkAuthentication(t *testing.T) { }) } } + +func TestGetSIPTrunkAuthenticationEmpty(t *testing.T) { + ctx := context.Background() + s, _ := ioStoreDocker(t) + + // No trunks defined at all + + tests := []struct { + name string + call *rpc.SIPCall + wantTrunkID string + wantUser string + wantPass string + wantErr bool + wantErrMsg string + }{ + { + name: "no trunks defined (TrunkMatchEmpty)", + call: &rpc.SIPCall{ + To: &livekit.SIPUri{User: "9999"}, + From: &livekit.SIPUri{User: "8888"}, + SourceIp: "192.168.1.1", + }, + wantErr: false, // TrunkMatchEmpty - no error + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := &rpc.GetSIPTrunkAuthenticationRequest{ + Call: tt.call, + } + resp, err := s.GetSIPTrunkAuthentication(ctx, req) + + if tt.wantErr { + require.Error(t, err) + if tt.wantErrMsg != "" { + require.Contains(t, err.Error(), tt.wantErrMsg) + } + return + } + + require.NoError(t, err) + require.Nil(t, resp) // For TrunkMatchEmpty, we expect no response + }) + } +}