Skip to content
Open
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
76 changes: 76 additions & 0 deletions cmd/livekit-sip/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package main
import (
"context"
"fmt"
"net"
"os"
"os/signal"
"syscall"
Expand Down Expand Up @@ -70,6 +71,17 @@ func runService(ctx context.Context, c *cli.Command) error {
if err != nil {
return err
}

// Resolve LIVEKIT_NODE_IP_IFACE (if set) into conf.ListenIP before the
// service starts. This mirrors the LiveKit media server behaviour: the env
// var names the network interface whose IPv4 address should be used for SIP
// signalling binding. It is useful in ECS/Fargate/Kubernetes where the
// container IP lives on a known interface (e.g. eth0) and must be bound
// explicitly rather than falling back to the 0.0.0.0 wildcard.
if err = applyNodeIPIface(conf); err != nil {
return err
}

log := logger.GetLogger()
if conf.JaegerURL != "" {
jaeger.Configure(ctx, conf.JaegerURL, conf.ServiceName)
Expand Down Expand Up @@ -124,6 +136,70 @@ func runService(ctx context.Context, c *cli.Command) error {
return svc.Run()
}

// applyNodeIPIface reads LIVEKIT_NODE_IP_IFACE and, if set, resolves the first
// IPv4 address of that interface into conf.ListenIP.
//
// The override is skipped when:
// - the env var is absent or empty, or
// - conf.ListenIP is already set to a specific (non-wildcard) address,
// meaning the operator made an explicit choice in the config file.
func applyNodeIPIface(conf *config.Config) error {
iface := os.Getenv("LIVEKIT_NODE_IP_IFACE")
if iface == "" {
return nil
}

log := logger.GetLogger()

// Respect an explicit listen_ip that isn't the wildcard.
if conf.ListenIP != "" && conf.ListenIP != "0.0.0.0" {
log.Infow("LIVEKIT_NODE_IP_IFACE set but listen_ip already configured, skipping interface resolution",
"iface", iface,
"listen_ip", conf.ListenIP,
)
return nil
}

ip, err := ipv4FromIface(iface)
if err != nil {
return fmt.Errorf("LIVEKIT_NODE_IP_IFACE=%s: %w", iface, err)
}

log.Infow("resolved listen_ip from network interface", "iface", iface, "ip", ip)
conf.ListenIP = ip
return nil
}

// ipv4FromIface returns the first IPv4 address assigned to the named interface.
func ipv4FromIface(name string) (string, error) {
ifc, err := net.InterfaceByName(name)
if err != nil {
return "", fmt.Errorf("interface %q not found: %w", name, err)
}
addrs, err := ifc.Addrs()
if err != nil {
return "", fmt.Errorf("cannot list addresses for %q: %w", name, err)
}
if ip, ok := firstIPv4(addrs); ok {
return ip, nil
}
return "", fmt.Errorf("no IPv4 address found on interface %q", name)
}

// firstIPv4 returns the first IPv4 address in addrs, if any.
func firstIPv4(addrs []net.Addr) (string, bool) {
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
if !ok {
continue
}
if ip4 := ipnet.IP.To4(); ip4 != nil {
return ip4.String(), true
}
}
return "", false
}

func getConfig(c *cli.Command, initialize bool) (*config.Config, error) {
configFile := c.String("config")
configBody := c.String("config-body")
Expand Down
96 changes: 96 additions & 0 deletions cmd/livekit-sip/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2023 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 main

import (
"net"
"testing"

"github.com/livekit/sip/pkg/config"
)

func TestFirstIPv4(t *testing.T) {
cases := []struct {
name string
addrs []net.Addr
want string
ok bool
}{
{
name: "empty",
addrs: nil,
want: "",
ok: false,
},
{
name: "ipv6 only",
addrs: []net.Addr{
&net.IPNet{IP: net.ParseIP("fe80::1"), Mask: net.CIDRMask(64, 128)},
},
want: "",
ok: false,
},
{
name: "picks first ipv4",
addrs: []net.Addr{
&net.IPNet{IP: net.ParseIP("fe80::1"), Mask: net.CIDRMask(64, 128)},
&net.IPNet{IP: net.ParseIP("10.0.0.5"), Mask: net.CIDRMask(24, 32)},
&net.IPNet{IP: net.ParseIP("10.0.0.9"), Mask: net.CIDRMask(24, 32)},
},
want: "10.0.0.5",
ok: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got, ok := firstIPv4(c.addrs)
if got != c.want || ok != c.ok {
t.Fatalf("firstIPv4() = (%q, %v), want (%q, %v)", got, ok, c.want, c.ok)
}
})
}
}

func TestApplyNodeIPIface(t *testing.T) {
t.Run("env unset is a no-op", func(t *testing.T) {
t.Setenv("LIVEKIT_NODE_IP_IFACE", "")
conf := &config.Config{ListenIP: ""}
if err := applyNodeIPIface(conf); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if conf.ListenIP != "" {
t.Fatalf("ListenIP = %q, want empty", conf.ListenIP)
}
})

t.Run("explicit listen_ip is preserved", func(t *testing.T) {
t.Setenv("LIVEKIT_NODE_IP_IFACE", "eth0")
conf := &config.Config{ListenIP: "10.1.2.3"}
if err := applyNodeIPIface(conf); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if conf.ListenIP != "10.1.2.3" {
t.Fatalf("ListenIP = %q, want 10.1.2.3", conf.ListenIP)
}
})

t.Run("unknown interface errors", func(t *testing.T) {
t.Setenv("LIVEKIT_NODE_IP_IFACE", "definitely-not-an-iface-xyz")
conf := &config.Config{ListenIP: ""}
if err := applyNodeIPIface(conf); err == nil {
t.Fatal("expected error for unknown interface, got nil")
}
})
}
Loading