Skip to content

Commit c4e36b1

Browse files
committed
Enhance Custom Authorization Abilities
1 parent cc03474 commit c4e36b1

File tree

9 files changed

+160
-22
lines changed

9 files changed

+160
-22
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package authz
2+
3+
import (
4+
"crypto/tls"
5+
"net"
6+
)
7+
8+
// RequestAttributes represents attributes of a TURN request which
9+
// may be useful for authorizing the underlying request.
10+
type RequestAttributes struct {
11+
Username string
12+
Realm string
13+
SrcAddr net.Addr
14+
TLS *tls.ConnectionState
15+
16+
// extend as needed
17+
}
18+
19+
// Authorizer represents functionality required to authorize a request.
20+
type Authorizer interface {
21+
Authorize(ra *RequestAttributes) (key []byte, ok bool)
22+
}

internal/server/authz/legacy.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package authz
2+
3+
import "net"
4+
5+
// LegacyAuthFunc is a function used to authorize requests compatible with legacy authorization.
6+
type LegacyAuthFunc func(username, realm string, srcAddr net.Addr) (key []byte, ok bool)
7+
8+
// legacyAuthorizer is the an Authorizer implementation
9+
// which wraps an AuthFunc in order to authorize requests.
10+
type legacyAuthorizer struct {
11+
authFunc LegacyAuthFunc
12+
}
13+
14+
// NewLegacy returns a new legacy authorizer.
15+
func NewLegacy(fn LegacyAuthFunc) Authorizer {
16+
return &legacyAuthorizer{authFunc: fn}
17+
}
18+
19+
// Authorize authorizes a request given request attributes.
20+
func (a *legacyAuthorizer) Authorize(ra *RequestAttributes) (key []byte, ok bool) {
21+
return a.authFunc(ra.Username, ra.Realm, ra.SrcAddr)
22+
}

internal/server/authz/tls.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package authz
2+
3+
import (
4+
"crypto/x509"
5+
)
6+
7+
// tlsAuthorizer is the an Authorizer implementation which verifies
8+
// client TLS certificate metadata in order to to authorize requests.
9+
type tlsAuthorizer struct {
10+
verifyOpts x509.VerifyOptions
11+
getKeyForUserFunc func(string) ([]byte, bool)
12+
}
13+
14+
// NewTLS returns a new client tls certificate authorizer.
15+
//
16+
// This authorizer ensures that the client presents a valid TLS certificate
17+
// for which the CommonName must match the TURN request's username attribute.
18+
func NewTLS(
19+
verifyOpts x509.VerifyOptions,
20+
getKeyForUserFunc func(string) ([]byte, bool),
21+
) Authorizer {
22+
return &tlsAuthorizer{
23+
verifyOpts: verifyOpts,
24+
getKeyForUserFunc: getKeyForUserFunc,
25+
}
26+
}
27+
28+
// Authorize authorizes a request given request attributes.
29+
func (a *tlsAuthorizer) Authorize(ra *RequestAttributes) ([]byte, bool) {
30+
if ra.TLS == nil || len(ra.TLS.PeerCertificates) == 0 {
31+
// request not allowed due to not having tls state metadata
32+
// TODO: INFO log
33+
return nil, false
34+
}
35+
36+
key, ok := a.getKeyForUserFunc(ra.Username)
37+
if !ok {
38+
// request not allowed due to having no key for the TURN request's username
39+
// TODO: INFO log
40+
return nil, false
41+
}
42+
43+
for _, cert := range ra.TLS.PeerCertificates {
44+
if cert.Subject.CommonName != ra.Username {
45+
// cert not allowed due to not matching the TURN username
46+
// TODO: DEBUG log
47+
continue
48+
}
49+
50+
if _, err := cert.Verify(a.verifyOpts); err != nil {
51+
// cert not allowed due to failed validation
52+
// TODO: WARN log
53+
continue
54+
}
55+
56+
// a valid certificate was allowed
57+
return key, true
58+
}
59+
60+
// request not allowed due to not having any valid certs
61+
// TODO: INFO log
62+
return nil, false
63+
}

internal/server/server.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package server
66

77
import (
8+
"crypto/tls"
89
"fmt"
910
"net"
1011
"time"
@@ -13,6 +14,7 @@ import (
1314
"github.com/pion/stun/v3"
1415
"github.com/pion/turn/v4/internal/allocation"
1516
"github.com/pion/turn/v4/internal/proto"
17+
"github.com/pion/turn/v4/internal/server/authz"
1618
)
1719

1820
// Request contains all the state needed to process a single incoming datagram
@@ -21,13 +23,14 @@ type Request struct {
2123
Conn net.PacketConn
2224
SrcAddr net.Addr
2325
Buff []byte
26+
TLS *tls.ConnectionState
2427

2528
// Server State
2629
AllocationManager *allocation.Manager
2730
NonceHash *NonceHash
2831

2932
// User Configuration
30-
AuthHandler func(username string, realm string, srcAddr net.Addr) (key []byte, ok bool)
33+
Authorizer authz.Authorizer
3134
Log logging.LeveledLogger
3235
Realm string
3336
ChannelBindTimeout time.Duration

internal/server/turn_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/pion/stun/v3"
1616
"github.com/pion/turn/v4/internal/allocation"
1717
"github.com/pion/turn/v4/internal/proto"
18+
"github.com/pion/turn/v4/internal/server/authz"
1819
"github.com/stretchr/testify/assert"
1920
)
2021

@@ -90,9 +91,9 @@ func TestAllocationLifeTime(t *testing.T) {
9091
Conn: l,
9192
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5000},
9293
Log: logger,
93-
AuthHandler: func(string, string, net.Addr) (key []byte, ok bool) {
94+
Authorizer: authz.NewLegacy(func(string, string, net.Addr) (key []byte, ok bool) {
9495
return []byte(staticKey), true
95-
},
96+
}),
9697
}
9798

