diff --git a/pkg/config/config.go b/pkg/config/config.go index 8192660d..094b60d8 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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. "" - 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"` diff --git a/pkg/sip/client.go b/pkg/sip/client.go index f8ddba33..6cd2eb93 100644 --- a/pkg/sip/client.go +++ b/pkg/sip/client.go @@ -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") @@ -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)) @@ -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 } @@ -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 } diff --git a/pkg/sip/features.go b/pkg/sip/features.go index e27299c9..7ef9ebd2 100644 --- a/pkg/sip/features.go +++ b/pkg/sip/features.go @@ -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" ) diff --git a/pkg/sip/outbound_test.go b/pkg/sip/outbound_test.go index 3ce2a4bb..39e0ef69 100644 --- a/pkg/sip/outbound_test.go +++ b/pkg/sip/outbound_test.go @@ -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()) @@ -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{ @@ -658,4 +661,51 @@ func TestBuildOutboundHeaders(t *testing.T) { `To: "User" `, ) }) + // 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" `, + `To: `, + ) + }) + 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" `, + `To: `, + ) + }) + 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" `, + `To: `, + ) + }) + 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" `, + `To: "User" `, + ) + }) }