diff --git a/pkg/config/config.go b/pkg/config/config.go index 8192660d..369c6a5a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -36,8 +36,9 @@ import ( ) const ( - DefaultSIPPort int = 5060 - DefaultSIPPortTLS int = 5061 + DefaultSIPPort int = 5060 + DefaultSIPPortTLS int = 5061 + DefaultRingingTimeoutStatus int = 486 // Busy Here ) var ( @@ -94,6 +95,7 @@ type Config struct { 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') + RingingTimeoutStatus int `yaml:"ringing_timeout_status"` // status when a call rings out, 3xx-6xx (default '486') TCP *TCPConfig `yaml:"tcp"` TLS *TLSConfig `yaml:"tls"` RTPPort rtcconfig.PortRange `yaml:"rtp_port"` @@ -208,6 +210,12 @@ func (c *Config) Init() error { if c.MaxCpuUtilization <= 0 || c.MaxCpuUtilization > 1 { c.MaxCpuUtilization = 0.9 } + if c.RingingTimeoutStatus == 0 { + c.RingingTimeoutStatus = DefaultRingingTimeoutStatus + } else if c.RingingTimeoutStatus < 300 || c.RingingTimeoutStatus > 699 { + return psrpc.NewErrorf(psrpc.InvalidArgument, + "ringing_timeout_status must be a SIP failure status between 300 and 699, got %d", c.RingingTimeoutStatus) + } if err := c.InitLogger(); err != nil { return err diff --git a/pkg/sip/inbound.go b/pkg/sip/inbound.go index 8b7f4f77..cd29542d 100644 --- a/pkg/sip/inbound.go +++ b/pkg/sip/inbound.go @@ -1226,7 +1226,11 @@ func (c *inboundCall) waitSubscribe(ctx context.Context, timeout time.Duration) c.close(ctx, end) return false, psrpc.NewErrorf(psrpc.Canceled, "rpc terminated the call") case <-timer.C: - c.closeWithTerm(ctx, stats.ServerError("cannot-subscribe")) + c.close(ctx, EndCall{ + Status: callDropped, + Term: stats.ServerError("cannot-subscribe"), + Code: sip.StatusCode(c.s.conf.RingingTimeoutStatus), + }) return false, psrpc.NewErrorf(psrpc.DeadlineExceeded, "room subscription timed out") case <-c.lkRoom.Subscribed(): return true, nil @@ -1368,6 +1372,9 @@ func (c *inboundCall) close(ctx context.Context, end EndCall) { Status: "Request Terminated", } } + if end.Code != 0 { + result = Result{Code: end.Code} + } log := c.log().WithValues("status", result.Code, "result", string(end.Term.Result), "reason", end.Term.Reason) defer func() { c.stats.Update() diff --git a/pkg/sip/protocol.go b/pkg/sip/protocol.go index b3ee1f9b..44727755 100644 --- a/pkg/sip/protocol.go +++ b/pkg/sip/protocol.go @@ -63,6 +63,7 @@ type EndCall struct { Term stats.Termination Reason livekit.DisconnectReason // disconnect reason for LiveKit participant Headers map[string]string // extra headers to send to SIP peer + Code sip.StatusCode // SIP status for an unanswered call; zero picks the default } var statusNamesMap = map[int]string{ diff --git a/pkg/sip/service_test.go b/pkg/sip/service_test.go index 7adbffcc..2e5774da 100644 --- a/pkg/sip/service_test.go +++ b/pkg/sip/service_test.go @@ -995,6 +995,51 @@ func TestCANCELSendsBothResponses(t *testing.T) { } } +// A call that rings for the whole ringing timeout without the room ever subscribing +// is rejected with ringing_timeout_status, or 486 Busy Here when it is not configured. +func TestRingingTimeoutStatus(t *testing.T) { + cases := []struct { + name string + configed int + expected sip.StatusCode + }{ + {name: "unconfigured", configed: 0, expected: sip.StatusBusyHere}, + {name: "configured", configed: 480, expected: sip.StatusTemporarilyUnavailable}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + h := &TestHandler{ + DispatchCallFunc: func(ctx context.Context, info *CallInfo) CallDispatch { + identity := fmt.Sprintf("test-participant-%s", info.Call.SipCallId) + return CallDispatch{ + Result: DispatchAccept, + RingingTimeout: 500 * time.Millisecond, + Room: RoomConfig{ + RoomName: "test-room", + Participant: ParticipantConfig{ + Identity: identity, + Name: identity, + }, + }, + } + }, + } + st := NewServiceTest(t, &serviceTestConfig{ + GetRoom: newTestRoomConfig(&testRoomConfig{ringForever: true}), + Handler: h, + }) + st.Server.conf.RingingTimeoutStatus = c.configed + + call := newTestCall(st.TestUA, false) + req, _, err := call.Invite(nil) + require.NoError(t, err) + + resp := st.TestUA.TransactionRequest(t, req, true) + require.Equal(t, c.expected, resp.StatusCode) + }) + } +} + // TestSameCallIDForAuthFlow verifies that the same LiveKit call ID is assigned to both // the initial INVITE (without auth) and the subsequent INVITE (with auth) func TestSameCallIDForAuthFlow(t *testing.T) { diff --git a/pkg/sip/signaling_test.go b/pkg/sip/signaling_test.go index 8744f46f..24c9218d 100644 --- a/pkg/sip/signaling_test.go +++ b/pkg/sip/signaling_test.go @@ -423,6 +423,7 @@ type serviceTest struct { type serviceTestConfig struct { GetRoom GetRoomFunc + Handler Handler } func NewServiceTest(t *testing.T, options *serviceTestConfig) *serviceTest { @@ -434,6 +435,9 @@ func NewServiceTest(t *testing.T, options *serviceTestConfig) *serviceTest { if options.GetRoom == nil { options.GetRoom = newTestRoomConfig(nil) } + if options.Handler == nil { + options.Handler = &TestHandler{} + } sipPort := rand.Intn(testPortSIPMax-testPortSIPMin) + testPortSIPMin loopback := netip.MustParseAddr("127.0.0.1") @@ -492,7 +496,7 @@ func NewServiceTest(t *testing.T, options *serviceTestConfig) *serviceTest { MediaIP: loopback, } - handler := &TestHandler{} + handler := options.Handler err = srv.Start(nil, sconf, nil, cli.OnRequest) require.NoError(t, err)