Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ type Config struct {
SIPPortListen int `yaml:"sip_port_listen"` // SIP signaling port to listen on
SIPHostname string `yaml:"sip_hostname"`
OutboundRouteHeaders []string `yaml:"outbound_route_headers"` // Route headers prepended to outbound requests, e.g. "<sip:proxy:5060;transport=tcp;lr>"
SIPRingingInterval time.Duration `yaml:"sip_ringing_interval"` // from 1 sec up to 60 (default '1s')
// URIUserPhone appends ;user=phone to outbound INVITE Request-URI, From, and To.
// Required by some PSTN carriers (e.g. Airtel). Can also be enabled per-call via
// feature flag sip.uri_user_phone=true. See livekit/sip#615.
URIUserPhone bool `yaml:"uri_user_phone"`
SIPRingingInterval time.Duration `yaml:"sip_ringing_interval"` // from 1 sec up to 60 (default '1s')
TCP *TCPConfig `yaml:"tcp"`
TLS *TLSConfig `yaml:"tls"`
RTPPort rtcconfig.PortRange `yaml:"rtp_port"`
Expand Down
29 changes: 27 additions & 2 deletions pkg/sip/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,25 @@ func setUriTransport(p *sip.Uri, tr livekit.SIPTransport) {
}
}

// setURIUserPhone adds the RFC 3261/3966 ;user=phone URI parameter.
// Idempotent if the parameter is already present.
func setURIUserPhone(p *sip.Uri) {
if p == nil {
return
}
p.UriParams.Add("user", "phone")
}

func wantURIUserPhone(req *rpc.InternalCreateSIPParticipantRequest, confEnabled bool) bool {
if confEnabled {
return true
}
if req == nil || req.FeatureFlags == nil {
return false
}
return req.FeatureFlags[uriUserPhoneFeatureFlag] == "true"
}

