mirror of
https://github.com/livekit/livekit.git
synced 2026-10-05 23:07:46 +00:00
Validate TURN config to guard against invalid values (#4742)
This commit is contained in:
+7
-1
@@ -100,7 +100,11 @@ rtc:
|
||||
# secret: ""
|
||||
# # Path for file containing shared secret for TURN server authentication
|
||||
# # When both secret and secret_file are set, secret takes precedence
|
||||
# # An empty/whitespace-only secret file fails startup rather than silently
|
||||
# # falling back to static credentials
|
||||
# secret_file: "/var/lib/coturn/secret"
|
||||
# # TTL of generated credentials in seconds. Defaults to 14400 (4h) when 0.
|
||||
# # Negative values fall back to the 300s (5m) default; values are capped at 86400 (24h).
|
||||
# ttl: 14400 # seconds
|
||||
# # Insecure username/password authentication
|
||||
# username: ""
|
||||
@@ -304,7 +308,8 @@ keys:
|
||||
# # optional (set only if not using external TLS termination)
|
||||
# # cert_file: /path/to/cert.pem
|
||||
# # key_file: /path/to/key.pem
|
||||
# # TTL of the TURN credentials in seconds - defaults to 300
|
||||
# # TTL of the TURN credentials in seconds - defaults to 300.
|
||||
# # Values <= 0 fall back to the 300s (5m) default; values are capped at 86400 (24h).
|
||||
# ttl_seconds: 300
|
||||
# # list of restricted peer CIDRs (loopback, link-local (unicast, multicast), multicast, private, unspecified) to allow access to.
|
||||
# # By default (i. e. empty list), all restricted peer CIDRs are denied access.
|
||||
@@ -316,6 +321,7 @@ keys:
|
||||
# # list of peer CIDRs to deny access to.
|
||||
# # This applies to all peer CIDRs, including restricted ones.
|
||||
# # Deny list takes precedence over allow list.
|
||||
# # A malformed CIDR in either list fails startup rather than being silently ignored.
|
||||
# deny_peer_cidrs:
|
||||
# - 10.0.0.0/8
|
||||
# - 192.168.0.0/16
|
||||
|
||||
+97
-19
@@ -46,12 +46,27 @@ import (
|
||||
|
||||
const (
|
||||
generatedCLIFlagUsage = "generated"
|
||||
|
||||
// TURNMaxTTLSeconds is the operational maximum for TURN credential TTLs (24 hours).
|
||||
// It bounds credential lifetime and prevents time.Duration overflow when multiplying
|
||||
// by time.Second (values above ~9.2e9 seconds wrap on 64-bit builds).
|
||||
TURNMaxTTLSeconds = 24 * 60 * 60
|
||||
|
||||
// DefaultTURNTTLSeconds is the default TTL for embedded TURN credentials, and the
|
||||
// fallback used when a configured TTL is <= 0.
|
||||
DefaultTURNTTLSeconds = 300
|
||||
|
||||
// DefaultExternalTURNTTLSeconds is the default TTL applied to external TURN
|
||||
// (static-auth-secret) credentials when the configured TTL is left at 0.
|
||||
DefaultExternalTURNTTLSeconds = 14400
|
||||
)
|
||||
|
||||
var (
|
||||
ErrKeyFileIncorrectPermission = errors.New("key file others permissions must be set to 0")
|
||||
ErrTURNSecretFileIncorrectPermission = errors.New("turn secret file others permissions must be set to 0")
|
||||
ErrKeysNotSet = errors.New("one of key-file or keys must be provided")
|
||||
ErrTURNSecretEmpty = errors.New("turn secret is empty")
|
||||
ErrTURNServerNoCredentials = errors.New("turn server has no usable credentials: set a non-empty secret/secret_file for dynamic auth, or username and credential for static auth")
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -174,7 +189,8 @@ type TURNServer struct {
|
||||
// File containing the secret
|
||||
SecretFile string `yaml:"secret_file,omitempty"`
|
||||
// TTL is the time-to-live in seconds for generated credentials when using Secret.
|
||||
// Defaults to 14400 seconds (4 hours) if not specified
|
||||
// Defaults to 14400 seconds (4 hours) when 0. Negative values fall back to the 5m
|
||||
// default and large values are capped at TURNMaxTTLSeconds.
|
||||
TTL int `yaml:"ttl,omitempty"`
|
||||
}
|
||||
|
||||
@@ -252,7 +268,8 @@ type TURNConfig struct {
|
||||
RelayPortRangeEnd uint16 `yaml:"relay_range_end,omitempty"`
|
||||
ExternalTLS bool `yaml:"external_tls,omitempty"`
|
||||
BindAddresses []string `yaml:"bind_addresses,omitempty"`
|
||||
// TTL of the TURN credentials in seconds - defaults to 300
|
||||
// TTL of the TURN credentials in seconds - defaults to 300. Values <= 0 fall back to the
|
||||
// 300s (5m) default and large values are capped at TURNMaxTTLSeconds.
|
||||
TTLSeconds int `yaml:"ttl_seconds,omitempty"`
|
||||
// list of restricted peer CIDRs (loopback, link-local (unicast, multicast), multicast, private, unspecified) to allow access to.
|
||||
// By default (i. e. empty list), all restricted peer CIDRs are denied access.
|
||||
@@ -521,7 +538,7 @@ var DefaultConfig = Config{
|
||||
TURN: TURNConfig{
|
||||
Enabled: false,
|
||||
BindAddresses: []string{"0.0.0.0"},
|
||||
TTLSeconds: 300,
|
||||
TTLSeconds: DefaultTURNTTLSeconds,
|
||||
},
|
||||
NodeSelector: NodeSelectorConfig{
|
||||
Kind: "any",
|
||||
@@ -580,6 +597,8 @@ func NewConfig(confString string, strictMode bool, c *cli.Command, baseFlags []c
|
||||
return nil, fmt.Errorf("could not validate RTC config: %v", err)
|
||||
}
|
||||
|
||||
conf.NormalizeTURNTTLs()
|
||||
|
||||
// expand env vars in filenames
|
||||
file, err := homedir.Expand(os.ExpandEnv(conf.KeyFile))
|
||||
if err != nil {
|
||||
@@ -734,30 +753,89 @@ func (conf *Config) ValidateKeys() error {
|
||||
func (conf *Config) LoadTURNSecrets() error {
|
||||
var otherFilter os.FileMode = 0o007
|
||||
for i, s := range conf.RTC.TURNServers {
|
||||
if s.SecretFile == "" {
|
||||
continue
|
||||
}
|
||||
if s.Secret != "" {
|
||||
// trim first so a blank inline secret falls back to secret_file rather than being treated as set
|
||||
inlineSecret := strings.TrimSpace(s.Secret)
|
||||
switch {
|
||||
case s.SecretFile != "" && inlineSecret != "":
|
||||
logger.Warnw("both secret and secret_file are set for TURN server, the hardcoded secret will be used", nil,
|
||||
"host", s.Host, "port", s.Port)
|
||||
continue
|
||||
conf.RTC.TURNServers[i].Secret = inlineSecret
|
||||
case s.SecretFile != "":
|
||||
st, err := os.Stat(s.SecretFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if st.Mode().Perm()&otherFilter != 0o000 {
|
||||
return ErrTURNSecretFileIncorrectPermission
|
||||
}
|
||||
data, err := os.ReadFile(s.SecretFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading turn secret file %q: %w", s.SecretFile, err)
|
||||
}
|
||||
secret := strings.TrimSpace(string(data))
|
||||
if secret == "" {
|
||||
return fmt.Errorf("turn server %q secret file %q: %w", s.Host, s.SecretFile, ErrTURNSecretEmpty)
|
||||
}
|
||||
conf.RTC.TURNServers[i].Secret = secret
|
||||
default:
|
||||
conf.RTC.TURNServers[i].Secret = inlineSecret
|
||||
}
|
||||
st, err := os.Stat(s.SecretFile)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
// validate credential mode as an explicit union rather than inferring it from a
|
||||
// post-load empty secret (which would silently fall back to static credentials)
|
||||
s := conf.RTC.TURNServers[i]
|
||||
hasDynamic := s.Secret != ""
|
||||
hasStatic := s.Username != "" && s.Credential != ""
|
||||
if !hasDynamic && !hasStatic {
|
||||
return fmt.Errorf("turn server %q: %w", s.Host, ErrTURNServerNoCredentials)
|
||||
}
|
||||
if st.Mode().Perm()&otherFilter != 0o000 {
|
||||
return ErrTURNSecretFileIncorrectPermission
|
||||
}
|
||||
data, err := os.ReadFile(s.SecretFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading turn secret file %q: %w", s.SecretFile, err)
|
||||
}
|
||||
conf.RTC.TURNServers[i].Secret = strings.TrimSpace(string(data))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClampTURNTTLSeconds bounds a TURN credential TTL to [DefaultTURNTTLSeconds, TURNMaxTTLSeconds]:
|
||||
// non-positive values (which would otherwise produce already-expired credentials) fall back to
|
||||
// the 5m default and overflowing values clamp to the max. The second return value reports whether
|
||||
// the input was out of range.
|
||||
func ClampTURNTTLSeconds(ttlSeconds int) (int, bool) {
|
||||
switch {
|
||||
case ttlSeconds <= 0:
|
||||
return DefaultTURNTTLSeconds, true
|
||||
case ttlSeconds > TURNMaxTTLSeconds:
|
||||
return TURNMaxTTLSeconds, true
|
||||
default:
|
||||
return ttlSeconds, false
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeTURNTTLs clamps configured TURN TTLs to the safe range, warning on any
|
||||
// adjustment. Safe to call more than once.
|
||||
func (conf *Config) NormalizeTURNTTLs() {
|
||||
if clamped, changed := ClampTURNTTLSeconds(conf.TURN.TTLSeconds); changed {
|
||||
logger.Warnw(
|
||||
"turn.ttl_seconds out of range, using safe value", nil,
|
||||
"configured", conf.TURN.TTLSeconds,
|
||||
"clamped", clamped,
|
||||
"max", TURNMaxTTLSeconds,
|
||||
)
|
||||
conf.TURN.TTLSeconds = clamped
|
||||
}
|
||||
for i := range conf.RTC.TURNServers {
|
||||
// external TTL of 0 means "use the default", so only cap the upper bound here;
|
||||
// non-positive values fall back to the default when credentials are generated
|
||||
if ttl := conf.RTC.TURNServers[i].TTL; ttl > TURNMaxTTLSeconds {
|
||||
logger.Warnw(
|
||||
"turn_servers ttl out of range, using safe value", nil,
|
||||
"host", conf.RTC.TURNServers[i].Host,
|
||||
"configured", ttl,
|
||||
"clamped", TURNMaxTTLSeconds,
|
||||
"max", TURNMaxTTLSeconds,
|
||||
)
|
||||
conf.RTC.TURNServers[i].TTL = TURNMaxTTLSeconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateCLIFlags(existingFlags []cli.Flag, hidden bool) ([]cli.Flag, error) {
|
||||
defaultConfig := &DefaultConfig
|
||||
flags := make([]cli.Flag, 0)
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -83,3 +85,125 @@ func TestGeneratedFlags(t *testing.T) {
|
||||
func TestYAMLTag(t *testing.T) {
|
||||
require.NoError(t, configtest.CheckYAMLTags(Config{}))
|
||||
}
|
||||
|
||||
func TestClampTURNTTLSeconds(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in int
|
||||
want int
|
||||
changed bool
|
||||
}{
|
||||
{"negative falls back to default", -1, DefaultTURNTTLSeconds, true},
|
||||
{"large negative falls back to default", -1 << 40, DefaultTURNTTLSeconds, true},
|
||||
{"zero falls back to default", 0, DefaultTURNTTLSeconds, true},
|
||||
{"in range preserved", 600, 600, false},
|
||||
{"max preserved", TURNMaxTTLSeconds, TURNMaxTTLSeconds, false},
|
||||
{"over max clamps to max", TURNMaxTTLSeconds + 1, TURNMaxTTLSeconds, true},
|
||||
{"overflowing value clamps to max", 1<<62 + 1, TURNMaxTTLSeconds, true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got, changed := ClampTURNTTLSeconds(c.in)
|
||||
require.Equal(t, c.want, got)
|
||||
require.Equal(t, c.changed, changed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeTURNTTLs(t *testing.T) {
|
||||
conf, err := NewConfig("", true, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
conf.TURN.TTLSeconds = -5
|
||||
conf.RTC.TURNServers = []TURNServer{
|
||||
{Host: "over", TTL: TURNMaxTTLSeconds + 100},
|
||||
{Host: "negative", TTL: -1},
|
||||
{Host: "default", TTL: 0},
|
||||
{Host: "ok", TTL: 600},
|
||||
}
|
||||
|
||||
conf.NormalizeTURNTTLs()
|
||||
|
||||
// embedded TTL <= 0 falls back to the 5m default
|
||||
require.Equal(t, DefaultTURNTTLSeconds, conf.TURN.TTLSeconds)
|
||||
// external TTLs: only the upper bound is capped at load; 0/negative keep their
|
||||
// "use the default" meaning and are resolved when credentials are generated
|
||||
require.Equal(t, TURNMaxTTLSeconds, conf.RTC.TURNServers[0].TTL)
|
||||
require.Equal(t, -1, conf.RTC.TURNServers[1].TTL)
|
||||
require.Equal(t, 0, conf.RTC.TURNServers[2].TTL)
|
||||
require.Equal(t, 600, conf.RTC.TURNServers[3].TTL)
|
||||
}
|
||||
|
||||
func TestNewConfigNormalizesTURNTTL(t *testing.T) {
|
||||
const content = `turn:
|
||||
ttl_seconds: -10`
|
||||
conf, err := NewConfig(content, true, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, DefaultTURNTTLSeconds, conf.TURN.TTLSeconds)
|
||||
}
|
||||
|
||||
func writeSecretFile(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "secret")
|
||||
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
|
||||
return path
|
||||
}
|
||||
|
||||
func TestLoadTURNSecrets(t *testing.T) {
|
||||
t.Run("empty secret file is rejected", func(t *testing.T) {
|
||||
conf := &Config{}
|
||||
conf.RTC.TURNServers = []TURNServer{{Host: "h", SecretFile: writeSecretFile(t, "")}}
|
||||
require.ErrorIs(t, conf.LoadTURNSecrets(), ErrTURNSecretEmpty)
|
||||
})
|
||||
|
||||
t.Run("whitespace-only secret file is rejected", func(t *testing.T) {
|
||||
conf := &Config{}
|
||||
conf.RTC.TURNServers = []TURNServer{{Host: "h", SecretFile: writeSecretFile(t, " \n\t ")}}
|
||||
require.ErrorIs(t, conf.LoadTURNSecrets(), ErrTURNSecretEmpty)
|
||||
})
|
||||
|
||||
t.Run("valid secret file is loaded and trimmed", func(t *testing.T) {
|
||||
conf := &Config{}
|
||||
conf.RTC.TURNServers = []TURNServer{{Host: "h", SecretFile: writeSecretFile(t, " topsecret\n")}}
|
||||
require.NoError(t, conf.LoadTURNSecrets())
|
||||
require.Equal(t, "topsecret", conf.RTC.TURNServers[0].Secret)
|
||||
})
|
||||
|
||||
t.Run("inline whitespace secret without static creds is rejected", func(t *testing.T) {
|
||||
conf := &Config{}
|
||||
conf.RTC.TURNServers = []TURNServer{{Host: "h", Secret: " "}}
|
||||
require.ErrorIs(t, conf.LoadTURNSecrets(), ErrTURNServerNoCredentials)
|
||||
})
|
||||
|
||||
t.Run("no credentials at all is rejected", func(t *testing.T) {
|
||||
conf := &Config{}
|
||||
conf.RTC.TURNServers = []TURNServer{{Host: "h"}}
|
||||
require.ErrorIs(t, conf.LoadTURNSecrets(), ErrTURNServerNoCredentials)
|
||||
})
|
||||
|
||||
t.Run("static credentials are accepted", func(t *testing.T) {
|
||||
conf := &Config{}
|
||||
conf.RTC.TURNServers = []TURNServer{{Host: "h", Username: "u", Credential: "c"}}
|
||||
require.NoError(t, conf.LoadTURNSecrets())
|
||||
})
|
||||
|
||||
t.Run("partial static credentials are rejected", func(t *testing.T) {
|
||||
conf := &Config{}
|
||||
conf.RTC.TURNServers = []TURNServer{{Host: "h", Username: "u"}}
|
||||
require.ErrorIs(t, conf.LoadTURNSecrets(), ErrTURNServerNoCredentials)
|
||||
})
|
||||
|
||||
t.Run("inline secret takes precedence and is trimmed", func(t *testing.T) {
|
||||
conf := &Config{}
|
||||
conf.RTC.TURNServers = []TURNServer{{Host: "h", Secret: " inline ", SecretFile: writeSecretFile(t, "fromfile")}}
|
||||
require.NoError(t, conf.LoadTURNSecrets())
|
||||
require.Equal(t, "inline", conf.RTC.TURNServers[0].Secret)
|
||||
})
|
||||
|
||||
t.Run("blank inline secret falls back to secret file", func(t *testing.T) {
|
||||
conf := &Config{}
|
||||
conf.RTC.TURNServers = []TURNServer{{Host: "h", Secret: " ", SecretFile: writeSecretFile(t, "fromfile")}}
|
||||
require.NoError(t, conf.LoadTURNSecrets())
|
||||
require.Equal(t, "fromfile", conf.RTC.TURNServers[0].Secret)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1096,11 +1096,14 @@ func (r *RoomManager) iceServersForParticipant(apiKey string, participant types.
|
||||
|
||||
var username, credential string
|
||||
if s.Secret != "" {
|
||||
// Generate dynamic credentials using TURN static auth secrets
|
||||
// Generate dynamic credentials using TURN static auth secrets.
|
||||
// 0 means use the default; clamp the rest so a negative or overflowing TTL
|
||||
// cannot produce a wrong-lifetime credential.
|
||||
ttl := s.TTL
|
||||
if ttl == 0 {
|
||||
ttl = 14400 // Default 4 hours
|
||||
ttl = config.DefaultExternalTURNTTLSeconds
|
||||
}
|
||||
ttl, _ = config.ClampTURNTTLSeconds(ttl)
|
||||
|
||||
expiry := time.Now().Add(time.Duration(ttl) * time.Second).Unix()
|
||||
participantID := string(participant.ID())
|
||||
|
||||
+34
-11
@@ -46,6 +46,20 @@ const (
|
||||
|
||||
var ErrExpired = errors.New("expired")
|
||||
|
||||
// parsePeerCIDRs compiles a list of CIDR strings, failing with a field-specific
|
||||
// error on any invalid entry so a malformed peer policy is never silently ignored.
|
||||
func parsePeerCIDRs(field string, cidrs []string) ([]*net.IPNet, error) {
|
||||
parsed := make([]*net.IPNet, 0, len(cidrs))
|
||||
for _, cidr := range cidrs {
|
||||
_, ipnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid CIDR %q in %s: %w", cidr, field, err)
|
||||
}
|
||||
parsed = append(parsed, ipnet)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone bool) (*turn.Server, error) {
|
||||
turnConf := conf.TURN
|
||||
if !turnConf.Enabled {
|
||||
@@ -64,6 +78,17 @@ func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone
|
||||
}
|
||||
}
|
||||
|
||||
// parse peer CIDR policies once at startup so a malformed entry fails loudly
|
||||
// instead of being silently skipped on every permission decision (fail-open)
|
||||
allowRestrictedPeerCIDRs, err := parsePeerCIDRs("turn.allow_restricted_peer_cidrs", turnConf.AllowRestrictedPeerCIDRs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
denyPeerCIDRs, err := parsePeerCIDRs("turn.deny_peer_cidrs", turnConf.DenyPeerCIDRs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverConfig := turn.ServerConfig{
|
||||
Realm: LivekitRealm,
|
||||
AuthHandler: authHandler,
|
||||
@@ -105,12 +130,10 @@ func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone
|
||||
peerIP.IsPrivate() ||
|
||||
peerIP.IsUnspecified() {
|
||||
allowed := false
|
||||
for _, cidr := range turnConf.AllowRestrictedPeerCIDRs {
|
||||
if _, ipnet, err := net.ParseCIDR(cidr); err == nil {
|
||||
if ipnet.Contains(peerIP) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
for _, ipnet := range allowRestrictedPeerCIDRs {
|
||||
if ipnet.Contains(peerIP) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
@@ -120,11 +143,9 @@ func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone
|
||||
// if allowed, check deny list for overrides
|
||||
}
|
||||
|
||||
for _, cidr := range turnConf.DenyPeerCIDRs {
|
||||
if _, ipnet, err := net.ParseCIDR(cidr); err == nil {
|
||||
if ipnet.Contains(peerIP) {
|
||||
return false
|
||||
}
|
||||
for _, ipnet := range denyPeerCIDRs {
|
||||
if ipnet.Contains(peerIP) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +227,8 @@ func NewTURNAuthHandler(keyProvider auth.KeyProvider) *TURNAuthHandler {
|
||||
}
|
||||
|
||||
func (h *TURNAuthHandler) CreateUsername(apiKey string, pID livekit.ParticipantID, ttlSeconds int) (string, int64) {
|
||||
// clamp defensively: non-positive TTLs fall back to the default and overflowing ones are capped
|
||||
ttlSeconds, _ = config.ClampTURNTTLSeconds(ttlSeconds)
|
||||
expiry := time.Now().Add(time.Duration(ttlSeconds) * time.Second).Unix()
|
||||
return base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", apiKey, pID, expiry)), expiry
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jxskiss/base62"
|
||||
"github.com/pion/stun/v3"
|
||||
@@ -26,6 +27,8 @@ import (
|
||||
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -75,7 +78,8 @@ func TestTURNAuthHandler_HandleAuth_ExpiredAllocateRejected(t *testing.T) {
|
||||
h := newTestTurnAuthHandler()
|
||||
pID := livekit.ParticipantID("PA_expired_alloc")
|
||||
|
||||
username, _ := h.CreateUsername(turnTestAPIKey, pID, -60)
|
||||
expiry := time.Now().Add(-time.Minute).Unix()
|
||||
username := base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", turnTestAPIKey, pID, expiry))
|
||||
_, _, ok := h.HandleAuth(&turn.RequestAttributes{
|
||||
Username: username,
|
||||
Realm: LivekitRealm,
|
||||
@@ -89,7 +93,8 @@ func TestTURNAuthHandler_HandleAuth_ExpiredNonAllocateAllowed(t *testing.T) {
|
||||
h := newTestTurnAuthHandler()
|
||||
pID := livekit.ParticipantID("PA_expired_refresh")
|
||||
|
||||
username, expiry := h.CreateUsername(turnTestAPIKey, pID, -60)
|
||||
expiry := time.Now().Add(-time.Minute).Unix()
|
||||
username := base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", turnTestAPIKey, pID, expiry))
|
||||
|
||||
// CreatePassword still enforces ErrExpired on its own, but the server hands
|
||||
// the same key it generated at allocation time — reproduce that by directly
|
||||
@@ -206,3 +211,70 @@ func TestTURNAuthHandler_CreatePassword_ZeroExpiryRejected(t *testing.T) {
|
||||
_, err := h.CreatePassword(turnTestAPIKey, pID, 0)
|
||||
require.ErrorIs(t, err, ErrExpired)
|
||||
}
|
||||
|
||||
func TestParsePeerCIDRs(t *testing.T) {
|
||||
t.Run("valid entries are compiled", func(t *testing.T) {
|
||||
nets, err := parsePeerCIDRs("turn.deny_peer_cidrs", []string{"203.0.113.0/24", "10.0.0.0/8"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, nets, 2)
|
||||
require.True(t, nets[0].Contains(net.ParseIP("203.0.113.5")))
|
||||
require.False(t, nets[0].Contains(net.ParseIP("203.0.114.5")))
|
||||
})
|
||||
|
||||
t.Run("empty list is fine", func(t *testing.T) {
|
||||
nets, err := parsePeerCIDRs("turn.deny_peer_cidrs", nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, nets)
|
||||
})
|
||||
|
||||
t.Run("invalid entry is rejected with field context", func(t *testing.T) {
|
||||
_, err := parsePeerCIDRs("turn.deny_peer_cidrs", []string{"203.0.113.0/33"})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "turn.deny_peer_cidrs")
|
||||
require.Contains(t, err.Error(), "203.0.113.0/33")
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewTurnServer_InvalidPeerCIDRFailsStartup(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
mutID func(c *config.Config)
|
||||
field string
|
||||
}{
|
||||
{
|
||||
name: "invalid deny cidr",
|
||||
mutID: func(c *config.Config) { c.TURN.DenyPeerCIDRs = []string{"203.0.113.0/33"} },
|
||||
field: "turn.deny_peer_cidrs",
|
||||
},
|
||||
{
|
||||
name: "invalid allow cidr",
|
||||
mutID: func(c *config.Config) { c.TURN.AllowRestrictedPeerCIDRs = []string{"not-a-cidr"} },
|
||||
field: "turn.allow_restricted_peer_cidrs",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
conf := &config.Config{}
|
||||
conf.TURN.Enabled = true
|
||||
conf.TURN.UDPPort = 3478
|
||||
tc.mutID(conf)
|
||||
|
||||
_, err := NewTurnServer(conf, nil, false)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.field)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTURNAuthHandler_CreateUsername_TTLClamped(t *testing.T) {
|
||||
h := newTestTurnAuthHandler()
|
||||
pID := livekit.ParticipantID("PA_ttl_clamp")
|
||||
|
||||
// An overflowing TTL must not wrap into a past expiry; it clamps to the max.
|
||||
_, overflowExpiry := h.CreateUsername(turnTestAPIKey, pID, 1<<62+1)
|
||||
require.Greater(t, overflowExpiry, time.Now().Unix())
|
||||
require.LessOrEqual(t, overflowExpiry, time.Now().Unix()+int64(config.TURNMaxTTLSeconds)+1)
|
||||
|
||||
// A non-positive TTL falls back to the default rather than producing a past/wrapped expiry.
|
||||
_, negativeExpiry := h.CreateUsername(turnTestAPIKey, pID, -1<<40)
|
||||
require.InDelta(t, time.Now().Unix()+int64(config.DefaultTURNTTLSeconds), negativeExpiry, 2)
|
||||
}
|
||||
|
||||
@@ -1434,8 +1434,9 @@ func TestTurnAuthFailure(t *testing.T) {
|
||||
validPassword, err := authHandler.CreatePassword(testApiKey, pID, validExpiry)
|
||||
require.NoError(t, err)
|
||||
|
||||
// username encoded with an already-expired timestamp.
|
||||
expiredUsername, _ := authHandler.CreateUsername(testApiKey, pID, -10)
|
||||
// username encoded with an already-expired timestamp; constructed directly
|
||||
// because CreateUsername floors non-positive TTLs to the default.
|
||||
expiredUsername := base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", testApiKey, pID, time.Now().Add(-time.Minute).Unix()))
|
||||
|
||||
// username encoded with an api key the server does not know about.
|
||||
unknownAPIKeyUsername, _ := authHandler.CreateUsername("unknown-api-key", pID, 300)
|
||||
|
||||
Reference in New Issue
Block a user