From 85aeb72ee8dd4b97076a28350a7781c5a2ac04a2 Mon Sep 17 00:00:00 2001 From: Alexander Matveev Date: Sun, 9 Aug 2026 01:46:12 +0300 Subject: [PATCH 1/4] fix: bind SIP media sockets to signaling interface RTP previously always listened on 0.0.0.0, so on multi-homed hosts media could egress via a different IP than SIP signaling. Bind media sockets to SignalingIPLocal so announce and source IP stay aligned. --- pkg/sip/inbound.go | 1 + pkg/sip/media_port.go | 7 ++++++- pkg/sip/media_port_test.go | 16 ++++++++++++++++ pkg/sip/outbound.go | 1 + 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pkg/sip/inbound.go b/pkg/sip/inbound.go index 8b7f4f77..690376d8 100644 --- a/pkg/sip/inbound.go +++ b/pkg/sip/inbound.go @@ -1115,6 +1115,7 @@ func (c *inboundCall) runMediaConn(tid traceid.ID, offerData []byte, mconf *sipM logSignalChanges, _ = strconv.ParseBool(featureFlags[signalLoggingFeatureFlag]) mp, err := NewMediaPort(tid, c.log(), c.mon, &MediaOptions{ IP: c.s.sconf.MediaIP, + BindIP: c.s.sconf.SignalingIPLocal, Ports: conf.RTPPort, MediaTimeoutInitial: c.s.conf.MediaTimeoutInitial, MediaTimeout: mconf.MediaTimeout, diff --git a/pkg/sip/media_port.go b/pkg/sip/media_port.go index e6df1d85..40d9c1b6 100644 --- a/pkg/sip/media_port.go +++ b/pkg/sip/media_port.go @@ -356,6 +356,7 @@ type MediaConf struct { type MediaOptions struct { IP netip.Addr + BindIP netip.Addr // local interface for RTP sockets; zero binds 0.0.0.0 Ports rtcconfig.PortRange MediaTimeoutInitial time.Duration MediaTimeout time.Duration @@ -389,7 +390,11 @@ func NewMediaPortWith(tid traceid.ID, log logger.Logger, mon *stats.CallMonitor, } if conn == nil { // use an even RTP port (RFC 3550); some gateways misroute media when offered an odd one - c, err := rtp.ListenUDPEvenPortRange(opts.Ports.Start, opts.Ports.End, netip.AddrFrom4([4]byte{0, 0, 0, 0})) + bindIP := opts.BindIP + if !bindIP.IsValid() { + bindIP = netip.AddrFrom4([4]byte{0, 0, 0, 0}) + } + c, err := rtp.ListenUDPEvenPortRange(opts.Ports.Start, opts.Ports.End, bindIP) if err != nil { return nil, err } diff --git a/pkg/sip/media_port_test.go b/pkg/sip/media_port_test.go index 64b693b4..e4a8dcd8 100644 --- a/pkg/sip/media_port_test.go +++ b/pkg/sip/media_port_test.go @@ -69,6 +69,22 @@ func newTestMediaPort(t testing.TB, provider string) *MediaPort { return mp } +func TestMediaPortBindIP(t *testing.T) { + mon := newTestCallMonitor(t) + bindIP := netip.MustParseAddr("127.0.0.1") + mp, err := NewMediaPort(1, logger.GetLogger(), mon, &MediaOptions{ + IP: bindIP, + BindIP: bindIP, + Ports: rtcconfig.PortRange{Start: 20000, End: 20020}, + }, 8000) + require.NoError(t, err) + t.Cleanup(func() { mp.Close() }) + + addr, ok := mp.port.LocalAddr().(*net.UDPAddr) + require.True(t, ok) + require.True(t, addr.IP.Equal(net.ParseIP("127.0.0.1")), "RTP socket must bind BindIP, got %v", addr.IP) +} + type testUDPConn struct { addr netip.AddrPort closed chan struct{} diff --git a/pkg/sip/outbound.go b/pkg/sip/outbound.go index 89d5252c..0863f583 100644 --- a/pkg/sip/outbound.go +++ b/pkg/sip/outbound.go @@ -128,6 +128,7 @@ func (c *Client) newCall(ctx context.Context, tid traceid.ID, conf *config.Confi call.media, err = NewMediaPort(tid, call.log, call.mon, &MediaOptions{ IP: c.sconf.MediaIP, + BindIP: c.sconf.SignalingIPLocal, Ports: conf.RTPPort, MediaTimeoutInitial: c.conf.MediaTimeoutInitial, MediaTimeout: sipConf.mediaConfig.MediaTimeout, From 7b4f124f37d96bc5540c561f9e5cf6e71cab81b3 Mon Sep 17 00:00:00 2001 From: Alexander Matveev Date: Sun, 9 Aug 2026 01:57:03 +0300 Subject: [PATCH 2/4] fix: safely resolve RTP bind IP for NAT and multi-homed hosts SignalingIPLocal is an announce value and may equal nat_1_to_1_ip when that address is not local. Bind media via media_listen_ip, then listen_ip, then SignalingIPLocal only if present on an interface; otherwise keep 0.0.0.0. --- pkg/config/config.go | 5 ++ pkg/sip/config.go | 67 ++++++++++++++++++++++ pkg/sip/config_media_bind_test.go | 92 +++++++++++++++++++++++++++++++ pkg/sip/inbound.go | 2 +- pkg/sip/outbound.go | 2 +- 5 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 pkg/sip/config_media_bind_test.go diff --git a/pkg/config/config.go b/pkg/config/config.go index 8192660d..db7155c6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -112,6 +112,11 @@ type Config struct { // if different from signaling IP MediaUseExternalIP bool `yaml:"media_use_external_ip"` MediaNAT1To1IP string `yaml:"media_nat_1_to_1_ip"` + // MediaListenIP binds RTP sockets to a specific local interface. Empty falls back to + // ListenIP (when specific), then SignalingIPLocal only if that address is assigned + // locally; otherwise RTP listens on 0.0.0.0. Distinct from media_nat_1_to_1_ip, which + // only affects the announced address in SDP. + MediaListenIP string `yaml:"media_listen_ip"` MediaTimeout time.Duration `yaml:"media_timeout"` MediaTimeoutInitial time.Duration `yaml:"media_timeout_initial"` diff --git a/pkg/sip/config.go b/pkg/sip/config.go index 4099e57b..0efbd016 100644 --- a/pkg/sip/config.go +++ b/pkg/sip/config.go @@ -19,6 +19,7 @@ import ( "fmt" "net" "net/netip" + "strings" "time" "github.com/livekit/mediatransportutil/pkg/rtcconfig" @@ -132,3 +133,69 @@ func getLocalIP(localNet string) (netip.Addr, error) { return netip.Addr{}, fmt.Errorf("no local interface found") } + +// resolveMediaBindIP chooses the local address for RTP sockets. +// +// SignalingIPLocal is an announce/discovery value and, with nat_1_to_1_ip, may be a public +// address that is not assigned to any interface. Binding RTP to that address fails with +// EADDRNOTAVAIL. Prefer an explicit listen address, then only use SignalingIPLocal when it +// is actually present on the host. +func resolveMediaBindIP(conf *config.Config, sconf *ServiceConfig) netip.Addr { + if conf != nil { + if ip, ok := parseSpecificListenIP(conf.MediaListenIP); ok { + return ip + } + if ip, ok := parseSpecificListenIP(conf.ListenIP); ok { + return ip + } + } + if sconf != nil && sconf.SignalingIPLocal.IsValid() && isLocalInterfaceIP(sconf.SignalingIPLocal) { + return sconf.SignalingIPLocal + } + return netip.Addr{} +} + +func parseSpecificListenIP(s string) (netip.Addr, bool) { + s = strings.TrimSpace(s) + if s == "" { + return netip.Addr{}, false + } + ip, err := netip.ParseAddr(s) + if err != nil || !ip.IsValid() || ip.IsUnspecified() { + return netip.Addr{}, false + } + return ip, true +} + +func isLocalInterfaceIP(ip netip.Addr) bool { + ifaces, err := net.Interfaces() + if err != nil { + return false + } + for _, iface := range ifaces { + addrs, err := iface.Addrs() + if err != nil { + continue + } + for _, a := range addrs { + var cand net.IP + switch v := a.(type) { + case *net.IPNet: + cand = v.IP + case *net.IPAddr: + cand = v.IP + default: + continue + } + addr, ok := netip.AddrFromSlice(cand) + if !ok { + continue + } + addr = addr.Unmap() + if addr == ip.Unmap() { + return true + } + } + } + return false +} diff --git a/pkg/sip/config_media_bind_test.go b/pkg/sip/config_media_bind_test.go new file mode 100644 index 00000000..b19e8fde --- /dev/null +++ b/pkg/sip/config_media_bind_test.go @@ -0,0 +1,92 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sip + +import ( + "net/netip" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/livekit/sip/pkg/config" +) + +func TestParseSpecificListenIP(t *testing.T) { + t.Parallel() + + ip, ok := parseSpecificListenIP("127.0.0.1") + require.True(t, ok) + require.Equal(t, netip.MustParseAddr("127.0.0.1"), ip) + + _, ok = parseSpecificListenIP("") + require.False(t, ok) + _, ok = parseSpecificListenIP("0.0.0.0") + require.False(t, ok) + _, ok = parseSpecificListenIP("::") + require.False(t, ok) + _, ok = parseSpecificListenIP("not-an-ip") + require.False(t, ok) +} + +func TestIsLocalInterfaceIP(t *testing.T) { + t.Parallel() + + require.True(t, isLocalInterfaceIP(netip.MustParseAddr("127.0.0.1"))) + require.False(t, isLocalInterfaceIP(netip.MustParseAddr("8.8.8.8"))) +} + +func TestResolveMediaBindIP(t *testing.T) { + t.Parallel() + + loopback := netip.MustParseAddr("127.0.0.1") + publicNAT := netip.MustParseAddr("203.0.113.10") // TEST-NET-3, not local + + t.Run("media_listen_ip wins", func(t *testing.T) { + got := resolveMediaBindIP(&config.Config{ + MediaListenIP: "127.0.0.1", + ListenIP: "127.0.0.1", + }, &ServiceConfig{SignalingIPLocal: publicNAT}) + require.Equal(t, loopback, got) + }) + + t.Run("listen_ip when media_listen_ip empty", func(t *testing.T) { + got := resolveMediaBindIP(&config.Config{ + ListenIP: "127.0.0.1", + }, &ServiceConfig{SignalingIPLocal: publicNAT}) + require.Equal(t, loopback, got) + }) + + t.Run("wildcard listen_ip ignored", func(t *testing.T) { + got := resolveMediaBindIP(&config.Config{ + ListenIP: "0.0.0.0", + }, &ServiceConfig{SignalingIPLocal: publicNAT}) + require.False(t, got.IsValid(), "classic nat_1_to_1 public IP must not become BindIP") + }) + + t.Run("local SignalingIPLocal used", func(t *testing.T) { + got := resolveMediaBindIP(&config.Config{}, &ServiceConfig{SignalingIPLocal: loopback}) + require.Equal(t, loopback, got) + }) + + t.Run("non-local SignalingIPLocal falls back", func(t *testing.T) { + got := resolveMediaBindIP(&config.Config{ + NAT1To1IP: publicNAT.String(), + }, &ServiceConfig{ + SignalingIP: publicNAT, + SignalingIPLocal: publicNAT, + }) + require.False(t, got.IsValid()) + }) +} diff --git a/pkg/sip/inbound.go b/pkg/sip/inbound.go index 690376d8..a2c4e24b 100644 --- a/pkg/sip/inbound.go +++ b/pkg/sip/inbound.go @@ -1115,7 +1115,7 @@ func (c *inboundCall) runMediaConn(tid traceid.ID, offerData []byte, mconf *sipM logSignalChanges, _ = strconv.ParseBool(featureFlags[signalLoggingFeatureFlag]) mp, err := NewMediaPort(tid, c.log(), c.mon, &MediaOptions{ IP: c.s.sconf.MediaIP, - BindIP: c.s.sconf.SignalingIPLocal, + BindIP: resolveMediaBindIP(c.s.conf, c.s.sconf), Ports: conf.RTPPort, MediaTimeoutInitial: c.s.conf.MediaTimeoutInitial, MediaTimeout: mconf.MediaTimeout, diff --git a/pkg/sip/outbound.go b/pkg/sip/outbound.go index 0863f583..8bb796c4 100644 --- a/pkg/sip/outbound.go +++ b/pkg/sip/outbound.go @@ -128,7 +128,7 @@ func (c *Client) newCall(ctx context.Context, tid traceid.ID, conf *config.Confi call.media, err = NewMediaPort(tid, call.log, call.mon, &MediaOptions{ IP: c.sconf.MediaIP, - BindIP: c.sconf.SignalingIPLocal, + BindIP: resolveMediaBindIP(c.conf, c.sconf), Ports: conf.RTPPort, MediaTimeoutInitial: c.conf.MediaTimeoutInitial, MediaTimeout: sipConf.mediaConfig.MediaTimeout, From 3f62e9dcc96f05ea540b07bc13f9055394f1cf80 Mon Sep 17 00:00:00 2001 From: Alexander Matveev Date: Sun, 9 Aug 2026 02:00:05 +0300 Subject: [PATCH 3/4] fix: make RTP bind IP opt-in via media_listen_ip/listen_ip Do not infer BindIP from SignalingIPLocal. Default remains 0.0.0.0 so multi-interface receive behavior is unchanged unless operators explicitly pin the media NIC. --- pkg/config/config.go | 5 +-- pkg/sip/config.go | 62 +++++++------------------------ pkg/sip/config_media_bind_test.go | 33 +++++----------- pkg/sip/inbound.go | 2 +- pkg/sip/outbound.go | 2 +- 5 files changed, 28 insertions(+), 76 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index db7155c6..6b859e7d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -113,9 +113,8 @@ type Config struct { MediaUseExternalIP bool `yaml:"media_use_external_ip"` MediaNAT1To1IP string `yaml:"media_nat_1_to_1_ip"` // MediaListenIP binds RTP sockets to a specific local interface. Empty falls back to - // ListenIP (when specific), then SignalingIPLocal only if that address is assigned - // locally; otherwise RTP listens on 0.0.0.0. Distinct from media_nat_1_to_1_ip, which - // only affects the announced address in SDP. + // ListenIP when that is a specific address; otherwise RTP listens on 0.0.0.0. + // Distinct from media_nat_1_to_1_ip, which only affects the announced address in SDP. MediaListenIP string `yaml:"media_listen_ip"` MediaTimeout time.Duration `yaml:"media_timeout"` diff --git a/pkg/sip/config.go b/pkg/sip/config.go index 0efbd016..a6160357 100644 --- a/pkg/sip/config.go +++ b/pkg/sip/config.go @@ -134,23 +134,22 @@ func getLocalIP(localNet string) (netip.Addr, error) { return netip.Addr{}, fmt.Errorf("no local interface found") } -// resolveMediaBindIP chooses the local address for RTP sockets. +// resolveMediaBindIP chooses an explicit local address for RTP sockets. // -// SignalingIPLocal is an announce/discovery value and, with nat_1_to_1_ip, may be a public -// address that is not assigned to any interface. Binding RTP to that address fails with -// EADDRNOTAVAIL. Prefer an explicit listen address, then only use SignalingIPLocal when it -// is actually present on the host. -func resolveMediaBindIP(conf *config.Config, sconf *ServiceConfig) netip.Addr { - if conf != nil { - if ip, ok := parseSpecificListenIP(conf.MediaListenIP); ok { - return ip - } - if ip, ok := parseSpecificListenIP(conf.ListenIP); ok { - return ip - } +// Binding is opt-in: only media_listen_ip or a specific listen_ip pin the socket. +// Otherwise RTP keeps listening on 0.0.0.0 so multi-interface deployments are unchanged. +// Do not infer from SignalingIPLocal — with nat_1_to_1_ip that value may be a public +// announce address not assigned locally, and even when local it may be an arbitrary +// first NIC from getLocalIP (e.g. docker0). +func resolveMediaBindIP(conf *config.Config) netip.Addr { + if conf == nil { + return netip.Addr{} + } + if ip, ok := parseSpecificListenIP(conf.MediaListenIP); ok { + return ip } - if sconf != nil && sconf.SignalingIPLocal.IsValid() && isLocalInterfaceIP(sconf.SignalingIPLocal) { - return sconf.SignalingIPLocal + if ip, ok := parseSpecificListenIP(conf.ListenIP); ok { + return ip } return netip.Addr{} } @@ -166,36 +165,3 @@ func parseSpecificListenIP(s string) (netip.Addr, bool) { } return ip, true } - -func isLocalInterfaceIP(ip netip.Addr) bool { - ifaces, err := net.Interfaces() - if err != nil { - return false - } - for _, iface := range ifaces { - addrs, err := iface.Addrs() - if err != nil { - continue - } - for _, a := range addrs { - var cand net.IP - switch v := a.(type) { - case *net.IPNet: - cand = v.IP - case *net.IPAddr: - cand = v.IP - default: - continue - } - addr, ok := netip.AddrFromSlice(cand) - if !ok { - continue - } - addr = addr.Unmap() - if addr == ip.Unmap() { - return true - } - } - } - return false -} diff --git a/pkg/sip/config_media_bind_test.go b/pkg/sip/config_media_bind_test.go index b19e8fde..a98a292f 100644 --- a/pkg/sip/config_media_bind_test.go +++ b/pkg/sip/config_media_bind_test.go @@ -40,53 +40,40 @@ func TestParseSpecificListenIP(t *testing.T) { require.False(t, ok) } -func TestIsLocalInterfaceIP(t *testing.T) { - t.Parallel() - - require.True(t, isLocalInterfaceIP(netip.MustParseAddr("127.0.0.1"))) - require.False(t, isLocalInterfaceIP(netip.MustParseAddr("8.8.8.8"))) -} - func TestResolveMediaBindIP(t *testing.T) { t.Parallel() loopback := netip.MustParseAddr("127.0.0.1") - publicNAT := netip.MustParseAddr("203.0.113.10") // TEST-NET-3, not local t.Run("media_listen_ip wins", func(t *testing.T) { got := resolveMediaBindIP(&config.Config{ MediaListenIP: "127.0.0.1", - ListenIP: "127.0.0.1", - }, &ServiceConfig{SignalingIPLocal: publicNAT}) + ListenIP: "10.0.0.1", + }) require.Equal(t, loopback, got) }) t.Run("listen_ip when media_listen_ip empty", func(t *testing.T) { got := resolveMediaBindIP(&config.Config{ ListenIP: "127.0.0.1", - }, &ServiceConfig{SignalingIPLocal: publicNAT}) + }) require.Equal(t, loopback, got) }) t.Run("wildcard listen_ip ignored", func(t *testing.T) { got := resolveMediaBindIP(&config.Config{ ListenIP: "0.0.0.0", - }, &ServiceConfig{SignalingIPLocal: publicNAT}) - require.False(t, got.IsValid(), "classic nat_1_to_1 public IP must not become BindIP") + }) + require.False(t, got.IsValid()) }) - t.Run("local SignalingIPLocal used", func(t *testing.T) { - got := resolveMediaBindIP(&config.Config{}, &ServiceConfig{SignalingIPLocal: loopback}) - require.Equal(t, loopback, got) + t.Run("default remains unspecified", func(t *testing.T) { + got := resolveMediaBindIP(&config.Config{}) + require.False(t, got.IsValid(), "without explicit listen config RTP must stay on 0.0.0.0") }) - t.Run("non-local SignalingIPLocal falls back", func(t *testing.T) { - got := resolveMediaBindIP(&config.Config{ - NAT1To1IP: publicNAT.String(), - }, &ServiceConfig{ - SignalingIP: publicNAT, - SignalingIPLocal: publicNAT, - }) + t.Run("nil config", func(t *testing.T) { + got := resolveMediaBindIP(nil) require.False(t, got.IsValid()) }) } diff --git a/pkg/sip/inbound.go b/pkg/sip/inbound.go index a2c4e24b..89ae540d 100644 --- a/pkg/sip/inbound.go +++ b/pkg/sip/inbound.go @@ -1115,7 +1115,7 @@ func (c *inboundCall) runMediaConn(tid traceid.ID, offerData []byte, mconf *sipM logSignalChanges, _ = strconv.ParseBool(featureFlags[signalLoggingFeatureFlag]) mp, err := NewMediaPort(tid, c.log(), c.mon, &MediaOptions{ IP: c.s.sconf.MediaIP, - BindIP: resolveMediaBindIP(c.s.conf, c.s.sconf), + BindIP: resolveMediaBindIP(c.s.conf), Ports: conf.RTPPort, MediaTimeoutInitial: c.s.conf.MediaTimeoutInitial, MediaTimeout: mconf.MediaTimeout, diff --git a/pkg/sip/outbound.go b/pkg/sip/outbound.go index 8bb796c4..bc45fe36 100644 --- a/pkg/sip/outbound.go +++ b/pkg/sip/outbound.go @@ -128,7 +128,7 @@ func (c *Client) newCall(ctx context.Context, tid traceid.ID, conf *config.Confi call.media, err = NewMediaPort(tid, call.log, call.mon, &MediaOptions{ IP: c.sconf.MediaIP, - BindIP: resolveMediaBindIP(c.conf, c.sconf), + BindIP: resolveMediaBindIP(c.conf), Ports: conf.RTPPort, MediaTimeoutInitial: c.conf.MediaTimeoutInitial, MediaTimeout: sipConf.mediaConfig.MediaTimeout, From a96065e99066512fddd0d3a632e9a49271902b31 Mon Sep 17 00:00:00 2001 From: Alexander Matveev Date: Sun, 9 Aug 2026 02:07:58 +0300 Subject: [PATCH 4/4] fix: validate media_listen_ip at config init Reject unparseable or unspecified media_listen_ip at startup so typos fail fast instead of silently falling back to 0.0.0.0. --- pkg/config/config.go | 10 +++++ pkg/config/config_media_listen_ip_test.go | 49 +++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 pkg/config/config_media_listen_ip_test.go diff --git a/pkg/config/config.go b/pkg/config/config.go index 6b859e7d..3213db48 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -225,6 +225,16 @@ func (c *Config) Init() error { return fmt.Errorf("media_use_external_ip and media_nat_1_to_1_ip can not both be set") } + if c.MediaListenIP != "" { + ip, err := netip.ParseAddr(c.MediaListenIP) + if err != nil { + return fmt.Errorf("invalid media_listen_ip %q: %w", c.MediaListenIP, err) + } + if ip.IsUnspecified() { + return fmt.Errorf("media_listen_ip must be a specific local address, got %q", c.MediaListenIP) + } + } + return nil } diff --git a/pkg/config/config_media_listen_ip_test.go b/pkg/config/config_media_listen_ip_test.go new file mode 100644 index 00000000..a0f3420f --- /dev/null +++ b/pkg/config/config_media_listen_ip_test.go @@ -0,0 +1,49 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestConfigInitMediaListenIP(t *testing.T) { + t.Parallel() + + t.Run("empty ok", func(t *testing.T) { + c := &Config{} + require.NoError(t, c.Init()) + }) + + t.Run("specific address ok", func(t *testing.T) { + c := &Config{MediaListenIP: "127.0.0.1"} + require.NoError(t, c.Init()) + }) + + t.Run("invalid address", func(t *testing.T) { + c := &Config{MediaListenIP: "10.0.0.5x"} + err := c.Init() + require.Error(t, err) + require.Contains(t, err.Error(), "media_listen_ip") + }) + + t.Run("unspecified rejected", func(t *testing.T) { + c := &Config{MediaListenIP: "0.0.0.0"} + err := c.Init() + require.Error(t, err) + require.Contains(t, err.Error(), "media_listen_ip") + }) +}