Skip to content

Commit 050eeba

Browse files
authored
Reuse the same ID for both auth-less and auth-ful INVITEs (#488)
1 parent 91e2088 commit 050eeba

5 files changed

Lines changed: 286 additions & 60 deletions

File tree

pkg/sip/inbound.go

Lines changed: 81 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"github.com/livekit/protocol/rpc"
4343
lksip "github.com/livekit/protocol/sip"
4444
"github.com/livekit/protocol/tracer"
45+
"github.com/livekit/protocol/utils"
4546
"github.com/livekit/protocol/utils/traceid"
4647
"github.com/livekit/psrpc"
4748
lksdk "github.com/livekit/server-sdk-go/v2"
@@ -64,6 +65,8 @@ const (
6465
inviteOKRetryAttempts = 5
6566
inviteOKRetryAttemptsNoACK = 2
6667
inviteOkAckLateTimeout = inviteOkRetryIntervalMax
68+
69+
inviteCredentialValidity = 60 * time.Minute // Allow reuse of credentials for 1h
6770
)
6871

6972
var errNoACK = errors.New("no ACK received for 200 OK")
@@ -134,23 +137,50 @@ func (s *Server) getCallInfo(id string) *inboundCallInfo {
134137
return c
135138
}
136139

137-
func (s *Server) getInvite(sipCallID string) *inProgressInvite {
138-
s.imu.Lock()
139-
defer s.imu.Unlock()
140-
for i := range s.inProgressInvites {
141-
if s.inProgressInvites[i].sipCallID == sipCallID {
142-
return s.inProgressInvites[i]
140+
func (s *Server) cleanupInvites() {
141+
ticker := time.NewTicker(5 * time.Minute) // Periodic cleanup every 5 minutes
142+
defer ticker.Stop()
143+
for {
144+
select {
145+
case <-s.closing.Watch():
146+
return
147+
case <-ticker.C:
148+
s.imu.Lock()
149+
for it := s.inviteTimeoutQueue.IterateRemoveAfter(inviteCredentialValidity); it.Next(); {
150+
key := it.Item().Value
151+
delete(s.inProgressInvites, key)
152+
}
153+
s.imu.Unlock()
143154
}
144155
}
145-
if len(s.inProgressInvites) >= digestLimit {
146-
s.inProgressInvites = s.inProgressInvites[1:]
156+
}
157+
158+
func (s *Server) getInvite(sipCallID, toTag, fromTag string) *inProgressInvite {
159+
key := dialogKey{
160+
sipCallID: sipCallID,
161+
toTag: toTag,
162+
fromTag: fromTag,
163+
}
164+
165+
s.imu.RLock()
166+
is, exists := s.inProgressInvites[key]
167+
s.imu.RUnlock()
168+
if !exists {
169+
s.imu.Lock()
170+
is, exists = s.inProgressInvites[key]
171+
if !exists {
172+
is = &inProgressInvite{sipCallID: sipCallID, timeoutLink: utils.TimeoutQueueItem[dialogKey]{Value: key}}
173+
s.inProgressInvites[key] = is
174+
}
175+
s.imu.Unlock()
147176
}
148-
is := &inProgressInvite{sipCallID: sipCallID}
149-
s.inProgressInvites = append(s.inProgressInvites, is)
177+
178+
// Always reset the timeout link, whether just created or not
179+
s.inviteTimeoutQueue.Reset(&is.timeoutLink)
150180
return is
151181
}
152182

153-
func (s *Server) handleInviteAuth(tid traceid.ID, log logger.Logger, req *sip.Request, tx sip.ServerTransaction, from, username, password string) (ok bool) {
183+
func (s *Server) handleInviteAuth(tid traceid.ID, log logger.Logger, req *sip.Request, tx sip.ServerTransaction, from, username, password string, inviteState *inProgressInvite) (ok bool) {
154184
log = log.WithValues(
155185
"username", username,
156186
"passwordHash", hashPassword(password),
@@ -171,14 +201,6 @@ func (s *Server) handleInviteAuth(tid traceid.ID, log logger.Logger, req *sip.Re
171201
_ = tx.Respond(sip.NewResponseFromRequest(req, 100, "Processing", nil))
172202
}
173203

174-
// Extract SIP Call ID for tracking in-progress invites
175-
sipCallID := ""
176-
if h := req.CallID(); h != nil {
177-
sipCallID = h.Value()
178-
}
179-
inviteState := s.getInvite(sipCallID)
180-
log = log.WithValues("inviteStateSipCallID", sipCallID)
181-
182204
h := req.GetHeader("Proxy-Authorization")
183205
if h == nil {
184206
inviteState.challenge = digest.Challenge{
@@ -230,7 +252,6 @@ func (s *Server) handleInviteAuth(tid traceid.ID, log logger.Logger, req *sip.Re
230252
// Check if we have a valid challenge state
231253
if inviteState.challenge.Realm == "" {
232254
log.Warnw("No challenge state found for authentication attempt", errors.New("missing challenge state"),
233-
"sipCallID", sipCallID,
234255
"expectedRealm", UserAgent,
235256
)
236257
_ = tx.Respond(sip.NewResponseFromRequest(req, 401, "Bad credentials", nil))
@@ -305,20 +326,18 @@ func (s *Server) processInvite(req *sip.Request, tx sip.ServerTransaction) (retE
305326
s.log.Errorw("cannot parse source IP", err, "fromIP", src)
306327
return psrpc.NewError(psrpc.MalformedRequest, errors.Wrap(err, "cannot parse source IP"))
307328
}
308-
callID := lksip.NewCallID()
309-
tid := traceid.FromGUID(callID)
329+
sipCallID := legCallIDFromReq(req)
310330
tr := callTransportFromReq(req)
311331
legTr := legTransportFromReq(req)
312332
log := s.log.WithValues(
313-
"callID", callID,
314-
"traceID", tid.String(),
333+
"sipCallID", sipCallID,
315334
"fromIP", src.Addr(),
316335
"toIP", req.Destination(),
317336
"transport", tr,
318337
)
319338

320339
var call *inboundCall
321-
cc := s.newInbound(log, LocalTag(callID), s.ContactURI(legTr), req, tx, func(headers map[string]string) map[string]string {
340+
cc := s.newInbound(log, s.ContactURI(legTr), req, tx, func(headers map[string]string) map[string]string {
322341
c := call
323342
if c == nil || len(c.attrsToHdr) == 0 {
324343
return headers
@@ -331,25 +350,53 @@ func (s *Server) processInvite(req *sip.Request, tx sip.ServerTransaction) (retE
331350
})
332351
log = LoggerWithParams(log, cc)
333352
log = LoggerWithHeaders(log, cc)
334-
cc.log = log
335-
log.Infow("processing invite")
336353

337354
if err := cc.ValidateInvite(); err != nil {
355+
log.Errorw("invalid invite", err)
338356
if s.conf.HideInboundPort {
339357
cc.Drop()
340358
} else {
341359
cc.RespondAndDrop(sip.StatusBadRequest, "Bad request")
342360
}
343361
return psrpc.NewError(psrpc.InvalidArgument, errors.Wrap(err, "invite validation failed"))
344362
}
363+
364+
// Establish ID
365+
fromTag, _ := req.From().Params.Get("tag") // always exists, via ValidateInvite() check
366+
toParams := req.To().Params // To() always exists, via ValidateInvite() check
367+
if toParams == nil {
368+
toParams = sip.NewParams()
369+
req.To().Params = toParams
370+
}
371+
toTag, ok := toParams.Get("tag")
372+
if !ok {
373+
// No to-tag on the invite means we need to generate one per RFC 3261 section 12.
374+
// Generate a new to-tag early, to make sure both INVITES have the same ID.
375+
toTag = utils.NewGuid("")
376+
toParams.Add("tag", toTag)
377+
}
378+
inviteProgress := s.getInvite(sipCallID, toTag, fromTag)
379+
callID := inviteProgress.lkCallID
380+
if callID == "" {
381+
callID = lksip.NewCallID()
382+
inviteProgress.lkCallID = callID
383+
}
384+
cc.id = LocalTag(callID)
385+
tid := traceid.FromGUID(sipCallID)
386+
387+
log = log.WithValues("callID", callID)
388+
log = log.WithValues("traceID", tid.String())
389+
cc.log = log
390+
log.Infow("processing invite")
391+
345392
ctx, span := tracer.Start(ctx, "Server.onInvite")
346393
defer span.End()
347394

348395
s.cmu.RLock()
349-
existing := s.byCallID[cc.SIPCallID()]
396+
existing := s.byCallID[sipCallID]
350397
s.cmu.RUnlock()
351398
if existing != nil && existing.cc.InviteCSeq() < cc.InviteCSeq() {
352-
log.Infow("accepting reinvite", "sipCallID", existing.cc.ID(), "content-type", req.ContentType(), "content-length", req.ContentLength())
399+
log.Infow("accepting reinvite", "content-type", req.ContentType(), "content-length", req.ContentLength())
353400
existing.log().Infow("reinvite", "content-type", req.ContentType(), "content-length", req.ContentLength(), "cseq", cc.InviteCSeq())
354401
cc.AcceptAsKeepAlive(existing.cc.OwnSDP())
355402
return nil
@@ -376,7 +423,7 @@ func (s *Server) processInvite(req *sip.Request, tx sip.ServerTransaction) (retE
376423

377424
callInfo := &rpc.SIPCall{
378425
LkCallId: callID,
379-
SipCallId: cc.SIPCallID(),
426+
SipCallId: sipCallID,
380427
SourceIp: src.Addr().String(),
381428
Address: ToSIPUri("", cc.Address()),
382429
From: ToSIPUri("", from),
@@ -447,15 +494,15 @@ func (s *Server) processInvite(req *sip.Request, tx sip.ServerTransaction) (retE
447494
// We will send password request anyway, so might as well signal that the progress is made.
448495
cc.Processing()
449496
}
450-
s.getCallInfo(cc.SIPCallID()).countInvite(log, req)
451-
if !s.handleInviteAuth(tid, log, req, tx, from.User, r.Username, r.Password) {
497+
s.getCallInfo(sipCallID).countInvite(log, req)
498+
if !s.handleInviteAuth(tid, log, req, tx, from.User, r.Username, r.Password, inviteProgress) {
452499
cmon.InviteErrorShort("unauthorized")
453500
// handleInviteAuth will generate the SIP Response as needed
454501
return psrpc.NewErrorf(psrpc.PermissionDenied, "invalid credentials were provided")
455502
}
456503
// ok
457504
case AuthAccept:
458-
s.getCallInfo(cc.SIPCallID()).countInvite(log, req)
505+
s.getCallInfo(sipCallID).countInvite(log, req)
459506
// ok
460507
}
461508

@@ -1374,11 +1421,10 @@ func (c *inboundCall) transferCall(ctx context.Context, transferTo string, heade
13741421

13751422
}
13761423

1377-
func (s *Server) newInbound(log logger.Logger, id LocalTag, contact URI, invite *sip.Request, inviteTx sip.ServerTransaction, getHeaders setHeadersFunc) *sipInbound {
1424+
func (s *Server) newInbound(log logger.Logger, contact URI, invite *sip.Request, inviteTx sip.ServerTransaction, getHeaders setHeadersFunc) *sipInbound {
13781425
c := &sipInbound{
13791426
log: log,
13801427
s: s,
1381-
id: id,
13821428
invite: invite,
13831429
inviteTx: inviteTx,
13841430
legTr: legTransportFromReq(invite),

pkg/sip/outbound.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,7 @@ authLoop:
843843
if err != nil {
844844
return nil, fmt.Errorf("invalid challenge %q: %w", challengeStr, err)
845845
}
846-
toHeader := resp.To()
846+
toHeader = resp.To()
847847
if toHeader == nil {
848848
return nil, errors.New("no 'To' header on Response")
849849
}

pkg/sip/protocol.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,13 @@ func legTransportFromReq(req *sip.Request) Transport {
173173
return ""
174174
}
175175

176+
func legCallIDFromReq(req *sip.Request) string {
177+
if callID := req.CallID(); callID != nil {
178+
return callID.Value()
179+
}
180+
return ""
181+
}
182+
176183
func transportPort(c *config.Config, t Transport) int {
177184
if t == TransportTLS {
178185
if tc := c.TLS; tc != nil {

pkg/sip/server.go

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/livekit/protocol/livekit"
3636
"github.com/livekit/protocol/logger"
3737
"github.com/livekit/protocol/rpc"
38+
"github.com/livekit/protocol/utils"
3839
"github.com/livekit/protocol/utils/traceid"
3940
"github.com/livekit/sipgo"
4041
"github.com/livekit/sipgo/sip"
@@ -44,8 +45,7 @@ import (
4445
)
4546

4647
const (
47-
UserAgent = "LiveKit"
48-
digestLimit = 500
48+
UserAgent = "LiveKit"
4949
)
5050

5151
const (
@@ -127,18 +127,25 @@ type Handler interface {
127127
OnSessionEnd(ctx context.Context, callIdentifier *CallIdentifier, callInfo *livekit.SIPCallInfo, reason string)
128128
}
129129

130+
type dialogKey struct {
131+
sipCallID string
132+
toTag string
133+
fromTag string
134+
}
135+
130136
type Server struct {
131-
log logger.Logger
132-
mon *stats.Monitor
133-
region string
134-
sipSrv *sipgo.Server
135-
getIOClient GetIOInfoClient
136-
getRoom GetRoomFunc
137-
sipListeners []io.Closer
138-
sipUnhandled RequestHandler
139-
140-
imu sync.Mutex
141-
inProgressInvites []*inProgressInvite
137+
log logger.Logger
138+
mon *stats.Monitor
139+
region string
140+
sipSrv *sipgo.Server
141+
getIOClient GetIOInfoClient
142+
getRoom GetRoomFunc
143+
sipListeners []io.Closer
144+
sipUnhandled RequestHandler
145+
inviteTimeoutQueue utils.TimeoutQueue[dialogKey]
146+
147+
imu sync.RWMutex
148+
inProgressInvites map[dialogKey]*inProgressInvite
142149

143150
closing core.Fuse
144151
cmu sync.RWMutex
@@ -159,8 +166,10 @@ type Server struct {
159166
}
160167

161168
type inProgressInvite struct {
162-
sipCallID string
163-
challenge digest.Challenge
169+
sipCallID string
170+
challenge digest.Challenge
171+
lkCallID string // SCL_* LiveKit call ID assigned to this dialog
172+
timeoutLink utils.TimeoutQueueItem[dialogKey]
164173
}
165174

166175
type ServerOption func(s *Server)
@@ -178,15 +187,16 @@ func NewServer(region string, conf *config.Config, log logger.Logger, mon *stats
178187
log = logger.GetLogger()
179188
}
180189
s := &Server{
181-
log: log,
182-
conf: conf,
183-
region: region,
184-
mon: mon,
185-
getIOClient: getIOClient,
186-
getRoom: DefaultGetRoomFunc,
187-
byRemoteTag: make(map[RemoteTag]*inboundCall),
188-
byLocalTag: make(map[LocalTag]*inboundCall),
189-
byCallID: make(map[string]*inboundCall),
190+
log: log,
191+
conf: conf,
192+
region: region,
193+
mon: mon,
194+
getIOClient: getIOClient,
195+
getRoom: DefaultGetRoomFunc,
196+
inProgressInvites: make(map[dialogKey]*inProgressInvite),
197+
byRemoteTag: make(map[RemoteTag]*inboundCall),
198+
byLocalTag: make(map[LocalTag]*inboundCall),
199+
byCallID: make(map[string]*inboundCall),
190200
}
191201
for _, option := range options {
192202
option(s)
@@ -330,6 +340,9 @@ func (s *Server) Start(agent *sipgo.UserAgent, sc *ServiceConfig, tlsConf *tls.C
330340
}
331341
}
332342

343+
// Start the cleanup task
344+
go s.cleanupInvites()
345+
333346
return nil
334347
}
335348

0 commit comments

Comments
 (0)