From 70d086e9d00504cb726f496e2d1744c8e38af762 Mon Sep 17 00:00:00 2001 From: "Omar.A" <58332033+civilcoder55@users.noreply.github.com> Date: Sun, 22 Feb 2026 17:49:15 +0200 Subject: [PATCH] feat: Add allow_custom_from_hostname config option to control custom From header hostname https://github.com/livekit/protocol/pull/1419 --- README.md | 1 + pkg/config/config.go | 3 ++ pkg/sip/client.go | 3 ++ pkg/sip/outbound_test.go | 82 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+) diff --git a/README.md b/README.md index 936ba076..501b7f7f 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ prometheus_port: port used to collect prometheus metrics. Used for autoscaling log_level: debug, info, warn, or error (default info) sip_port: port to listen and send SIP traffic (default 5060) rtp_port: port to listen and send RTP traffic (default 10000-20000) +allow_custom_from_hostname: if true, allows the From header to be set to a custom hostname retrieved from the outbound trunk (default false) ``` The config file can be added to a mounted volume with its location passed in the SIP_CONFIG_FILE env var, or its body can be passed in the SIP_CONFIG_BODY env var. diff --git a/pkg/config/config.go b/pkg/config/config.go index 3d2845b7..5513b1bb 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -124,6 +124,9 @@ type Config struct { // InboundWaitACK forces SIP to wait for an ACK to 200 OK before proceeding with the call. InboundWaitACK bool `yaml:"inbound_wait_ack"` } `yaml:"experimental"` + + // AllowCustomFromHostname allows the FROM header to be set to a custom hostname. + AllowCustomFromHostname bool `yaml:"allow_custom_from_hostname"` } func NewConfig(confString string) (*Config, error) { diff --git a/pkg/sip/client.go b/pkg/sip/client.go index 9e9dea33..0969aab8 100644 --- a/pkg/sip/client.go +++ b/pkg/sip/client.go @@ -172,6 +172,9 @@ func (c *Client) CreateSIPParticipant(ctx context.Context, req *rpc.InternalCrea } func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCreateSIPParticipantRequest) (resp *rpc.InternalCreateSIPParticipantResponse, retErr error) { + if c.conf.AllowCustomFromHostname { + req.Hostname = req.FromHostname + } if c.mon.Health() != stats.HealthOK { return nil, siperrors.ErrUnavailable } diff --git a/pkg/sip/outbound_test.go b/pkg/sip/outbound_test.go index ec0893ca..9313fff8 100644 --- a/pkg/sip/outbound_test.go +++ b/pkg/sip/outbound_test.go @@ -123,3 +123,85 @@ func TestOutboundRouteHeaderWithRecordRoute(t *testing.T) { cancel() } +func TestCreateSIPParticipant_CustomFromHostname(t *testing.T) { + t.Run("when allowCustomFromHostname is false", func(t *testing.T) { + customFromHost := "custom-from.example.com" + + client := NewOutboundTestClient(t, TestClientConfig{}) + client.conf.AllowCustomFromHostname = false + + req := MinimalCreateSIPParticipantRequest() + req.FromHostname = customFromHost + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + _, _ = client.CreateSIPParticipant(ctx, req) + }() + + var sipClient *testSIPClient + select { + case sipClient = <-createdClients: + t.Cleanup(func() { _ = sipClient.Close() }) + case <-time.After(100 * time.Millisecond): + require.Fail(t, "expected client to be created") + return + } + + var tr *transactionRequest + select { + case tr = <-sipClient.transactions: + t.Cleanup(func() { tr.transaction.Terminate() }) + case <-time.After(500 * time.Millisecond): + require.Fail(t, "expected transaction request to be created") + return + } + + require.NotNil(t, tr) + require.NotNil(t, tr.req) + require.NotNil(t, tr.req.From()) + require.NotEqual(t, customFromHost, tr.req.From().Address.Host) + + require.NoError(t, tr.transaction.SendResponse(sip.NewResponseFromRequest(tr.req, sip.StatusBusyHere, "Busy Here", nil))) + }) + + t.Run("when allowCustomFromHostname is true", func(t *testing.T) { + customFromHost := "custom-from.example.com" + + client := NewOutboundTestClient(t, TestClientConfig{}) + client.conf.AllowCustomFromHostname = true + + req := MinimalCreateSIPParticipantRequest() + req.FromHostname = customFromHost + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + _, _ = client.CreateSIPParticipant(ctx, req) + }() + + var sipClient *testSIPClient + select { + case sipClient = <-createdClients: + t.Cleanup(func() { _ = sipClient.Close() }) + case <-time.After(100 * time.Millisecond): + require.Fail(t, "expected client to be created") + return + } + + var tr *transactionRequest + select { + case tr = <-sipClient.transactions: + t.Cleanup(func() { tr.transaction.Terminate() }) + case <-time.After(500 * time.Millisecond): + require.Fail(t, "expected transaction request to be created") + return + } + + require.NotNil(t, tr) + require.NotNil(t, tr.req) + require.NotNil(t, tr.req.From()) + require.Equal(t, customFromHost, tr.req.From().Address.Host) + require.NoError(t, tr.transaction.SendResponse(sip.NewResponseFromRequest(tr.req, sip.StatusBusyHere, "Busy Here", nil))) + }) +}