9899
fiveTuple := &allocation.FiveTuple{SrcAddr: r.SrcAddr, DstAddr: r.Conn.LocalAddr(), Protocol: allocation.UDP}

internal/server/util.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/pion/stun/v3"
1313
"github.com/pion/turn/v4/internal/proto"
14+
"github.com/pion/turn/v4/internal/server/authz"
1415
)
1516

1617
const (
@@ -66,9 +67,9 @@ func authenticateRequest(r Request, m *stun.Message, callingMethod stun.Method)
6667
realmAttr := &stun.Realm{}
6768
badRequestMsg := buildMsg(m.TransactionID, stun.NewType(callingMethod, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeBadRequest})
6869

69-
// No Auth handler is set, server is running in STUN only mode
70+
// No Authorizer is set, server is running in STUN only mode
7071
// Respond with 400 so clients don't retry
71-
if r.AuthHandler == nil {
72+
if r.Authorizer == nil {
7273
sendErr := buildAndSend(r.Conn, r.SrcAddr, badRequestMsg...)
7374
return nil, false, sendErr
7475
}
@@ -88,7 +89,12 @@ func authenticateRequest(r Request, m *stun.Message, callingMethod stun.Method)
8889
return nil, false, buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
8990
}
9091

91-
ourKey, ok := r.AuthHandler(usernameAttr.String(), realmAttr.String(), r.SrcAddr)
92+
ourKey, ok := r.Authorizer.Authorize(&authz.RequestAttributes{
93+
Username: usernameAttr.String(),
94+
Realm: realmAttr.String(),
95+
SrcAddr: r.SrcAddr,
96+
TLS: r.TLS,
97+
})
9298
if !ok {
9399
return nil, false, buildAndSendErr(r.Conn, r.SrcAddr, fmt.Errorf("%w %s", errNoSuchUser, usernameAttr.String()), badRequestMsg...)
94100
}

lt_cred.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import ( //nolint:gci
1313
"time"
1414

1515
"github.com/pion/logging"
16+
"github.com/pion/turn/v4/internal/server/authz"
1617
)
1718

1819
// GenerateLongTermCredentials can be used to create credentials valid for [duration] time
@@ -44,7 +45,7 @@ func longTermCredentials(username string, sharedSecret string) (string, error) {
4445

4546
// NewLongTermAuthHandler returns a turn.AuthAuthHandler used with Long Term (or Time Windowed) Credentials.
4647
// See: https://datatracker.ietf.org/doc/html/rfc8489#section-9.2
47-
func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHandler {
48+
func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) authz.LegacyAuthFunc {
4849
if l == nil {
4950
l = logging.NewDefaultLoggerFactory().NewLogger("turn")
5051
}
@@ -74,7 +75,7 @@ func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHa
7475
//
7576
// The supported format of is timestamp:username, where username is an arbitrary user id and the
7677
// timestamp specifies the expiry of the credential.
77-
func LongTermTURNRESTAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHandler {
78+
func LongTermTURNRESTAuthHandler(sharedSecret string, l logging.LeveledLogger) authz.LegacyAuthFunc {
7879
if l == nil {
7980
l = logging.NewDefaultLoggerFactory().NewLogger("turn")
8081
}

server.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package turn
66

77
import (
8+
"crypto/tls"
89
"errors"
910
"fmt"
1011
"net"
@@ -14,6 +15,7 @@ import (
1415
"github.com/pion/turn/v4/internal/allocation"
1516
"github.com/pion/turn/v4/internal/proto"
1617
"github.com/pion/turn/v4/internal/server"
18+
"github.com/pion/turn/v4/internal/server/authz"
1719
)
1820

1921
const (
@@ -23,7 +25,7 @@ const (
2325
// Server is an instance of the Pion TURN Server
2426
type Server struct {
2527
log logging.LeveledLogger
26-
authHandler AuthHandler
28+
authorizer authz.Authorizer
2729
realm string
2830
channelBindTimeout time.Duration
2931
nonceHash *server.NonceHash
@@ -57,9 +59,16 @@ func NewServer(config ServerConfig) (*Server, error) {
5759
return nil, err
5860
}
5961

62+
// determine authorizer, prioritizing the
63+
// (legacy) AuthHandler if it was provided.
64+
authorizer := config.Authorizer
65+
if config.AuthHandler != nil {
66+
authorizer = authz.NewLegacy(config.AuthHandler)
67+
}
68+
6069
s := &Server{
6170
log: loggerFactory.NewLogger("turn"),
62-
authHandler: config.AuthHandler,
71+
authorizer: authorizer,
6372
realm: config.Realm,
6473
channelBindTimeout: config.ChannelBindTimeout,
6574
packetConnConfigs: config.PacketConnConfigs,
@@ -79,7 +88,7 @@ func NewServer(config ServerConfig) (*Server, error) {
7988
}
8089

8190
go func(cfg PacketConnConfig, am *allocation.Manager) {
82-
s.readLoop(cfg.PacketConn, am)
91+
s.readLoop(cfg.PacketConn, am, nil)
8392

8493
if err := am.Close(); err != nil {
8594
s.log.Errorf("Failed to close AllocationManager: %s", err)
@@ -151,7 +160,16 @@ func (s *Server) readListener(l net.Listener, am *allocation.Manager) {
151160
}
152161

153162
go func() {
154-
s.readLoop(NewSTUNConn(conn), am)
163+
var tlsConnectionState *tls.ConnectionState
164+
165+
// extract tls connection state if possible
166+
tlsConn, ok := conn.(*tls.Conn)
167+
if ok {
168+
cs := tlsConn.ConnectionState()
169+
tlsConnectionState = &cs
170+
}
171+
172+
s.readLoop(NewSTUNConn(conn), am, tlsConnectionState)
155173

156174
// Delete allocation
157175
am.DeleteAllocation(&allocation.FiveTuple{
@@ -202,7 +220,7 @@ func (s *Server) createAllocationManager(addrGenerator RelayAddressGenerator, ha
202220
return am, err
203221
}
204222

205-
func (s *Server) readLoop(p net.PacketConn, allocationManager *allocation.Manager) {
223+
func (s *Server) readLoop(p net.PacketConn, allocationManager *allocation.Manager, tls *tls.ConnectionState) {
206224
buf := make([]byte, s.inboundMTU)
207225
for {
208226
n, addr, err := p.ReadFrom(buf)
@@ -219,8 +237,9 @@ func (s *Server) readLoop(p net.PacketConn, allocationManager *allocation.Manage
219237
Conn: p,
220238
SrcAddr: addr,
221239
Buff: buf[:n],
240+
TLS: tls,
222241
Log: s.log,
223-
AuthHandler: s.authHandler,
242+
Authorizer: s.authorizer,
224243
Realm: s.realm,
225244
AllocationManager: allocationManager,
226245
ChannelBindTimeout: s.channelBindTimeout,

server_config.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212

1313
"github.com/pion/logging"
14+
"github.com/pion/turn/v4/internal/server/authz"
1415
)
1516

1617
// RelayAddressGenerator is used to generate a RelayAddress when creating an allocation.
@@ -93,9 +94,6 @@ func (c *ListenerConfig) validate() error {
9394
return c.RelayAddressGenerator.Validate()
9495
}
9596

96-
// AuthHandler is a callback used to handle incoming auth requests, allowing users to customize Pion TURN with custom behavior
97-
type AuthHandler func(username, realm string, srcAddr net.Addr) (key []byte, ok bool)
98-
9997
// GenerateAuthKey is a convenience function to easily generate keys in the format used by AuthHandler
10098
func GenerateAuthKey(username, realm, password string) []byte {
10199
// #nosec
@@ -106,25 +104,28 @@ func GenerateAuthKey(username, realm, password string) []byte {
106104

107105
// ServerConfig configures the Pion TURN Server
108106
type ServerConfig struct {
109-
// PacketConnConfigs and ListenerConfigs are a list of all the turn listeners
110-
// Each listener can have custom behavior around the creation of Relays
107+
// PacketConnConfigs and ListenerConfigs are a list of all the turn listeners.
108+
// Each listener can have custom behavior around the creation of Relays.
111109
PacketConnConfigs []PacketConnConfig
112110
ListenerConfigs []ListenerConfig
113111

114112
// LoggerFactory must be set for logging from this server.
115113
LoggerFactory logging.LoggerFactory
116114

117-
// Realm sets the realm for this server
115+
// Realm sets the realm for this server.
118116
Realm string
119117

120-
// AuthHandler is a callback used to handle incoming auth requests, allowing users to customize Pion TURN with custom behavior
121-
AuthHandler AuthHandler
118+
// Authorizer is user to handle incoming auth requests, allowing users to customize Pion TURN with custom behavior.
119+
Authorizer authz.Authorizer
122120

123121
// ChannelBindTimeout sets the lifetime of channel binding. Defaults to 10 minutes.
124122
ChannelBindTimeout time.Duration
125123

126124
// Sets the server inbound MTU(Maximum transmition unit). Defaults to 1600 bytes.
127125
InboundMTU int
126+
127+
// AuthHandler is deprecated, use Authorizer instead.
128+
AuthHandler authz.LegacyAuthFunc
128129
}
129130

130131
func (s *ServerConfig) validate() error {

0 commit comments

Comments
 (0)