diff --git a/pkg/sip/outbound.go b/pkg/sip/outbound.go index 98fe2914..52d6990e 100644 --- a/pkg/sip/outbound.go +++ b/pkg/sip/outbound.go @@ -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) @@ -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 } @@ -889,7 +888,7 @@ 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) @@ -897,7 +896,6 @@ func (c *sipOutbound) attemptInvite(ctx context.Context, callID sip.CallIDHeader req.RemoveHeader("Call-ID") req.AppendHeader(&callID) - req.SetDestination(dest) req.SetBody(offer) req.AppendHeader(to) req.AppendHeader(c.from) diff --git a/test/integration/sip_test.go b/test/integration/sip_test.go index ab4f1ad3..3051ef1c 100644 --- a/test/integration/sip_test.go +++ b/test/integration/sip_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "math/rand" + "net" "net/netip" "os" "strconv" @@ -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("", 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") + } +}