Apply ttl check only when authenticate allocation creating (#4526)

* Apply ttl check only when authenticate allocation creating

TTL check could reject allocation/persmission refresh in
security enhancement #4505, cause long-live session disconnect
when turn credential is expired.
Only check ttl on allocation creating to prevent abusing leaked
credential but keep long-live session work.
This commit is contained in:
cnderrauber
2026-05-15 14:55:05 +08:00
committed by GitHub
parent b32933b0d4
commit 89faaeba82
9 changed files with 180 additions and 32 deletions
+2 -1
View File
@@ -38,7 +38,7 @@ require (
github.com/pion/sctp v1.9.5
github.com/pion/sdp/v3 v3.0.18
github.com/pion/transport/v4 v4.0.1
github.com/pion/turn/v4 v4.1.4
github.com/pion/turn/v5 v5.0.4
github.com/pion/webrtc/v4 v4.2.11
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.23.2
@@ -74,6 +74,7 @@ require (
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.6 // indirect
github.com/pion/turn/v4 v4.1.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
+3 -3
View File
@@ -181,9 +181,7 @@ github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5AT
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
github.com/livekit/mediatransportutil v0.0.0-20260501135216-8818f1b77e59 h1:lWRMrb4ReRJu/e/BAp1kpT6fQOjS8WjCxdp0PGjgrBc=
github.com/livekit/mediatransportutil v0.0.0-20260501135216-8818f1b77e59/go.mod h1:RCd46PT+6sEztld6XpkCrG1xskb0u3SqxIjy4G897Ss=
github.com/livekit/protocol v1.45.9-0.20260507075906-0781956bb0b7 h1:twNVS8XPY22cEWMHTCLyrXkFdft24+FZiGEGL3sQNRs=
github.com/livekit/protocol v1.45.9-0.20260507075906-0781956bb0b7/go.mod h1:KEPIJ/ZdMFQ9tmmfv/uT9TjQEuEcZupCZBabuRGEC1k=
github.com/livekit/protocol v1.45.9-0.20260513103514-a36e9bfa4777/go.mod h1:KEPIJ/ZdMFQ9tmmfv/uT9TjQEuEcZupCZBabuRGEC1k=
github.com/livekit/protocol v1.45.9-0.20260514081508-d0e065ec5133 h1:b6Eodjgt2IdhKMhenZmGlZxDbRYuR+QEzdkhy7DdGRw=
github.com/livekit/protocol v1.45.9-0.20260514081508-d0e065ec5133/go.mod h1:KEPIJ/ZdMFQ9tmmfv/uT9TjQEuEcZupCZBabuRGEC1k=
github.com/livekit/psrpc v0.7.1 h1:ms37az0QTD3UXIWuUC5D/SkmKOlRMVRsI261eBWu/Vw=
github.com/livekit/psrpc v0.7.1/go.mod h1:bZ4iHFQptTkbPnB0LasvRNu/OBYXEu1NA6O5BMFo9kk=
@@ -286,6 +284,8 @@ github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
github.com/pion/turn/v5 v5.0.4 h1:xKAnP1b5eCnjFPd55OgxkqIVoyzbHKZa06SxZ3fopXQ=
github.com/pion/turn/v5 v5.0.4/go.mod h1:zbPsMp+fIVhKt5uWu2jcjk88FcoRbGMBSzVsaTuhcmM=
github.com/pion/webrtc/v4 v4.2.11 h1:QUX1QZKlNIn4O7U5JxLPGP0sV5RTncZkzu9SPR3jVNU=
github.com/pion/webrtc/v4 v4.2.11/go.mod h1:s/rAiyy77GyRFrZMx+Ls6aua26dIBPudH8/ZHYbIRWY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+1 -1
View File
@@ -27,7 +27,7 @@ import (
"strconv"
"time"
"github.com/pion/turn/v4"
"github.com/pion/turn/v5"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/cors"
"github.com/twitchtv/twirp"
+28 -18
View File
@@ -24,7 +24,8 @@ import (
"time"
"github.com/jxskiss/base62"
"github.com/pion/turn/v4"
"github.com/pion/stun/v3"
"github.com/pion/turn/v5"
"github.com/pkg/errors"
"github.com/livekit/protocol/auth"
@@ -230,6 +231,13 @@ func (h *TURNAuthHandler) ParseUsername(username string) (apiKey string, pID liv
}
func (h *TURNAuthHandler) CreatePassword(apiKey string, pID livekit.ParticipantID, expiry int64) (string, error) {
if expiry != 0 && time.Now().After(time.Unix(expiry, 0)) {
return "", ErrExpired
}
return h.computePassword(apiKey, pID, expiry)
}
func (h *TURNAuthHandler) computePassword(apiKey string, pID livekit.ParticipantID, expiry int64) (string, error) {
secret := h.keyProvider.GetSecret(apiKey)
if secret == "" {
return "", ErrInvalidAPIKey
@@ -237,11 +245,6 @@ func (h *TURNAuthHandler) CreatePassword(apiKey string, pID livekit.ParticipantI
keyInput := fmt.Sprintf("%s|%s", secret, pID)
if expiry != 0 {
expiryTime := time.Unix(expiry, 0)
if time.Now().After(expiryTime) {
return "", ErrExpired
}
keyInput = fmt.Sprintf("%s|%s|%d", secret, pID, expiry)
}
@@ -249,31 +252,38 @@ func (h *TURNAuthHandler) CreatePassword(apiKey string, pID livekit.ParticipantI
return base62.EncodeToString(sum[:]), nil
}
func (h *TURNAuthHandler) HandleAuth(username, realm string, srcAddr net.Addr) (key []byte, ok bool) {
func (h *TURNAuthHandler) HandleAuth(ra *turn.RequestAttributes) (userID string, key []byte, ok bool) {
username := ra.Username
decoded, err := base62.DecodeString(username)
if err != nil {
return nil, false
return "", nil, false
}
parts := strings.Split(string(decoded), "|")
if len(parts) != 2 && len(parts) != 3 {
return nil, false
return "", nil, false
}
expiry := int64(0)
if len(parts) == 3 {
var err error
if expiry, err = strconv.ParseInt(parts[2], 10, 64); err != nil {
return nil, false
} else {
expiryTime := time.Unix(expiry, 0)
if time.Now().After(expiryTime) {
return nil, false
return "", nil, false
}
expiryTime := time.Unix(expiry, 0)
if time.Now().After(expiryTime) {
// TTL only applies to initial allocation. Refresh / CreatePermission /
// ChannelBind / Send / Data requests are still authenticated against the
// username/password but skip the TTL check so long-running sessions can
// keep refreshing past the credential expiry.
if ra.Method == stun.MethodAllocate {
logger.Infow("TURN credential expired", "username", decoded, "participantID", parts[1], "expiry", expiryTime, "method", ra.Method)
return "", nil, false
}
}
}
password, err := h.CreatePassword(parts[0], livekit.ParticipantID(parts[1]), expiry)
password, err := h.computePassword(parts[0], livekit.ParticipantID(parts[1]), expiry)
if err != nil {
logger.Warnw("could not create TURN password", err, "username", username)
return nil, false
logger.Warnw("could not create TURN password", err, "username", decoded)
return "", nil, false
}
return turn.GenerateAuthKey(username, LivekitRealm, password), true
return parts[1], turn.GenerateAuthKey(username, LivekitRealm, password), true
}
+128
View File
@@ -0,0 +1,128 @@
// 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 service
import (
"net"
"testing"
"github.com/pion/stun/v3"
"github.com/pion/turn/v5"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/livekit"
)
const (
turnTestAPIKey = "APITestKey"
turnTestAPISecret = "TestSecret"
)
func newTestTurnAuthHandler() *TURNAuthHandler {
return NewTURNAuthHandler(auth.NewSimpleKeyProvider(turnTestAPIKey, turnTestAPISecret))
}
func mustAuthCreds(t *testing.T, h *TURNAuthHandler, pID livekit.ParticipantID, ttlSeconds int) (username string, key []byte) {
t.Helper()
username, expiry := h.CreateUsername(turnTestAPIKey, pID, ttlSeconds)
password, err := h.CreatePassword(turnTestAPIKey, pID, expiry)
require.NoError(t, err)
return username, turn.GenerateAuthKey(username, LivekitRealm, password)
}
func TestTURNAuthHandler_HandleAuth_ValidCredentials(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_valid")
username, expectedKey := mustAuthCreds(t, h, pID, 300)
for _, method := range []stun.Method{
stun.MethodAllocate,
stun.MethodRefresh,
stun.MethodCreatePermission,
stun.MethodChannelBind,
stun.MethodSend,
} {
t.Run(method.String(), func(t *testing.T) {
userID, key, ok := h.HandleAuth(&turn.RequestAttributes{
Username: username,
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: method,
})
require.True(t, ok)
require.Equal(t, string(pID), userID)
require.Equal(t, expectedKey, key)
})
}
}
func TestTURNAuthHandler_HandleAuth_ExpiredAllocateRejected(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_expired_alloc")
username, _ := h.CreateUsername(turnTestAPIKey, pID, -60)
_, _, ok := h.HandleAuth(&turn.RequestAttributes{
Username: username,
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: stun.MethodAllocate,
})
require.False(t, ok, "Allocate request with expired credentials must be rejected")
}
func TestTURNAuthHandler_HandleAuth_ExpiredNonAllocateAllowed(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_expired_refresh")
username, expiry := h.CreateUsername(turnTestAPIKey, pID, -60)
// CreatePassword still enforces ErrExpired on its own, but the server hands
// the same key it generated at allocation time — reproduce that by directly
// hashing without going through CreatePassword's expiry guard.
password, err := h.computePassword(turnTestAPIKey, pID, expiry)
require.NoError(t, err)
expectedKey := turn.GenerateAuthKey(username, LivekitRealm, password)
for _, method := range []stun.Method{
stun.MethodRefresh,
stun.MethodCreatePermission,
stun.MethodChannelBind,
stun.MethodSend,
} {
t.Run(method.String(), func(t *testing.T) {
userID, key, ok := h.HandleAuth(&turn.RequestAttributes{
Username: username,
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: method,
})
require.True(t, ok, "Non-allocate request with expired credentials must succeed")
require.Equal(t, string(pID), userID)
require.Equal(t, expectedKey, key)
})
}
}
func TestTURNAuthHandler_HandleAuth_WrongUsernameRejected(t *testing.T) {
h := newTestTurnAuthHandler()
_, _, ok := h.HandleAuth(&turn.RequestAttributes{
Username: "not-base62!!!",
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: stun.MethodRefresh,
})
require.False(t, ok)
}
+1 -1
View File
@@ -22,7 +22,7 @@ import (
"os"
"github.com/google/wire"
"github.com/pion/turn/v4"
"github.com/pion/turn/v5"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
"gopkg.in/yaml.v3"
+1 -1
View File
@@ -22,7 +22,7 @@ import (
"github.com/livekit/protocol/webhook"
"github.com/livekit/psrpc"
"github.com/livekit/psrpc/pkg/middleware/otelpsrpc"
"github.com/pion/turn/v4"
"github.com/pion/turn/v5"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
"gopkg.in/yaml.v3"
+15 -6
View File
@@ -17,7 +17,7 @@ package telemetry
import (
"net"
"github.com/pion/turn/v4"
"github.com/pion/turn/v5"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
)
@@ -113,8 +113,8 @@ func NewRelayAddressGenerator(g turn.RelayAddressGenerator) *RelayAddressGenerat
return &RelayAddressGenerator{RelayAddressGenerator: g}
}
func (g *RelayAddressGenerator) AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error) {
conn, addr, err := g.RelayAddressGenerator.AllocatePacketConn(network, requestedPort)
func (g *RelayAddressGenerator) AllocatePacketConn(c turn.AllocateListenerConfig) (net.PacketConn, net.Addr, error) {
conn, addr, err := g.RelayAddressGenerator.AllocatePacketConn(c)
if err != nil {
return nil, addr, err
}
@@ -122,11 +122,20 @@ func (g *RelayAddressGenerator) AllocatePacketConn(network string, requestedPort
return NewPacketConn(conn, prometheus.Outgoing), addr, err
}
func (g *RelayAddressGenerator) AllocateConn(network string, requestedPort int) (net.Conn, net.Addr, error) {
conn, addr, err := g.RelayAddressGenerator.AllocateConn(network, requestedPort)
func (g *RelayAddressGenerator) AllocateConn(c turn.AllocateConnConfig) (net.Conn, error) {
conn, err := g.RelayAddressGenerator.AllocateConn(c)
if err != nil {
return nil, err
}
return NewConn(conn, prometheus.Outgoing), err
}
func (g *RelayAddressGenerator) AllocateListener(c turn.AllocateListenerConfig) (net.Listener, net.Addr, error) {
l, addr, err := g.RelayAddressGenerator.AllocateListener(c)
if err != nil {
return nil, addr, err
}
return NewConn(conn, prometheus.Outgoing), addr, err
return NewListener(l), addr, err
}
+1 -1
View File
@@ -29,7 +29,7 @@ import (
"github.com/pion/sdp/v3"
"github.com/pion/stun/v3"
"github.com/pion/turn/v4"
"github.com/pion/turn/v5"
"github.com/pion/webrtc/v4"
"github.com/stretchr/testify/require"
"github.com/thoas/go-funk"