* Detailed responses in trunk iter
This will help discern between cases when
1. No trunks are configured
2. Trunks are configured but none matched.(This can happen when there is no default trunk)
3. There is a default trunk that got applied
4. A specific trunk matched
* Dropping the Iter from the function name
* Adding changeset
928 lines
28 KiB
Go
928 lines
28 KiB
Go
// Copyright 2023 LiveKit, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package sip
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"math"
|
|
"net/netip"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/dennwc/iters"
|
|
"github.com/twitchtv/twirp"
|
|
"golang.org/x/exp/slices"
|
|
|
|
"github.com/livekit/protocol/livekit"
|
|
"github.com/livekit/protocol/logger"
|
|
"github.com/livekit/protocol/rpc"
|
|
"github.com/livekit/protocol/utils"
|
|
"github.com/livekit/protocol/utils/guid"
|
|
)
|
|
|
|
//go:generate stringer -type TrunkFilteredReason -trimprefix TrunkFiltered
|
|
//go:generate stringer -type TrunkConflictReason -trimprefix TrunkConflict
|
|
//go:generate stringer -type DispatchRuleConflictReason -trimprefix DispatchRuleConflict
|
|
|
|
func NewCallID() string {
|
|
return guid.New(utils.SIPCallPrefix)
|
|
}
|
|
|
|
type ErrNoDispatchMatched struct {
|
|
NoRules bool
|
|
NoTrunks bool
|
|
CalledNumber string
|
|
}
|
|
|
|
func (e *ErrNoDispatchMatched) Error() string {
|
|
if e.NoRules {
|
|
return "No SIP Dispatch Rules defined"
|
|
}
|
|
if e.NoTrunks {
|
|
return fmt.Sprintf("No SIP Trunk or Dispatch Rules matched for %q", e.CalledNumber)
|
|
}
|
|
return fmt.Sprintf("No SIP Dispatch Rules matched for %q", e.CalledNumber)
|
|
}
|
|
|
|
// DispatchRulePriority returns sorting priority for dispatch rules. Lower value means higher priority.
|
|
func DispatchRulePriority(info *livekit.SIPDispatchRuleInfo) int32 {
|
|
// In all these cases, prefer pin-protected rules and rules for specific calling number.
|
|
// Thus, the order will be the following:
|
|
// - 0: Direct or Pin (both pin-protected)
|
|
// - 1: Caller, aka Individual (pin-protected)
|
|
// - 2: Callee (pin-protected)
|
|
// - 100: Direct (open)
|
|
// - 101: Caller, aka Individual (open)
|
|
// - 102: Callee (open)
|
|
// Also, add 1K penalty for not specifying the calling number.
|
|
const (
|
|
last = math.MaxInt32
|
|
)
|
|
// TODO: Maybe allow setting specific priorities for dispatch rules?
|
|
priority := int32(0)
|
|
switch rule := info.GetRule().GetRule().(type) {
|
|
default:
|
|
return last
|
|
case *livekit.SIPDispatchRule_DispatchRuleDirect:
|
|
if rule.DispatchRuleDirect.GetPin() != "" {
|
|
priority = 0
|
|
} else {
|
|
priority = 100
|
|
}
|
|
case *livekit.SIPDispatchRule_DispatchRuleIndividual:
|
|
if rule.DispatchRuleIndividual.GetPin() != "" {
|
|
priority = 1
|
|
} else {
|
|
priority = 101
|
|
}
|
|
case *livekit.SIPDispatchRule_DispatchRuleCallee:
|
|
if rule.DispatchRuleCallee.GetPin() != "" {
|
|
priority = 2
|
|
} else {
|
|
priority = 102
|
|
}
|
|
}
|
|
if len(info.InboundNumbers) == 0 {
|
|
priority += 1000
|
|
}
|
|
return priority
|
|
}
|
|
|
|
func hasHigherPriority(r1, r2 *livekit.SIPDispatchRuleInfo) bool {
|
|
p1, p2 := DispatchRulePriority(r1), DispatchRulePriority(r2)
|
|
if p1 < p2 {
|
|
return true
|
|
} else if p1 > p2 {
|
|
return false
|
|
}
|
|
// For predictable sorting order.
|
|
room1, _, _ := GetPinAndRoom(r1)
|
|
room2, _, _ := GetPinAndRoom(r2)
|
|
return room1 < room2
|
|
}
|
|
|
|
// SortDispatchRules predictably sorts dispatch rules by priority (first one is highest).
|
|
func SortDispatchRules(rules []*livekit.SIPDispatchRuleInfo) {
|
|
sort.Slice(rules, func(i, j int) bool {
|
|
return hasHigherPriority(rules[i], rules[j])
|
|
})
|
|
}
|
|
|
|
func printID(s string) string {
|
|
if s == "" {
|
|
return "<new>"
|
|
}
|
|
return s
|
|
}
|
|
|
|
// ValidateDispatchRules checks a set of dispatch rules for conflicts.
|
|
//
|
|
// Deprecated: use ValidateDispatchRulesIter
|
|
func ValidateDispatchRules(rules []*livekit.SIPDispatchRuleInfo, opts ...MatchDispatchRuleOpt) error {
|
|
_, err := ValidateDispatchRulesIter(iters.Slice(rules), opts...)
|
|
return err
|
|
}
|
|
|
|
// ValidateDispatchRulesIter checks a set of dispatch rules for conflicts.
|
|
func ValidateDispatchRulesIter(it iters.Iter[*livekit.SIPDispatchRuleInfo], opts ...MatchDispatchRuleOpt) (best *livekit.SIPDispatchRuleInfo, _ error) {
|
|
it = NewDispatchRuleValidator(opts...).ValidateIter(it)
|
|
defer it.Close()
|
|
for {
|
|
r, err := it.Next()
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return best, err
|
|
}
|
|
if best == nil || hasHigherPriority(r, best) {
|
|
best = r
|
|
}
|
|
}
|
|
return best, nil
|
|
}
|
|
|
|
func NewDispatchRuleValidator(opts ...MatchDispatchRuleOpt) *DispatchRuleValidator {
|
|
var opt matchDispatchRuleOpts
|
|
for _, fnc := range opts {
|
|
fnc(&opt)
|
|
}
|
|
opt.defaults()
|
|
return &DispatchRuleValidator{
|
|
opt: opt,
|
|
byRuleKey: make(map[dispatchRuleKey]*livekit.SIPDispatchRuleInfo),
|
|
}
|
|
}
|
|
|
|
type dispatchRuleKey struct {
|
|
Pin string
|
|
Trunk string
|
|
Number string
|
|
}
|
|
|
|
type DispatchRuleValidator struct {
|
|
opt matchDispatchRuleOpts
|
|
byRuleKey map[dispatchRuleKey]*livekit.SIPDispatchRuleInfo
|
|
}
|
|
|
|
func (v *DispatchRuleValidator) ValidateIter(it iters.Iter[*livekit.SIPDispatchRuleInfo]) iters.Iter[*livekit.SIPDispatchRuleInfo] {
|
|
return &dispatchRuleValidatorIter{v: v, it: it}
|
|
}
|
|
|
|
func (v *DispatchRuleValidator) Validate(r *livekit.SIPDispatchRuleInfo) error {
|
|
_, pin, err := GetPinAndRoom(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
trunks := r.TrunkIds
|
|
if len(trunks) == 0 {
|
|
// This rule matches all trunks, but collides only with other default ones (specific rules take priority).
|
|
trunks = []string{""}
|
|
}
|
|
numbers := r.InboundNumbers
|
|
if len(numbers) == 0 {
|
|
// This rule matches all numbers, but collides only with other default ones (specific rules take priority).
|
|
numbers = []string{""}
|
|
}
|
|
for _, trunk := range trunks {
|
|
for _, number := range numbers {
|
|
key := dispatchRuleKey{Pin: pin, Trunk: trunk, Number: NormalizeNumber(number)}
|
|
r2 := v.byRuleKey[key]
|
|
if r2 != nil {
|
|
v.opt.Conflict(r, r2, DispatchRuleConflictGeneric)
|
|
if v.opt.AllowConflicts {
|
|
continue
|
|
}
|
|
return twirp.NewErrorf(twirp.InvalidArgument, "Conflicting SIP Dispatch Rules: same Trunk+Number+PIN combination for for %q and %q",
|
|
printID(r.SipDispatchRuleId), printID(r2.SipDispatchRuleId))
|
|
}
|
|
v.byRuleKey[key] = r
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type dispatchRuleValidatorIter struct {
|
|
v *DispatchRuleValidator
|
|
it iters.Iter[*livekit.SIPDispatchRuleInfo]
|
|
}
|
|
|
|
func (v *dispatchRuleValidatorIter) Next() (*livekit.SIPDispatchRuleInfo, error) {
|
|
r, err := v.it.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r = v.v.opt.Replace(r)
|
|
if err = v.v.Validate(r); err != nil {
|
|
return nil, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (v *dispatchRuleValidatorIter) Close() {
|
|
v.it.Close()
|
|
}
|
|
|
|
// SelectDispatchRule takes a list of dispatch rules, and takes the decision which one should be selected.
|
|
// It returns an error if there are conflicting rules. Returns nil if no rules match.
|
|
//
|
|
// Deprecated: use MatchDispatchRuleIter
|
|
func SelectDispatchRule(rules []*livekit.SIPDispatchRuleInfo, req *rpc.EvaluateSIPDispatchRulesRequest, opts ...MatchDispatchRuleOpt) (*livekit.SIPDispatchRuleInfo, error) {
|
|
// Sorting will do the selection for us. We already filtered out irrelevant ones in MatchDispatchRule and above.
|
|
// Nil is fine here. We will report "no rules matched" later.
|
|
return ValidateDispatchRulesIter(iters.Slice(rules), opts...)
|
|
}
|
|
|
|
// GetPinAndRoom returns a room name/prefix and the pin for a dispatch rule. Just a convenience wrapper.
|
|
func GetPinAndRoom(info *livekit.SIPDispatchRuleInfo) (room, pin string, err error) {
|
|
// TODO: Could probably add methods on SIPDispatchRuleInfo struct instead.
|
|
switch rule := info.GetRule().GetRule().(type) {
|
|
default:
|
|
return "", "", fmt.Errorf("Unsupported SIP Dispatch Rule: %T", rule)
|
|
case *livekit.SIPDispatchRule_DispatchRuleDirect:
|
|
pin = rule.DispatchRuleDirect.GetPin()
|
|
room = rule.DispatchRuleDirect.GetRoomName()
|
|
case *livekit.SIPDispatchRule_DispatchRuleIndividual:
|
|
pin = rule.DispatchRuleIndividual.GetPin()
|
|
room = rule.DispatchRuleIndividual.GetRoomPrefix()
|
|
case *livekit.SIPDispatchRule_DispatchRuleCallee:
|
|
pin = rule.DispatchRuleCallee.GetPin()
|
|
room = rule.DispatchRuleCallee.GetRoomPrefix()
|
|
}
|
|
return room, pin, nil
|
|
}
|
|
|
|
func printNumbers(numbers []string) string {
|
|
if len(numbers) == 0 {
|
|
return "<any>"
|
|
}
|
|
return fmt.Sprintf("%q", numbers)
|
|
}
|
|
|
|
var (
|
|
reNumber = regexp.MustCompile(`^\+?[\d\- ()]+$`)
|
|
reNumberRepl = strings.NewReplacer(
|
|
" ", "",
|
|
"-", "",
|
|
"(", "",
|
|
")", "",
|
|
)
|
|
)
|
|
|
|
func NormalizeNumber(num string) string {
|
|
if num == "" {
|
|
return ""
|
|
}
|
|
if !reNumber.MatchString(num) {
|
|
return num
|
|
}
|
|
num = reNumberRepl.Replace(num)
|
|
if !strings.HasPrefix(num, "+") {
|
|
return "+" + num
|
|
}
|
|
return num
|
|
}
|
|
|
|
func validateTrunkInbound(byInbound map[string]*livekit.SIPInboundTrunkInfo, t *livekit.SIPInboundTrunkInfo, opt *matchTrunkOpts) error {
|
|
if len(t.AllowedNumbers) == 0 {
|
|
if t2 := byInbound[""]; t2 != nil {
|
|
opt.Conflict(t, t2, TrunkConflictCalledNumber)
|
|
if opt.AllowConflicts {
|
|
return nil
|
|
}
|
|
return twirp.NewErrorf(twirp.InvalidArgument, "Conflicting inbound SIP Trunks: %q and %q, using the same number(s) %s without AllowedNumbers set",
|
|
printID(t.SipTrunkId), printID(t2.SipTrunkId), printNumbers(t.Numbers))
|
|
}
|
|
byInbound[""] = t
|
|
} else {
|
|
for _, num := range t.AllowedNumbers {
|
|
inboundKey := NormalizeNumber(num)
|
|
t2 := byInbound[inboundKey]
|
|
if t2 != nil {
|
|
opt.Conflict(t, t2, TrunkConflictCallingNumber)
|
|
if opt.AllowConflicts {
|
|
continue
|
|
}
|
|
return twirp.NewErrorf(twirp.InvalidArgument, "Conflicting inbound SIP Trunks: %q and %q, using the same number(s) %s and AllowedNumber %q",
|
|
printID(t.SipTrunkId), printID(t2.SipTrunkId), printNumbers(t.Numbers), num)
|
|
}
|
|
byInbound[inboundKey] = t
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateTrunks checks a set of trunks for conflicts.
|
|
//
|
|
// Deprecated: use ValidateTrunksIter
|
|
func ValidateTrunks(trunks []*livekit.SIPInboundTrunkInfo, opts ...MatchTrunkOpt) error {
|
|
return ValidateTrunksIter(iters.Slice(trunks), opts...)
|
|
}
|
|
|
|
// ValidateTrunksIter checks a set of trunks for conflicts.
|
|
func ValidateTrunksIter(it iters.Iter[*livekit.SIPInboundTrunkInfo], opts ...MatchTrunkOpt) error {
|
|
defer it.Close()
|
|
var opt matchTrunkOpts
|
|
for _, fnc := range opts {
|
|
fnc(&opt)
|
|
}
|
|
opt.defaults()
|
|
byOutboundAndInbound := make(map[string]map[string]*livekit.SIPInboundTrunkInfo)
|
|
for {
|
|
t, err := it.Next()
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
t = opt.Replace(t)
|
|
if len(t.Numbers) == 0 {
|
|
byInbound := byOutboundAndInbound[""]
|
|
if byInbound == nil {
|
|
byInbound = make(map[string]*livekit.SIPInboundTrunkInfo)
|
|
byOutboundAndInbound[""] = byInbound
|
|
}
|
|
if err := validateTrunkInbound(byInbound, t, &opt); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
for _, num := range t.Numbers {
|
|
byInbound := byOutboundAndInbound[num]
|
|
if byInbound == nil {
|
|
byInbound = make(map[string]*livekit.SIPInboundTrunkInfo)
|
|
byOutboundAndInbound[num] = byInbound
|
|
}
|
|
if err := validateTrunkInbound(byInbound, t, &opt); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isValidMask(mask string) bool {
|
|
// Allowed formats:
|
|
// - 1.2.3.4
|
|
// - 1.2.3.4/8
|
|
// - [::]
|
|
// - [::]/8
|
|
// - some.host.name
|
|
if strings.ContainsAny(mask, "()+*;, \t\n\r") {
|
|
return false
|
|
}
|
|
if strings.Contains(mask, "://") {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func filterInvalidAddrMasks(masks []string) []string {
|
|
if len(masks) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(masks))
|
|
for _, m := range masks {
|
|
if isValidMask(m) {
|
|
out = append(out, m)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func matchAddrMask(ip netip.Addr, mask string) bool {
|
|
if !strings.Contains(mask, "/") {
|
|
expIP, err := netip.ParseAddr(mask)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return ip == expIP
|
|
}
|
|
pref, err := netip.ParsePrefix(mask)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return pref.Contains(ip)
|
|
}
|
|
|
|
func matchAddrMasks(addr string, host string, masks []string) bool {
|
|
ip, err := netip.ParseAddr(addr)
|
|
if err != nil {
|
|
return true
|
|
}
|
|
masks = filterInvalidAddrMasks(masks)
|
|
if len(masks) == 0 {
|
|
return true
|
|
}
|
|
for _, mask := range masks {
|
|
if mask == host || matchAddrMask(ip, mask) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func matchNumbers(num string, allowed []string) bool {
|
|
if len(allowed) == 0 {
|
|
return true
|
|
}
|
|
norm := NormalizeNumber(num)
|
|
for _, allow := range allowed {
|
|
if num == allow || norm == NormalizeNumber(allow) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// TrunkMatchType indicates how a trunk was matched
|
|
type TrunkMatchType int
|
|
|
|
const (
|
|
// TrunkMatchEmpty indicates no trunks were defined
|
|
TrunkMatchEmpty TrunkMatchType = iota
|
|
// TrunkMatchNone indicates trunks exist but none matched
|
|
TrunkMatchNone
|
|
// TrunkMatchDefault indicates only a default trunk (with no specific numbers) matched
|
|
TrunkMatchDefault
|
|
// TrunkMatchSpecific indicates a trunk with specific numbers matched
|
|
TrunkMatchSpecific
|
|
)
|
|
|
|
// TrunkMatchResult provides detailed information about the trunk matching process
|
|
type TrunkMatchResult struct {
|
|
// The matched trunk, if any
|
|
Trunk *livekit.SIPInboundTrunkInfo
|
|
// How the trunk was matched
|
|
MatchType TrunkMatchType
|
|
// Number of default trunks found
|
|
DefaultTrunkCount int
|
|
}
|
|
|
|
// MatchTrunk finds a SIP Trunk definition matching the request.
|
|
// Returns nil if no rules matched or an error if there are conflicting definitions.
|
|
//
|
|
// Deprecated: use MatchTrunkIter
|
|
func MatchTrunk(trunks []*livekit.SIPInboundTrunkInfo, call *rpc.SIPCall, opts ...MatchTrunkOpt) (*livekit.SIPInboundTrunkInfo, error) {
|
|
return MatchTrunkIter(iters.Slice(trunks), call, opts...)
|
|
}
|
|
|
|
// MatchTrunkDetailed is like MatchTrunkIter but returns detailed match information
|
|
func MatchTrunkDetailed(it iters.Iter[*livekit.SIPInboundTrunkInfo], call *rpc.SIPCall, opts ...MatchTrunkOpt) (*TrunkMatchResult, error) {
|
|
defer it.Close()
|
|
var opt matchTrunkOpts
|
|
for _, fnc := range opts {
|
|
fnc(&opt)
|
|
}
|
|
opt.defaults()
|
|
|
|
result := &TrunkMatchResult{
|
|
MatchType: TrunkMatchEmpty, // Start with assumption it's empty
|
|
}
|
|
|
|
var (
|
|
selectedTrunk *livekit.SIPInboundTrunkInfo
|
|
defaultTrunk *livekit.SIPInboundTrunkInfo
|
|
defaultTrunkPrev *livekit.SIPInboundTrunkInfo
|
|
sawAnyTrunk bool
|
|
)
|
|
calledNorm := NormalizeNumber(call.To.User)
|
|
for {
|
|
tr, err := it.Next()
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return nil, err
|
|
}
|
|
if !sawAnyTrunk {
|
|
sawAnyTrunk = true
|
|
result.MatchType = TrunkMatchNone // We have trunks but haven't matched any yet
|
|
}
|
|
tr = opt.Replace(tr)
|
|
// Do not consider it if number doesn't match.
|
|
if !matchNumbers(call.From.User, tr.AllowedNumbers) {
|
|
if !opt.Filtered(tr, TrunkFilteredCallingNumberDisallowed) {
|
|
continue
|
|
}
|
|
}
|
|
if !matchAddrMasks(call.SourceIp, call.From.Host, tr.AllowedAddresses) {
|
|
if !opt.Filtered(tr, TrunkFilteredSourceAddressDisallowed) {
|
|
continue
|
|
}
|
|
}
|
|
if len(tr.Numbers) == 0 {
|
|
// Default/wildcard trunk.
|
|
defaultTrunkPrev = defaultTrunk
|
|
defaultTrunk = tr
|
|
result.DefaultTrunkCount++
|
|
} else {
|
|
for _, num := range tr.Numbers {
|
|
if num == call.To.User || NormalizeNumber(num) == calledNorm {
|
|
// Trunk specific to the number.
|
|
if selectedTrunk != nil {
|
|
opt.Conflict(selectedTrunk, tr, TrunkConflictCalledNumber)
|
|
if opt.AllowConflicts {
|
|
// This path is unreachable, since we pick the first trunk. Kept for completeness.
|
|
continue
|
|
}
|
|
return nil, twirp.NewErrorf(twirp.FailedPrecondition, "Multiple SIP Trunks matched for %q", call.To.User)
|
|
}
|
|
selectedTrunk = tr
|
|
if opt.AllowConflicts {
|
|
// Pick the first match as soon as it's found. We don't care about conflicts.
|
|
result.Trunk = selectedTrunk
|
|
result.MatchType = TrunkMatchSpecific
|
|
return result, nil
|
|
}
|
|
// Keep searching! We want to know if there are any conflicting Trunk definitions.
|
|
} else {
|
|
opt.Filtered(tr, TrunkFilteredCalledNumberDisallowed)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if selectedTrunk != nil {
|
|
result.Trunk = selectedTrunk
|
|
result.MatchType = TrunkMatchSpecific
|
|
return result, nil
|
|
}
|
|
if result.DefaultTrunkCount > 1 {
|
|
opt.Conflict(defaultTrunk, defaultTrunkPrev, TrunkConflictDefault)
|
|
if !opt.AllowConflicts {
|
|
return nil, twirp.NewErrorf(twirp.FailedPrecondition, "Multiple default SIP Trunks matched for %q", call.To.User)
|
|
}
|
|
}
|
|
if defaultTrunk != nil {
|
|
result.Trunk = defaultTrunk
|
|
result.MatchType = TrunkMatchDefault
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
type matchTrunkOpts struct {
|
|
AllowConflicts bool
|
|
Filtered TrunkFilteredFunc
|
|
Conflict TrunkConflictFunc
|
|
Replace TrunkReplaceFunc
|
|
}
|
|
|
|
func (opt *matchTrunkOpts) defaults() {
|
|
if opt.Filtered == nil {
|
|
opt.Filtered = func(_ *livekit.SIPInboundTrunkInfo, _ TrunkFilteredReason) bool {
|
|
return false
|
|
}
|
|
}
|
|
if opt.Conflict == nil {
|
|
opt.Conflict = func(_, _ *livekit.SIPInboundTrunkInfo, _ TrunkConflictReason) {}
|
|
}
|
|
if opt.Replace == nil {
|
|
opt.Replace = func(t *livekit.SIPInboundTrunkInfo) *livekit.SIPInboundTrunkInfo {
|
|
return t
|
|
}
|
|
}
|
|
}
|
|
|
|
type MatchTrunkOpt func(opt *matchTrunkOpts)
|
|
|
|
type TrunkFilteredReason int
|
|
|
|
const (
|
|
TrunkFilteredInvalid = TrunkFilteredReason(iota)
|
|
TrunkFilteredCallingNumberDisallowed
|
|
TrunkFilteredCalledNumberDisallowed
|
|
TrunkFilteredSourceAddressDisallowed
|
|
)
|
|
|
|
type TrunkFilteredFunc func(tr *livekit.SIPInboundTrunkInfo, reason TrunkFilteredReason) bool
|
|
|
|
// WithTrunkFiltered sets a callback that is called when selected Trunk(s) doesn't match the call.
|
|
// If the callback returns true, trunk will not be filtered.
|
|
func WithTrunkFiltered(fnc TrunkFilteredFunc) MatchTrunkOpt {
|
|
return func(opt *matchTrunkOpts) {
|
|
opt.Filtered = fnc
|
|
}
|
|
}
|
|
|
|
type TrunkConflictReason int
|
|
|
|
const (
|
|
TrunkConflictDefault = TrunkConflictReason(iota)
|
|
TrunkConflictCalledNumber
|
|
TrunkConflictCallingNumber
|
|
)
|
|
|
|
type TrunkConflictFunc func(t1, t2 *livekit.SIPInboundTrunkInfo, reason TrunkConflictReason)
|
|
|
|
// WithAllowTrunkConflicts allows conflicting Trunk definitions by picking the first match.
|
|
//
|
|
// Using this option will prevent TrunkConflictFunc from firing, since the first match will be returned immediately.
|
|
func WithAllowTrunkConflicts() MatchTrunkOpt {
|
|
return func(opt *matchTrunkOpts) {
|
|
opt.AllowConflicts = true
|
|
}
|
|
}
|
|
|
|
// WithTrunkConflict sets a callback that is called when two Trunks conflict.
|
|
func WithTrunkConflict(fnc TrunkConflictFunc) MatchTrunkOpt {
|
|
return func(opt *matchTrunkOpts) {
|
|
opt.Conflict = fnc
|
|
}
|
|
}
|
|
|
|
type TrunkReplaceFunc func(t *livekit.SIPInboundTrunkInfo) *livekit.SIPInboundTrunkInfo
|
|
|
|
// WithTrunkReplace sets a callback that is called to potentially replace trunks before matching runs.
|
|
func WithTrunkReplace(fnc TrunkReplaceFunc) MatchTrunkOpt {
|
|
return func(opt *matchTrunkOpts) {
|
|
opt.Replace = fnc
|
|
}
|
|
}
|
|
|
|
// MatchTrunkIter finds a SIP Trunk definition matching the request.
|
|
// Returns nil if no rules matched or an error if there are conflicting definitions.
|
|
func MatchTrunkIter(it iters.Iter[*livekit.SIPInboundTrunkInfo], call *rpc.SIPCall, opts ...MatchTrunkOpt) (*livekit.SIPInboundTrunkInfo, error) {
|
|
result, err := MatchTrunkDetailed(it, call, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result.Trunk, nil
|
|
}
|
|
|
|
// MatchDispatchRule finds the best dispatch rule matching the request parameters. Returns an error if no rule matched.
|
|
// Trunk parameter can be nil, in which case only wildcard dispatch rules will be effective (ones without Trunk IDs).
|
|
//
|
|
// Deprecated: use MatchDispatchRuleIter
|
|
func MatchDispatchRule(trunk *livekit.SIPInboundTrunkInfo, rules []*livekit.SIPDispatchRuleInfo, req *rpc.EvaluateSIPDispatchRulesRequest, opts ...MatchDispatchRuleOpt) (*livekit.SIPDispatchRuleInfo, error) {
|
|
return MatchDispatchRuleIter(trunk, iters.Slice(rules), req, opts...)
|
|
}
|
|
|
|
type matchDispatchRuleOpts struct {
|
|
AllowConflicts bool
|
|
Conflict DispatchRuleConflictFunc
|
|
Replace DispatchRuleReplaceFunc
|
|
}
|
|
|
|
func (opt *matchDispatchRuleOpts) defaults() {
|
|
if opt.Conflict == nil {
|
|
opt.Conflict = func(_, _ *livekit.SIPDispatchRuleInfo, _ DispatchRuleConflictReason) {}
|
|
}
|
|
if opt.Replace == nil {
|
|
opt.Replace = func(r *livekit.SIPDispatchRuleInfo) *livekit.SIPDispatchRuleInfo {
|
|
return r
|
|
}
|
|
}
|
|
}
|
|
|
|
type MatchDispatchRuleOpt func(opt *matchDispatchRuleOpts)
|
|
|
|
type DispatchRuleConflictReason int
|
|
|
|
const (
|
|
DispatchRuleConflictGeneric = DispatchRuleConflictReason(iota)
|
|
)
|
|
|
|
type DispatchRuleConflictFunc func(r1, r2 *livekit.SIPDispatchRuleInfo, reason DispatchRuleConflictReason)
|
|
|
|
// WithAllowDispatchRuleConflicts allows conflicting DispatchRule definitions.
|
|
func WithAllowDispatchRuleConflicts() MatchDispatchRuleOpt {
|
|
return func(opt *matchDispatchRuleOpts) {
|
|
opt.AllowConflicts = true
|
|
}
|
|
}
|
|
|
|
// WithDispatchRuleConflict sets a callback that is called when two DispatchRules conflict.
|
|
func WithDispatchRuleConflict(fnc DispatchRuleConflictFunc) MatchDispatchRuleOpt {
|
|
return func(opt *matchDispatchRuleOpts) {
|
|
opt.Conflict = fnc
|
|
}
|
|
}
|
|
|
|
type DispatchRuleReplaceFunc func(r *livekit.SIPDispatchRuleInfo) *livekit.SIPDispatchRuleInfo
|
|
|
|
// WithDispatchRuleReplace sets a callback that is called to potentially replace dispatch rules before matching runs.
|
|
func WithDispatchRuleReplace(fnc DispatchRuleReplaceFunc) MatchDispatchRuleOpt {
|
|
return func(opt *matchDispatchRuleOpts) {
|
|
opt.Replace = fnc
|
|
}
|
|
}
|
|
|
|
// MatchDispatchRuleIter finds the best dispatch rule matching the request parameters. Returns an error if no rule matched.
|
|
// Trunk parameter can be nil, in which case only wildcard dispatch rules will be effective (ones without Trunk IDs).
|
|
func MatchDispatchRuleIter(trunk *livekit.SIPInboundTrunkInfo, rules iters.Iter[*livekit.SIPDispatchRuleInfo], req *rpc.EvaluateSIPDispatchRulesRequest, opts ...MatchDispatchRuleOpt) (*livekit.SIPDispatchRuleInfo, error) {
|
|
rules = NewDispatchRuleValidator(opts...).ValidateIter(rules)
|
|
defer rules.Close()
|
|
// Trunk can still be nil here in case none matched or were defined.
|
|
// This is still fine, but only in case we'll match exactly one wildcard dispatch rule.
|
|
|
|
// We split the matched dispatch rules into two sets in relation to Trunks: specific and default (aka wildcard).
|
|
// First, attempt to match any of the specific rules, where we did match the Trunk ID.
|
|
// If nothing matches there - fallback to default/wildcard rules, where no Trunk IDs were mentioned.
|
|
var (
|
|
specificRule *livekit.SIPDispatchRuleInfo
|
|
specificRuleCnt int
|
|
defaultRule *livekit.SIPDispatchRuleInfo
|
|
defaultRuleCnt int
|
|
)
|
|
noPin := req.NoPin
|
|
sentPin := req.GetPin()
|
|
for {
|
|
info, err := rules.Next()
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(info.InboundNumbers) != 0 && !slices.Contains(info.InboundNumbers, req.CallingNumber) {
|
|
continue
|
|
}
|
|
_, rulePin, err := GetPinAndRoom(info)
|
|
if err != nil {
|
|
logger.Errorw("Invalid SIP Dispatch Rule", err, "dispatchRuleID", info.SipDispatchRuleId)
|
|
continue
|
|
}
|
|
// Filter heavily on the Pin, so that only relevant rules remain.
|
|
if noPin {
|
|
if rulePin != "" {
|
|
// Skip pin-protected rules if no pin mode requested.
|
|
continue
|
|
}
|
|
} else if sentPin != "" {
|
|
if rulePin == "" {
|
|
// Pin already sent, skip non-pin-protected rules.
|
|
continue
|
|
}
|
|
if sentPin != rulePin {
|
|
// Pin doesn't match. Don't return an error here, just wait for other rule to match (or none at all).
|
|
// Note that we will NOT match non-pin-protected rules, thus it will not fallback to open rules.
|
|
continue
|
|
}
|
|
}
|
|
if len(info.TrunkIds) == 0 {
|
|
// Default/wildcard dispatch rule.
|
|
defaultRuleCnt++
|
|
if defaultRule == nil || hasHigherPriority(info, defaultRule) {
|
|
defaultRule = info
|
|
}
|
|
continue
|
|
}
|
|
// Specific dispatch rules. Require a Trunk associated with the number.
|
|
if trunk == nil {
|
|
continue
|
|
}
|
|
if !slices.Contains(info.TrunkIds, trunk.SipTrunkId) {
|
|
continue
|
|
}
|
|
specificRuleCnt++
|
|
if specificRule == nil || hasHigherPriority(info, specificRule) {
|
|
specificRule = info
|
|
}
|
|
}
|
|
if specificRuleCnt == 0 && defaultRuleCnt == 0 {
|
|
err := &ErrNoDispatchMatched{NoRules: true, NoTrunks: trunk == nil, CalledNumber: req.CalledNumber}
|
|
return nil, twirp.WrapError(twirp.NewErrorf(twirp.FailedPrecondition, err.Error()), err)
|
|
}
|
|
if specificRule != nil {
|
|
return specificRule, nil
|
|
}
|
|
if defaultRule != nil {
|
|
return defaultRule, nil
|
|
}
|
|
err := &ErrNoDispatchMatched{NoRules: false, NoTrunks: trunk == nil, CalledNumber: req.CalledNumber}
|
|
return nil, twirp.WrapError(twirp.NewErrorf(twirp.FailedPrecondition, err.Error()), err)
|
|
}
|
|
|
|
// EvaluateDispatchRule checks a selected Dispatch Rule against the provided request.
|
|
func EvaluateDispatchRule(projectID string, trunk *livekit.SIPInboundTrunkInfo, rule *livekit.SIPDispatchRuleInfo, req *rpc.EvaluateSIPDispatchRulesRequest) (*rpc.EvaluateSIPDispatchRulesResponse, error) {
|
|
call := req.SIPCall()
|
|
sentPin := req.GetPin()
|
|
|
|
trunkID := req.SipTrunkId
|
|
if trunk != nil {
|
|
trunkID = trunk.SipTrunkId
|
|
}
|
|
enc := livekit.SIPMediaEncryption_SIP_MEDIA_ENCRYPT_DISABLE
|
|
if trunk != nil {
|
|
enc = trunk.MediaEncryption
|
|
}
|
|
attrs := maps.Clone(rule.Attributes)
|
|
if attrs == nil {
|
|
attrs = make(map[string]string)
|
|
}
|
|
for k, v := range req.ExtraAttributes {
|
|
attrs[k] = v
|
|
}
|
|
attrs[livekit.AttrSIPCallID] = call.LkCallId
|
|
attrs[livekit.AttrSIPTrunkID] = trunkID
|
|
|
|
to := call.To.User
|
|
from := call.From.User
|
|
fromName := "Phone " + from
|
|
fromID := "sip_" + from
|
|
if rule.HidePhoneNumber {
|
|
// Mask the phone number, hash identity. Omit number in attrs.
|
|
h := sha256.Sum256([]byte(call.From.User))
|
|
fromID = "sip_" + hex.EncodeToString(h[:8])
|
|
// TODO: Maybe keep regional code, but mask all but 4 last digits?
|
|
n := 4
|
|
if len(from) <= 4 {
|
|
n = 1
|
|
}
|
|
from = from[len(from)-n:]
|
|
fromName = "Phone " + from
|
|
} else {
|
|
attrs[livekit.AttrSIPPhoneNumber] = call.From.User
|
|
attrs[livekit.AttrSIPHostName] = call.From.Host
|
|
attrs[livekit.AttrSIPTrunkNumber] = call.To.User
|
|
}
|
|
|
|
room, rulePin, err := GetPinAndRoom(rule)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rulePin != "" {
|
|
if sentPin == "" {
|
|
return &rpc.EvaluateSIPDispatchRulesResponse{
|
|
ProjectId: projectID,
|
|
SipTrunkId: trunkID,
|
|
SipDispatchRuleId: rule.SipDispatchRuleId,
|
|
Result: rpc.SIPDispatchResult_REQUEST_PIN,
|
|
MediaEncryption: enc,
|
|
RequestPin: true,
|
|
}, nil
|
|
}
|
|
if rulePin != sentPin {
|
|
// This should never happen in practice, because matchSIPDispatchRule should remove rules with the wrong pin.
|
|
return nil, twirp.NewError(twirp.PermissionDenied, "Incorrect PIN for SIP room")
|
|
}
|
|
} else {
|
|
// Pin was sent, but room doesn't require one. Assume user accidentally pressed phone button.
|
|
}
|
|
switch rule := rule.GetRule().GetRule().(type) {
|
|
case *livekit.SIPDispatchRule_DispatchRuleIndividual:
|
|
// TODO: Remove "_" if the prefix is empty for consistency with Callee dispatch rule.
|
|
// TODO: Do we need to escape specific characters in the number?
|
|
// TODO: Include actual SIP call ID in the room name?
|
|
room = fmt.Sprintf("%s_%s_%s", rule.DispatchRuleIndividual.GetRoomPrefix(), from, guid.New(""))
|
|
case *livekit.SIPDispatchRule_DispatchRuleCallee:
|
|
room = to
|
|
if pref := rule.DispatchRuleCallee.GetRoomPrefix(); pref != "" {
|
|
room = pref + "_" + to
|
|
}
|
|
if rule.DispatchRuleCallee.Randomize {
|
|
room += "_" + guid.New("")
|
|
}
|
|
}
|
|
attrs[livekit.AttrSIPDispatchRuleID] = rule.SipDispatchRuleId
|
|
resp := &rpc.EvaluateSIPDispatchRulesResponse{
|
|
ProjectId: projectID,
|
|
SipTrunkId: trunkID,
|
|
SipDispatchRuleId: rule.SipDispatchRuleId,
|
|
Result: rpc.SIPDispatchResult_ACCEPT,
|
|
RoomName: room,
|
|
ParticipantIdentity: fromID,
|
|
ParticipantName: fromName,
|
|
ParticipantMetadata: rule.Metadata,
|
|
ParticipantAttributes: attrs,
|
|
RoomPreset: rule.RoomPreset,
|
|
RoomConfig: rule.RoomConfig,
|
|
MediaEncryption: enc,
|
|
}
|
|
krispEnabled := false
|
|
if trunk != nil {
|
|
resp.Headers = trunk.Headers
|
|
resp.HeadersToAttributes = trunk.HeadersToAttributes
|
|
resp.AttributesToHeaders = trunk.AttributesToHeaders
|
|
resp.IncludeHeaders = trunk.IncludeHeaders
|
|
resp.RingingTimeout = trunk.RingingTimeout
|
|
resp.MaxCallDuration = trunk.MaxCallDuration
|
|
krispEnabled = krispEnabled || trunk.KrispEnabled
|
|
}
|
|
if rule != nil {
|
|
krispEnabled = krispEnabled || rule.KrispEnabled
|
|
if rule.MediaEncryption != 0 {
|
|
resp.MediaEncryption = rule.MediaEncryption
|
|
}
|
|
}
|
|
if krispEnabled {
|
|
resp.EnabledFeatures = append(resp.EnabledFeatures, livekit.SIPFeature_KRISP_ENABLED)
|
|
}
|
|
return resp, nil
|
|
}
|