mirror of
https://github.com/livekit/livekit.git
synced 2026-05-17 02:45:32 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user