func buildLegacyURI(user, addr string, tr livekit.SIPTransport) (*sip.Uri, error) {
if user == "" {
return nil, fmt.Errorf("number must be set")
Expand Down Expand Up @@ -324,7 +343,7 @@ func buildToHeader(u *livekit.SIPNamedDest, legacyUser, legacyAddr string, tr li
return h, nil
}

func buildOutboundHeaders(req *rpc.InternalCreateSIPParticipantRequest, defaultHost string) (*sip.Uri, *sip.FromHeader, *sip.ToHeader, error) {
func buildOutboundHeaders(req *rpc.InternalCreateSIPParticipantRequest, defaultHost string, uriUserPhone bool) (*sip.Uri, *sip.FromHeader, *sip.ToHeader, error) {
uri, err := buildRequestURI(req.SipRequestUri, req.CallTo, req.Address, req.Transport)
if err != nil {
return nil, nil, nil, psrpc.NewError(psrpc.InvalidArgument, fmt.Errorf("invalid request URI: %w", err))
Expand All @@ -341,6 +360,12 @@ func buildOutboundHeaders(req *rpc.InternalCreateSIPParticipantRequest, defaultH
if err != nil {
return nil, nil, nil, psrpc.NewError(psrpc.InvalidArgument, fmt.Errorf("invalid From header: %w", err))
}
if wantURIUserPhone(req, uriUserPhone) {
// Some carriers (e.g. Airtel) require ;user=phone on telephone URIs (#615).
setURIUserPhone(uri)
setURIUserPhone(&from.Address)
setURIUserPhone(&to.Address)
}
return uri, from, to, nil
}

Expand All @@ -364,7 +389,7 @@ func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCrea
if err != nil {
return nil, err
}
uri, from, to, err := buildOutboundHeaders(req, defaultHost)
uri, from, to, err := buildOutboundHeaders(req, defaultHost, c.conf.URIUserPhone)
if err != nil {
return nil, err
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/sip/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ package sip
const (
signalLoggingFeatureFlag = "sip.signal_logging"
outboundRouteHeadersFeatureFlag = "sip.outbound_route_headers"
// uriUserPhoneFeatureFlag appends ;user=phone to outbound Request-URI, From, and To
// (needed by some carriers such as Airtel; see livekit/sip#615).
uriUserPhoneFeatureFlag = "sip.uri_user_phone"
)
58 changes: 54 additions & 4 deletions pkg/sip/outbound_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,11 +422,11 @@ func TestBuildOutboundHeaders(t *testing.T) {
newReq := func() *rpc.InternalCreateSIPParticipantRequest {
return &rpc.InternalCreateSIPParticipantRequest{}
}
check := func(t testing.TB, req *rpc.InternalCreateSIPParticipantRequest, defaultHost string, expURI, expFrom, expTo, expErr string) {
check := func(t testing.TB, req *rpc.InternalCreateSIPParticipantRequest, defaultHost string, uriUserPhone bool, expURI, expFrom, expTo, expErr string) {
if defaultHost == "" {
defaultHost = "sip.default.test"
}
uri, from, to, err := buildOutboundHeaders(req, defaultHost)
uri, from, to, err := buildOutboundHeaders(req, defaultHost, uriUserPhone)
if expErr != "" {
require.Error(t, err)
require.Equal(t, expErr, err.Error())
Expand All @@ -438,10 +438,13 @@ func TestBuildOutboundHeaders(t *testing.T) {
require.Equal(t, expTo, to.String())
}
expectErr := func(t testing.TB, req *rpc.InternalCreateSIPParticipantRequest, expErr string) {
check(t, req, "", "", "", "", expErr)
check(t, req, "", false, "", "", "", expErr)
}
expect := func(t testing.TB, req *rpc.InternalCreateSIPParticipantRequest, expURI, expFrom, expTo string) {
check(t, req, "", expURI, expFrom, expTo, "")
check(t, req, "", false, expURI, expFrom, expTo, "")
}
expectUserPhone := func(t testing.TB, req *rpc.InternalCreateSIPParticipantRequest, uriUserPhone bool, expURI, expFrom, expTo string) {
check(t, req, "", uriUserPhone, expURI, expFrom, expTo, "")
}
uriVals := func(u *livekit.SIPUri) *livekit.SIPRequestDest {
return &livekit.SIPRequestDest{
Expand Down Expand Up @@ -658,4 +661,51 @@ func TestBuildOutboundHeaders(t *testing.T) {
`To: "User" <sip:333@sip.another.com;transport=tls>`,
)
})
// livekit/sip#615 — Airtel and similar carriers require ;user=phone on telephone URIs.
t.Run("legacy user=phone via config", func(t *testing.T) {
req := newReq()
req.Address = "117.96.31.175:5076"
req.Number = "+911111111111"
req.CallTo = "+912222222222"
expectUserPhone(t, req, true,
`sip:+912222222222@117.96.31.175:5076;user=phone`,
`From: "+911111111111" <sip:+911111111111@sip.default.test;user=phone>`,
`To: <sip:+912222222222@117.96.31.175:5076;user=phone>`,
)
})
t.Run("legacy user=phone via feature flag", func(t *testing.T) {
req := newReq()
req.Address = "sip.test.com"
req.Number = "111"
req.CallTo = "222"
req.FeatureFlags = map[string]string{uriUserPhoneFeatureFlag: "true"}
expectUserPhone(t, req, false,
`sip:222@sip.test.com;user=phone`,
`From: "111" <sip:111@sip.default.test;user=phone>`,
`To: <sip:222@sip.test.com;user=phone>`,
)
})
t.Run("user=phone with transport", func(t *testing.T) {
req := newReq()
req.Transport = livekit.SIPTransport_SIP_TRANSPORT_UDP
req.Address = "sip.test.com"
req.Number = "111"
req.CallTo = "222"
expectUserPhone(t, req, true,
`sip:222@sip.test.com;transport=udp;user=phone`,
`From: "111" <sip:111@sip.default.test;transport=udp;user=phone>`,
`To: <sip:222@sip.test.com;transport=udp;user=phone>`,
)
})
t.Run("user=phone idempotent on raw", func(t *testing.T) {
req := newReq()
req.SipRequestUri = uriRaw(`sip:222@sip.test.com;user=phone`)
req.SipFromHeader = namedRaw("LK", `sip:111@example.com;user=phone`)
req.SipToHeader = namedRaw("User", `sip:333@sip.another.com;user=phone`)
expectUserPhone(t, req, true,
`sip:222@sip.test.com;user=phone`,
`From: "LK" <sip:111@example.com;user=phone>`,
`To: "User" <sip:333@sip.another.com;user=phone>`,
)
})
}