Skip to content

Commit 6f33540

Browse files
committed
Support specifying To/From SIP headers for outbound.
1 parent 6813231 commit 6f33540

4 files changed

Lines changed: 202 additions & 80 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ require (
1111
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731
1212
github.com/livekit/media-sdk v0.0.0-20260522182459-8bac15173fa2
1313
github.com/livekit/mediatransportutil v0.0.0-20260309115634-0e2e24b36ee8
14-
github.com/livekit/protocol v1.45.9-0.20260518225207-2cfe2d2aa772
14+
github.com/livekit/protocol v1.46.1-0.20260526102102-06bc4e74f196
1515
github.com/livekit/psrpc v0.7.1
1616
github.com/livekit/server-sdk-go/v2 v2.16.4-0.20260518235838-059306bbcfac
1717
github.com/livekit/sipgo v0.13.2-0.20260519205735-a5b4a38b6ceb

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ github.com/livekit/media-sdk v0.0.0-20260522182459-8bac15173fa2 h1:lLf+4efpv5gme
138138
github.com/livekit/media-sdk v0.0.0-20260522182459-8bac15173fa2/go.mod h1:y6iM86wusHKLd5Cqomiq/nRPB+UkMV6H7JTz/zAOoMs=
139139
github.com/livekit/mediatransportutil v0.0.0-20260309115634-0e2e24b36ee8 h1:coWig9fKxdb/nwOaIoGUUAogso12GblAJh/9SA9hcxk=
140140
github.com/livekit/mediatransportutil v0.0.0-20260309115634-0e2e24b36ee8/go.mod h1:RCd46PT+6sEztld6XpkCrG1xskb0u3SqxIjy4G897Ss=
141-
github.com/livekit/protocol v1.45.9-0.20260518225207-2cfe2d2aa772 h1:JOlU9yyc65+qGW5os8fFgPtih89vSsGT+Dv42nzTMEw=
142-
github.com/livekit/protocol v1.45.9-0.20260518225207-2cfe2d2aa772/go.mod h1:KEPIJ/ZdMFQ9tmmfv/uT9TjQEuEcZupCZBabuRGEC1k=
141+
github.com/livekit/protocol v1.46.1-0.20260526102102-06bc4e74f196 h1:KVmbUFbgfpXVSSbkVB6GiEBnWbYg+xvfDZqhJkPpVKA=
142+
github.com/livekit/protocol v1.46.1-0.20260526102102-06bc4e74f196/go.mod h1:KEPIJ/ZdMFQ9tmmfv/uT9TjQEuEcZupCZBabuRGEC1k=
143143
github.com/livekit/psrpc v0.7.1 h1:ms37az0QTD3UXIWuUC5D/SkmKOlRMVRsI261eBWu/Vw=
144144
github.com/livekit/psrpc v0.7.1/go.mod h1:bZ4iHFQptTkbPnB0LasvRNu/OBYXEu1NA6O5BMFo9kk=
145145
github.com/livekit/server-sdk-go/v2 v2.16.4-0.20260518235838-059306bbcfac h1:/pDmTAM7N3J5bzd5oGUBpQRUPAt17iyn1buTen5ZhXY=

pkg/sip/client.go

Lines changed: 179 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@ package sip
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"log/slog"
21+
"net"
2022
"net/netip"
23+
"strconv"
2124
"strings"
2225
"sync"
2326
"time"
2427

2528
"github.com/frostbyte73/core"
2629
"golang.org/x/exp/maps"
2730

31+
esip "github.com/emiago/sipgo/sip"
32+
2833
"github.com/livekit/protocol/livekit"
2934
"github.com/livekit/protocol/logger"
3035
"github.com/livekit/protocol/rpc"
@@ -175,32 +180,168 @@ func (c *Client) getActiveCall(tag LocalTag) *outboundCall {
175180
return c.activeCalls[tag]
176181
}
177182

183+
func setUriTransport(p *sip.Uri, tr livekit.SIPTransport) {
184+
if tr != livekit.SIPTransport_SIP_TRANSPORT_AUTO {
185+
p.UriParams.Add("transport", tr.Name())
186+
}
187+
}
188+
189+
func buildLegacyURI(user, addr string, tr livekit.SIPTransport) (*sip.Uri, error) {
190+
if user == "" {
191+
return nil, fmt.Errorf("number must be set")
192+
} else if strings.Contains(user, "@") {
193+
return nil, fmt.Errorf("should be a phone number or SIP user, not a full SIP URI")
194+
}
195+
if addr == "" {
196+
return nil, fmt.Errorf("address must be set")
197+
}
198+
if strings.HasPrefix(addr, "sip:") || strings.HasPrefix(addr, "sips:") {
199+
return nil, fmt.Errorf("address must be a hostname without 'sip:' prefix")
200+
} else if strings.Contains(addr, "transport=") {
201+
return nil, fmt.Errorf("legacy address must not contain parameters; use transport field")
202+
} else if strings.ContainsAny(addr, ";=") {
203+
return nil, fmt.Errorf("legacy address must not contain parameters")
204+
}
205+
p := &sip.Uri{Scheme: "sip"}
206+
setUriTransport(p, tr)
207+
208+
p.User = user
209+
if host, sport, err := net.SplitHostPort(addr); err == nil && sport != "" {
210+
p.Host = host
211+
p.Port, err = strconv.Atoi(sport)
212+
if err != nil {
213+
return nil, fmt.Errorf("invalid port in hostname: %q", sport)
214+
}
215+
} else {
216+
p.Host = addr
217+
}
218+
return p, nil
219+
}
220+
221+
func buildRawURI(raw string, tr livekit.SIPTransport) (*sip.Uri, error) {
222+
p := &sip.Uri{Scheme: "sip"}
223+
setUriTransport(p, tr)
224+
if err := esip.ParseUri(raw, p); err != nil {
225+
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "invalid request URI")
226+
}
227+
return p, nil
228+
}
229+
230+
func buildValuesURI(u *livekit.SIPUri, tr livekit.SIPTransport) (*sip.Uri, error) {
231+
if tr != u.Transport {
232+
if u.Transport == livekit.SIPTransport_SIP_TRANSPORT_AUTO {
233+
tr = tr
234+
} else if tr == livekit.SIPTransport_SIP_TRANSPORT_AUTO {
235+
tr = u.Transport
236+
} else {
237+
return nil, fmt.Errorf("different transports specified: %v vs %v", tr, u.Transport)
238+
}
239+
}
240+
p := &sip.Uri{Scheme: "sip"}
241+
setUriTransport(p, tr)
242+
if u.User == "" {
243+
return nil, fmt.Errorf("username or number must be set")
244+
}
245+
if u.Host == "" && u.Ip == "" {
246+
return nil, fmt.Errorf("host or ip must be set")
247+
}
248+
p.User = u.User
249+
p.Host = u.Host
250+
if p.Host == "" {
251+
p.Host = u.Ip
252+
}
253+
if _, sport, err := net.SplitHostPort(p.Host); err == nil && sport != "" {
254+
return nil, fmt.Errorf("host or ip must not contain port")
255+
}
256+
p.Port = int(u.Port)
257+
return p, nil
258+
}
259+
260+
func buildRequestURI(u *livekit.SIPRequestDest, legacyUser, legacyAddr string, tr livekit.SIPTransport) (*sip.Uri, error) {
261+
if u == nil {
262+
return buildLegacyURI(legacyUser, legacyAddr, tr)
263+
}
264+
switch u := u.Uri.(type) {
265+
default:
266+
case *livekit.SIPRequestDest_Raw:
267+
return buildRawURI(u.Raw, tr)
268+
case *livekit.SIPRequestDest_Values:
269+
return buildValuesURI(u.Values, tr)
270+
}
271+
return nil, fmt.Errorf("invalid request URI type")
272+
}
273+
274+
func buildFromToURI(u *livekit.SIPNamedDest, legacyUser, legacyAddr string, tr livekit.SIPTransport) (*sip.Uri, error) {
275+
if u == nil {
276+
return buildLegacyURI(legacyUser, legacyAddr, tr)
277+
}
278+
switch u := u.Uri.(type) {
279+
default:
280+
case *livekit.SIPNamedDest_Raw:
281+
return buildRawURI(u.Raw, tr)
282+
case *livekit.SIPNamedDest_Values:
283+
return buildValuesURI(u.Values, tr)
284+
}
285+
return nil, fmt.Errorf("invalid URI type")
286+
}
287+
288+
func buildFromHeader(u *livekit.SIPNamedDest, legacyName *string, legacyUser, legacyAddr string, tr livekit.SIPTransport) (*sip.FromHeader, error) {
289+
su, err := buildFromToURI(u, legacyUser, legacyAddr, tr)
290+
if err != nil {
291+
return nil, err
292+
}
293+
h := &sip.FromHeader{
294+
Address: *su,
295+
}
296+
if u != nil {
297+
h.DisplayName = u.DisplayName
298+
} else if legacyName != nil {
299+
h.DisplayName = *legacyName
300+
} else {
301+
// Nothing specified, preserve legacy behavior
302+
h.DisplayName = su.User
303+
}
304+
return h, nil
305+
}
306+
307+
func buildToHeader(u *livekit.SIPNamedDest, legacyUser, legacyAddr string, tr livekit.SIPTransport) (*sip.ToHeader, error) {
308+
su, err := buildFromToURI(u, legacyUser, legacyAddr, tr)
309+
if err != nil {
310+
return nil, err
311+
}
312+
h := &sip.ToHeader{
313+
Address: *su,
314+
}
315+
if u != nil {
316+
h.DisplayName = u.DisplayName
317+
}
318+
return h, nil
319+
}
320+
321+
func buildOutboundHeaders(req *rpc.InternalCreateSIPParticipantRequest) (*sip.Uri, *sip.FromHeader, *sip.ToHeader, error) {
322+
uri, err := buildRequestURI(req.SipRequestUri, req.CallTo, req.Address, req.Transport)
323+
if err != nil {
324+
return nil, nil, nil, psrpc.NewError(psrpc.InvalidArgument, fmt.Errorf("invalid request URI: %w", err))
325+
}
326+
to, err := buildToHeader(req.SipToHeader, req.CallTo, req.Address, req.Transport)
327+
if err != nil {
328+
return nil, nil, nil, psrpc.NewError(psrpc.InvalidArgument, fmt.Errorf("invalid To header: %w", err))
329+
}
330+
from, err := buildFromHeader(req.SipFromHeader, req.DisplayName, req.Number, req.Hostname, req.Transport)
331+
if err != nil {
332+
return nil, nil, nil, psrpc.NewError(psrpc.InvalidArgument, fmt.Errorf("invalid From header: %w", err))
333+
}
334+
return uri, from, to, nil
335+
}
336+
178337
func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCreateSIPParticipantRequest) (resp *rpc.InternalCreateSIPParticipantResponse, retErr error) {
179338
if c.mon.Health() != stats.HealthOK {
180339
return nil, siperrors.ErrUnavailable
181340
}
182341
req.Upgrade()
183-
if req.CallTo == "" {
184-
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "call-to number must be set")
185-
} else if req.Address == "" {
186-
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "trunk adresss must be set")
187-
} else if req.Number == "" {
188-
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "trunk outbound number must be set")
189-
} else if req.RoomName == "" {
342+
if req.RoomName == "" {
190343
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "room name must be set")
191344
}
192-
if strings.Contains(req.CallTo, "@") {
193-
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "call_to should be a phone number or SIP user, not a full SIP URI")
194-
}
195-
if strings.HasPrefix(req.Address, "sip:") || strings.HasPrefix(req.Address, "sips:") {
196-
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "address must be a hostname without 'sip:' prefix")
197-
}
198-
if strings.Contains(req.Address, "transport=") {
199-
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "address must not contain parameters; use transport field")
200-
}
201-
if strings.ContainsAny(req.Address, ";=") {
202-
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "address must not contain parameters")
203-
}
204345
log := c.log
205346
if req.ProjectId != "" {
206347
log = log.WithValues("projectID", req.ProjectId)
@@ -212,22 +353,28 @@ func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCrea
212353
if err != nil {
213354
return nil, err
214355
}
356+
uri, from, to, err := buildOutboundHeaders(req)
357+
if err != nil {
358+
return nil, err
359+
}
215360
tid := traceid.FromGUID(req.SipCallId)
216361
log = log.WithValues(
217362
"callID", req.SipCallId,
218363
"traceID", tid.String(),
219364
"room", req.RoomName,
220365
"participant", req.ParticipantIdentity,
221366
"participantName", req.ParticipantName,
222-
"fromHost", req.Hostname,
223-
"fromUser", req.Number,
224-
"toHost", req.Address,
225-
"toUser", req.CallTo,
367+
"fromHost", from.Address.Host,
368+
"fromUser", from.Address.User,
369+
"toHost", to.Address.Host,
370+
"toUser", to.Address.User,
371+
"reqHost", uri.Host,
372+
"reqUser", uri.User,
226373
"direction", "outbound",
227374
)
228375

229376
req.ParticipantAttributes = maps.Clone(req.ParticipantAttributes) // shallow clone - string/string map. Needed to avoid mutating psrpc req
230-
state := NewCallState(c.getIOClient(req.ProjectId), c.createSIPCallInfo(req))
377+
state := NewCallState(c.getIOClient(req.ProjectId), c.createSIPCallInfo(uri, from, to, req))
231378

232379
defer func() {
233380
state.Update(ctx, func(info *livekit.SIPCallInfo) {
@@ -255,11 +402,10 @@ func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCrea
255402
},
256403
}
257404
sipConf := sipOutboundConfig{
258-
address: req.Address,
259405
transport: req.Transport,
260-
host: req.Hostname,
261-
from: req.Number,
262-
to: req.CallTo,
406+
uri: uri,
407+
from: from,
408+
to: to,
263409
user: req.Username,
264410
pass: req.Password,
265411
dtmf: req.Dtmf,
@@ -273,7 +419,6 @@ func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCrea
273419
enabledFeatures: req.EnabledFeatures,
274420
featureFlags: req.FeatureFlags,
275421
mediaConfig: mconf,
276-
displayName: req.DisplayName,
277422
}
278423
log.Infow("Creating SIP participant")
279424
call, err := c.newCall(ctx, tid, c.conf, log, LocalTag(req.SipCallId), roomConf, sipConf, state, req.ProjectId)
@@ -299,13 +444,10 @@ func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCrea
299444
return info, nil
300445
}
301446

302-
func (c *Client) createSIPCallInfo(req *rpc.InternalCreateSIPParticipantRequest) *livekit.SIPCallInfo {
303-
toUri := CreateURIFromUserAndAddress(req.CallTo, req.Address, TransportFrom(req.Transport))
304-
fromiUri := URI{
305-
User: req.Number,
306-
Host: req.Hostname,
307-
Addr: netip.AddrPortFrom(c.sconf.SignalingIP, uint16(c.conf.SIPPort)),
308-
}
447+
func (c *Client) createSIPCallInfo(uri *sip.Uri, from *sip.FromHeader, to *sip.ToHeader, req *rpc.InternalCreateSIPParticipantRequest) *livekit.SIPCallInfo {
448+
toUri := ConvertURI(&to.Address)
449+
fromUri := ConvertURI(&from.Address)
450+
fromUri.Addr = netip.AddrPortFrom(c.sconf.SignalingIP, uint16(c.conf.SIPPort))
309451

310452
callInfo := &livekit.SIPCallInfo{
311453
CallId: req.SipCallId,
@@ -316,7 +458,7 @@ func (c *Client) createSIPCallInfo(req *rpc.InternalCreateSIPParticipantRequest)
316458
ParticipantAttributes: req.ParticipantAttributes,
317459
CallDirection: livekit.SIPCallDirection_SCD_OUTBOUND,
318460
ToUri: toUri.ToSIPUri(),
319-
FromUri: fromiUri.ToSIPUri(),
461+
FromUri: fromUri.ToSIPUri(),
320462
CreatedAtNs: time.Now().UnixNano(),
321463
MediaEncryption: req.MediaEncryption.String(),
322464
EnabledFeatures: req.EnabledFeatures,

0 commit comments

Comments
 (0)