From aac5b3dfa8138293525ac2dd4d257be608dbedb9 Mon Sep 17 00:00:00 2001 From: Adriano Sela Aviles Date: Mon, 14 Oct 2024 13:12:24 -0700 Subject: [PATCH] Enhance Custom Authorization Abilities --- internal/server/authz/authorizer.go | 25 +++++++++++ internal/server/authz/legacy.go | 25 +++++++++++ internal/server/authz/tls.go | 66 +++++++++++++++++++++++++++++ internal/server/server.go | 5 ++- internal/server/turn_test.go | 5 ++- internal/server/util.go | 12 ++++-- lt_cred.go | 5 ++- server.go | 31 +++++++++++--- server_config.go | 17 ++++---- 9 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 internal/server/authz/authorizer.go create mode 100644 internal/server/authz/legacy.go create mode 100644 internal/server/authz/tls.go diff --git a/internal/server/authz/authorizer.go b/internal/server/authz/authorizer.go new file mode 100644 index 00000000..2a22a3f1 --- /dev/null +++ b/internal/server/authz/authorizer.go @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package authz + +import ( + "crypto/tls" + "net" +) + +// RequestAttributes represents attributes of a TURN request which +// may be useful for authorizing the underlying request. +type RequestAttributes struct { + Username string + Realm string + SrcAddr net.Addr + TLS *tls.ConnectionState + + // extend as needed +} + +// Authorizer represents functionality required to authorize a request. +type Authorizer interface { + Authorize(ra *RequestAttributes) (key []byte, ok bool) +} diff --git a/internal/server/authz/legacy.go b/internal/server/authz/legacy.go new file mode 100644 index 00000000..00c0eb33 --- /dev/null +++ b/internal/server/authz/legacy.go @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package authz + +import "net" + +// LegacyAuthFunc is a function used to authorize requests compatible with legacy authorization. +type LegacyAuthFunc func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) + +// legacyAuthorizer is the an Authorizer implementation +// which wraps an AuthFunc in order to authorize requests. +type legacyAuthorizer struct { + authFunc LegacyAuthFunc +} + +// NewLegacy returns a new legacy authorizer. +func NewLegacy(fn LegacyAuthFunc) Authorizer { + return &legacyAuthorizer{authFunc: fn} +} + +// Authorize authorizes a request given request attributes. +func (a *legacyAuthorizer) Authorize(ra *RequestAttributes) (key []byte, ok bool) { + return a.authFunc(ra.Username, ra.Realm, ra.SrcAddr) +} diff --git a/internal/server/authz/tls.go b/internal/server/authz/tls.go new file mode 100644 index 00000000..a645a312 --- /dev/null +++ b/internal/server/authz/tls.go @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package authz + +import ( + "crypto/x509" +) + +// tlsAuthorizer is the an Authorizer implementation which verifies +// client TLS certificate metadata in order to to authorize requests. +type tlsAuthorizer struct { + verifyOpts x509.VerifyOptions + getKeyForUserFunc func(string) ([]byte, bool) +} + +// NewTLS returns a new client tls certificate authorizer. +// +// This authorizer ensures that the client presents a valid TLS certificate +// for which the CommonName must match the TURN request's username attribute. +func NewTLS( + verifyOpts x509.VerifyOptions, + getKeyForUserFunc func(string) ([]byte, bool), +) Authorizer { + return &tlsAuthorizer{ + verifyOpts: verifyOpts, + getKeyForUserFunc: getKeyForUserFunc, + } +} + +// Authorize authorizes a request given request attributes. +func (a *tlsAuthorizer) Authorize(ra *RequestAttributes) ([]byte, bool) { + if ra.TLS == nil || len(ra.TLS.PeerCertificates) == 0 { + // request not allowed due to not having tls state metadata + // TODO: INFO log + return nil, false + } + + key, ok := a.getKeyForUserFunc(ra.Username) + if !ok { + // request not allowed due to having no key for the TURN request's username + // TODO: INFO log + return nil, false + } + + for _, cert := range ra.TLS.PeerCertificates { + if cert.Subject.CommonName != ra.Username { + // cert not allowed due to not matching the TURN username + // TODO: DEBUG log + continue + } + + if _, err := cert.Verify(a.verifyOpts); err != nil { + // cert not allowed due to failed validation + // TODO: WARN log + continue + } + + // a valid certificate was allowed + return key, true + } + + // request not allowed due to not having any valid certs + // TODO: INFO log + return nil, false +} diff --git a/internal/server/server.go b/internal/server/server.go index 253492e9..e20afa91 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -5,6 +5,7 @@ package server import ( + "crypto/tls" "fmt" "net" "time" @@ -13,6 +14,7 @@ import ( "github.com/pion/stun/v3" "github.com/pion/turn/v4/internal/allocation" "github.com/pion/turn/v4/internal/proto" + "github.com/pion/turn/v4/internal/server/authz" ) // Request contains all the state needed to process a single incoming datagram @@ -21,13 +23,14 @@ type Request struct { Conn net.PacketConn SrcAddr net.Addr Buff []byte + TLS *tls.ConnectionState // Server State AllocationManager *allocation.Manager NonceHash *NonceHash // User Configuration - AuthHandler func(username string, realm string, srcAddr net.Addr) (key []byte, ok bool) + Authorizer authz.Authorizer Log logging.LeveledLogger Realm string ChannelBindTimeout time.Duration diff --git a/internal/server/turn_test.go b/internal/server/turn_test.go index e4a3b947..8ec5c655 100644 --- a/internal/server/turn_test.go +++ b/internal/server/turn_test.go @@ -15,6 +15,7 @@ import ( "github.com/pion/stun/v3" "github.com/pion/turn/v4/internal/allocation" "github.com/pion/turn/v4/internal/proto" + "github.com/pion/turn/v4/internal/server/authz" "github.com/stretchr/testify/assert" ) @@ -90,9 +91,9 @@ func TestAllocationLifeTime(t *testing.T) { Conn: l, SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5000}, Log: logger, - AuthHandler: func(string, string, net.Addr) (key []byte, ok bool) { + Authorizer: authz.NewLegacy(func(string, string, net.Addr) (key []byte, ok bool) { return []byte(staticKey), true - }, + }), } fiveTuple := &allocation.FiveTuple{SrcAddr: r.SrcAddr, DstAddr: r.Conn.LocalAddr(), Protocol: allocation.UDP} diff --git a/internal/server/util.go b/internal/server/util.go index 7c01d329..def60e5e 100644 --- a/internal/server/util.go +++ b/internal/server/util.go @@ -11,6 +11,7 @@ import ( "github.com/pion/stun/v3" "github.com/pion/turn/v4/internal/proto" + "github.com/pion/turn/v4/internal/server/authz" ) const ( @@ -66,9 +67,9 @@ func authenticateRequest(r Request, m *stun.Message, callingMethod stun.Method) realmAttr := &stun.Realm{} badRequestMsg := buildMsg(m.TransactionID, stun.NewType(callingMethod, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeBadRequest}) - // No Auth handler is set, server is running in STUN only mode + // No Authorizer is set, server is running in STUN only mode // Respond with 400 so clients don't retry - if r.AuthHandler == nil { + if r.Authorizer == nil { sendErr := buildAndSend(r.Conn, r.SrcAddr, badRequestMsg...) return nil, false, sendErr } @@ -88,7 +89,12 @@ func authenticateRequest(r Request, m *stun.Message, callingMethod stun.Method) return nil, false, buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...) } - ourKey, ok := r.AuthHandler(usernameAttr.String(), realmAttr.String(), r.SrcAddr) + ourKey, ok := r.Authorizer.Authorize(&authz.RequestAttributes{ + Username: usernameAttr.String(), + Realm: realmAttr.String(), + SrcAddr: r.SrcAddr, + TLS: r.TLS, + }) if !ok { return nil, false, buildAndSendErr(r.Conn, r.SrcAddr, fmt.Errorf("%w %s", errNoSuchUser, usernameAttr.String()), badRequestMsg...) } diff --git a/lt_cred.go b/lt_cred.go index 42466c38..8617cbff 100644 --- a/lt_cred.go +++ b/lt_cred.go @@ -13,6 +13,7 @@ import ( //nolint:gci "time" "github.com/pion/logging" + "github.com/pion/turn/v4/internal/server/authz" ) // GenerateLongTermCredentials can be used to create credentials valid for [duration] time @@ -44,7 +45,7 @@ func longTermCredentials(username string, sharedSecret string) (string, error) { // NewLongTermAuthHandler returns a turn.AuthAuthHandler used with Long Term (or Time Windowed) Credentials. // See: https://datatracker.ietf.org/doc/html/rfc8489#section-9.2 -func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHandler { +func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) authz.LegacyAuthFunc { if l == nil { l = logging.NewDefaultLoggerFactory().NewLogger("turn") } @@ -74,7 +75,7 @@ func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHa // // The supported format of is timestamp:username, where username is an arbitrary user id and the // timestamp specifies the expiry of the credential. -func LongTermTURNRESTAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHandler { +func LongTermTURNRESTAuthHandler(sharedSecret string, l logging.LeveledLogger) authz.LegacyAuthFunc { if l == nil { l = logging.NewDefaultLoggerFactory().NewLogger("turn") } diff --git a/server.go b/server.go index 3b58938f..16ab8cd5 100644 --- a/server.go +++ b/server.go @@ -5,6 +5,7 @@ package turn import ( + "crypto/tls" "errors" "fmt" "net" @@ -14,6 +15,7 @@ import ( "github.com/pion/turn/v4/internal/allocation" "github.com/pion/turn/v4/internal/proto" "github.com/pion/turn/v4/internal/server" + "github.com/pion/turn/v4/internal/server/authz" ) const ( @@ -23,7 +25,7 @@ const ( // Server is an instance of the Pion TURN Server type Server struct { log logging.LeveledLogger - authHandler AuthHandler + authorizer authz.Authorizer realm string channelBindTimeout time.Duration nonceHash *server.NonceHash @@ -57,9 +59,16 @@ func NewServer(config ServerConfig) (*Server, error) { return nil, err } + // determine authorizer, prioritizing the + // (legacy) AuthHandler if it was provided. + authorizer := config.Authorizer + if config.AuthHandler != nil { + authorizer = authz.NewLegacy(config.AuthHandler) + } + s := &Server{ log: loggerFactory.NewLogger("turn"), - authHandler: config.AuthHandler, + authorizer: authorizer, realm: config.Realm, channelBindTimeout: config.ChannelBindTimeout, packetConnConfigs: config.PacketConnConfigs, @@ -79,7 +88,7 @@ func NewServer(config ServerConfig) (*Server, error) { } go func(cfg PacketConnConfig, am *allocation.Manager) { - s.readLoop(cfg.PacketConn, am) + s.readLoop(cfg.PacketConn, am, nil) if err := am.Close(); err != nil { s.log.Errorf("Failed to close AllocationManager: %s", err) @@ -151,7 +160,16 @@ func (s *Server) readListener(l net.Listener, am *allocation.Manager) { } go func() { - s.readLoop(NewSTUNConn(conn), am) + var tlsConnectionState *tls.ConnectionState + + // extract tls connection state if possible + tlsConn, ok := conn.(*tls.Conn) + if ok { + cs := tlsConn.ConnectionState() + tlsConnectionState = &cs + } + + s.readLoop(NewSTUNConn(conn), am, tlsConnectionState) // Delete allocation am.DeleteAllocation(&allocation.FiveTuple{ @@ -202,7 +220,7 @@ func (s *Server) createAllocationManager(addrGenerator RelayAddressGenerator, ha return am, err } -func (s *Server) readLoop(p net.PacketConn, allocationManager *allocation.Manager) { +func (s *Server) readLoop(p net.PacketConn, allocationManager *allocation.Manager, tls *tls.ConnectionState) { buf := make([]byte, s.inboundMTU) for { n, addr, err := p.ReadFrom(buf) @@ -219,8 +237,9 @@ func (s *Server) readLoop(p net.PacketConn, allocationManager *allocation.Manage Conn: p, SrcAddr: addr, Buff: buf[:n], + TLS: tls, Log: s.log, - AuthHandler: s.authHandler, + Authorizer: s.authorizer, Realm: s.realm, AllocationManager: allocationManager, ChannelBindTimeout: s.channelBindTimeout, diff --git a/server_config.go b/server_config.go index eab2988e..c53136b3 100644 --- a/server_config.go +++ b/server_config.go @@ -11,6 +11,7 @@ import ( "time" "github.com/pion/logging" + "github.com/pion/turn/v4/internal/server/authz" ) // RelayAddressGenerator is used to generate a RelayAddress when creating an allocation. @@ -93,9 +94,6 @@ func (c *ListenerConfig) validate() error { return c.RelayAddressGenerator.Validate() } -// AuthHandler is a callback used to handle incoming auth requests, allowing users to customize Pion TURN with custom behavior -type AuthHandler func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) - // GenerateAuthKey is a convenience function to easily generate keys in the format used by AuthHandler func GenerateAuthKey(username, realm, password string) []byte { // #nosec @@ -106,25 +104,28 @@ func GenerateAuthKey(username, realm, password string) []byte { // ServerConfig configures the Pion TURN Server type ServerConfig struct { - // PacketConnConfigs and ListenerConfigs are a list of all the turn listeners - // Each listener can have custom behavior around the creation of Relays + // PacketConnConfigs and ListenerConfigs are a list of all the turn listeners. + // Each listener can have custom behavior around the creation of Relays. PacketConnConfigs []PacketConnConfig ListenerConfigs []ListenerConfig // LoggerFactory must be set for logging from this server. LoggerFactory logging.LoggerFactory - // Realm sets the realm for this server + // Realm sets the realm for this server. Realm string - // AuthHandler is a callback used to handle incoming auth requests, allowing users to customize Pion TURN with custom behavior - AuthHandler AuthHandler + // Authorizer is user to handle incoming auth requests, allowing users to customize Pion TURN with custom behavior. + Authorizer authz.Authorizer // ChannelBindTimeout sets the lifetime of channel binding. Defaults to 10 minutes. ChannelBindTimeout time.Duration // Sets the server inbound MTU(Maximum transmition unit). Defaults to 1600 bytes. InboundMTU int + + // AuthHandler is deprecated, use Authorizer instead. + AuthHandler authz.LegacyAuthFunc } func (s *ServerConfig) validate() error {