Skip to content
Merged
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: 2 additions & 4 deletions pkg/sip/outbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,6 @@ func (c *sipOutbound) Invite(ctx context.Context, to URI, user, pass string, hea
defer c.mu.Unlock()
toHeader := &sip.ToHeader{Address: *to.GetURI()}

dest := to.GetDest()
c.callID = guid.HashedID(fmt.Sprintf("%s-%s", string(c.id), toHeader.Address.String()))
c.log = c.log.WithValues("sipCallID", c.callID)

Expand All @@ -779,7 +778,7 @@ authLoop:
if try >= 5 {
return nil, fmt.Errorf("max auth retry attemps reached")
}
req, resp, err = c.attemptInvite(ctx, sip.CallIDHeader(c.callID), dest, toHeader, sdpOffer, authHeaderRespName, authHeader, sipHeaders, setState)
req, resp, err = c.attemptInvite(ctx, sip.CallIDHeader(c.callID), toHeader, sdpOffer, authHeaderRespName, authHeader, sipHeaders, setState)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -889,15 +888,14 @@ func (c *sipOutbound) AckInviteOK(ctx context.Context) error {
return c.c.sipCli.WriteRequest(sip.NewAckRequest(c.invite, c.inviteOk, nil))
}

func (c *sipOutbound) attemptInvite(ctx context.Context, callID sip.CallIDHeader, dest string, to *sip.ToHeader, offer []byte, authHeaderName, authHeader string, headers Headers, setState sipRespFunc) (*sip.Request, *sip.Response, error) {
func (c *sipOutbound) attemptInvite(ctx context.Context, callID sip.CallIDHeader, to *sip.ToHeader, offer []byte, authHeaderName, authHeader string, headers Headers, setState sipRespFunc) (*sip.Request, *sip.Response, error) {
ctx, span := tracer.Start(ctx, "sipOutbound.attemptInvite")
defer span.End()
req := sip.NewRequest(sip.INVITE, to.Address)
c.setCSeq(req)
req.RemoveHeader("Call-ID")
req.AppendHeader(&callID)

req.SetDestination(dest)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed, it now defaults to req.Destination(), which already respects Route headers.
This looks like it was introduced in the first iteration of outbound calling altogether.
The one possible downside I see to this is we will potentially re-resolve if DNS cache expires exactly in-between attempt 1 and any following re-transmit/re-auth attempt, causing an extra bit of latency.
However. I think this is still the right thing to do here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: Denys recalled the possible reason for this was to avoid DNS re-resolution mid call, possibly arriving to a different host without the previous INVITE credentials.

That is absolutely a valid concern,m but not for this change - req.SetDestination() simply stores a string, so we're subject to DNS re-resolution today, with SetDestination or without.

req.SetBody(offer)
req.AppendHeader(to)
req.AppendHeader(c.from)
Expand Down
113 changes: 113 additions & 0 deletions test/integration/sip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log/slog"
"math/rand"
"net"
"net/netip"
"os"
"strconv"
Expand Down Expand Up @@ -945,3 +946,115 @@ func TestSIPOutbound(t *testing.T) {
})
}
}

func TestSIPOutboundRouteHeader(t *testing.T) {
// Test that when a Route header is specified in CreateSIPParticipant request,
// the SIP message is sent to the route header target instead of the request URI.

// Set up two LiveKit servers and SIP servers
lkOut := runLiveKit(t)
lkIn := runLiveKit(t)
srvOut := runSIPServer(t, lkOut)
srvIn := runSIPServer(t, lkIn)

const (
roomIn = "inbound"
userName = "test-user"
userPass = "test-pass"
meta = `{"test":true}`
)

// Configure Trunk for inbound server
trunkIn := srvIn.CreateTrunkIn(t, &livekit.SIPInboundTrunkInfo{
Name: "Test In",
Numbers: []string{serverNumber},
AuthUsername: userName,
AuthPassword: userPass,
})
t.Cleanup(func() {
srvIn.DeleteTrunk(t, trunkIn)
})

ruleIn := srvIn.CreateDirectDispatch(t, roomIn, "", meta, nil)
t.Cleanup(func() {
srvIn.DeleteDispatch(t, ruleIn)
})

// Create a mock SIP server that will receive the route header target
// This server should be different from the request URI
routeTarget := "127.0.0.1:5061" // Different from srvIn.Address

// Set up a mock SIP server to capture the route header target
// We'll create a simple UDP server that can receive and log the SIP message
routeServer, err := net.ListenPacket("udp", routeTarget)
require.NoError(t, err)
defer routeServer.Close()

// Channel to capture received messages
receivedMessages := make(chan string, 10)

// Start a goroutine to listen for messages on the route target
go func() {
buffer := make([]byte, 1024)
for {
n, addr, err := routeServer.ReadFrom(buffer)
if err != nil {
return
}
message := string(buffer[:n])
receivedMessages <- message
t.Logf("Route server received message from %s: %s", addr, message)
}
}()

// Configure Trunk for outbound server with the request URI (different from route target)
trunkOut := srvOut.CreateTrunkOut(t, &livekit.SIPOutboundTrunkInfo{
Name: "Test Out",
Numbers: []string{clientNumber},
Address: srvIn.Address, // This will be the request URI
Transport: livekit.SIPTransport_SIP_TRANSPORT_UDP,
AuthUsername: userName,
AuthPassword: userPass,
})
t.Cleanup(func() {
srvOut.DeleteTrunk(t, trunkOut)
})

// Create the outbound SIP participant with a Route header
// The Route header should point to a different destination than the request URI
routeHeader := fmt.Sprintf("<sip:%s;lr>", routeTarget)

// Create the SIP participant with the Route header
// We need to pass the Route header in the headers map
headers := map[string]string{
"Route": routeHeader,
}

// Create the outbound SIP participant with the Route header
t.Logf("Testing Route header: %s", routeHeader)
t.Logf("Request URI target: %s", srvIn.Address)
t.Logf("Route header target: %s", routeTarget)

// Create the outbound SIP participant
r := lkOut.CreateSIPParticipant(t, &livekit.CreateSIPParticipantRequest{
SipTrunkId: trunkOut,
SipCallTo: serverNumber,
RoomName: "outbound",
ParticipantIdentity: "siptest_outbound",
ParticipantName: "Outbound Call",
ParticipantMetadata: `{"test":true, "dir": "out"}`,
Headers: headers, // This is the key - passing the Route header
})
t.Logf("outbound call ID: %s", r.SipCallId)

// Wait a bit to see if any messages are received on the route target
select {
case msg := <-receivedMessages:
t.Logf("Received message on route target: %s", msg)
// If we receive a message, it means the Route header is working
require.Contains(t, msg, "INVITE", "Should receive INVITE message on route target")
t.Log("SUCCESS: Route header is working - message was sent to route target instead of request URI")
case <-time.After(10 * time.Second):
t.Fatal("No message received on route target within timeout - Route header processing is not working correctly")
}
}
Loading