Repository: pion/turn

Issue:
Add AllocationHandler
We should think about adding support for an `AllocationHandler` in a similar fashion like we already have for the `PermissionHandler`.

My motiviation behind it is the following:

We could use such a `AllocationHandler` to redirect clients via a `ALTERNATE-SERVER` response to other TURN servers.
However, we should give some flexibility to the user when and which alternate servers are signaled to the client.
This allows the user to define its own schemes to detect an overloaded server, or cycle through different available alternate servers. 

Discussion:
@rg0now: Seems like a useful thing to add. Can you be a bit more specific though? What's the API? Where would we call the allocation handler, inside `server.CreateAllocation`? What would our allocation handler receive as argument and what to return?@stv0g: > Where would we call the allocation handler, inside server.CreateAllocation?

This would be in [`internal/server.Server.handleAllocate()`](https://github.com/pion/turn/blob/f880e55089ad0e0cdb53c5a0e6b89954512dada6/internal/server/turn.go#L112).
Actually, the function already contains a comment:

https://github.com/pion/turn/blob/f880e55089ad0e0cdb53c5a0e6b89954512dada6/internal/server/turn.go#L112-L122

> What would our allocation handler receive as argument and what to return?

Good question. What about the following (just a first proposal):

```go
// AllocationHandler is a callback to ...
type AllocationHandler func(clientAddr net.Addr) (alternateServer *net.UDPAddr, stun.ErrorCode)
```@rg0now: Do you also want to handle the allocation quota case (as per point 7 above) in the same `AllocationHandler` callback, or it is intended only for signaling the `ALTERNATE-SERVER` response?

Why I'm asking this is that every now and then our users complain about the lack of allocation quotas. So far, I failed to find sane API to support this: should we limit allocations per source IP? Or per TURN username/credential? Total allocations?

I guess [coturn implements the first approach and the third one](https://matrix-org.github.io/synapse/latest/setup/turn/coturn.html):

```
# consider whether you want to limit the quota of relayed streams per user (or total) to avoid risk of DoS.
user-quota=12 # 4 streams per video call, so 12 streams = 3 simultaneous relayed calls per user.
total-quota=1200
```

If we follow this course then I think your `AllocationHandler` API could also be used to support user quotas and total quotas. Wdyt?
@stv0g: Hi @rg0now,

yes this was my plan to also cover quotas with this handler. The whole logic about which quotas are enforced should be user customizable with the handler. We could event provide a few generic handlers for covering the easy cases like you mentioned.

It would also be nice to have the TURN credentials as an argument for the handler to cover per-user quotas.
Hence, we must probably include the TURN credentials as an argument.

I initially also thought about using the handler for implementing custom authentication logic. However we already have the `AuthHandler` for this..

Solution patch:
diff --git a/internal/allocation/allocation.go b/internal/allocation/allocation.go
index 25c64d6..8fb4752 100644
--- a/internal/allocation/allocation.go
+++ b/internal/allocation/allocation.go
@@ -35,6 +35,8 @@ type Allocation struct {
 	channelBindings     []*ChannelBind
 	lifetimeTimer       *time.Timer
 	closed              chan any
+	username, realm     string
+	eventHandler        EventHandler
 	log                 logging.LeveledLogger
 
 	// Some clients (Firefox or others using resiprocate's nICE lib) may retry allocation
@@ -45,13 +47,19 @@ type Allocation struct {
 }
 
 // NewAllocation creates a new instance of NewAllocation.
-func NewAllocation(turnSocket net.PacketConn, fiveTuple *FiveTuple, log logging.LeveledLogger) *Allocation {
+func NewAllocation(
+	turnSocket net.PacketConn,
+	fiveTuple *FiveTuple,
+	eventHandler EventHandler,
+	log logging.LeveledLogger,
+) *Allocation {
 	return &Allocation{
-		TurnSocket:  turnSocket,
-		fiveTuple:   fiveTuple,
-		permissions: make(map[string]*Permission, 64),
-		closed:      make(chan any),
-		log:         log,
+		TurnSocket:   turnSocket,
+		fiveTuple:    fiveTuple,
+		permissions:  make(map[string]*Permission, 64),
+		closed:       make(chan any),
+		eventHandler: eventHandler,
+		log:          log,
 	}
 }
 
@@ -82,6 +90,14 @@ func (a *Allocation) AddPermission(perms *Permission) {
 	a.permissions[fingerprint] = perms
 	a.permissionsLock.Unlock()
 
+	if a.eventHandler.OnPermissionCreated != nil {
+		if u, ok := perms.Addr.(*net.UDPAddr); ok {
+			a.eventHandler.OnPermissionCreated(a.fiveTuple.SrcAddr, a.fiveTuple.DstAddr,
+				a.fiveTuple.Protocol.String(), a.username, a.realm,
+				a.RelayAddr, u.IP)
+		}
+	}
+
 	perms.start(permissionTimeout)
 }
 
@@ -90,6 +106,26 @@ func (a *Allocation) RemovePermission(addr net.Addr) {
 	a.permissionsLock.Lock()
 	defer a.permissionsLock.Unlock()
 	delete(a.permissions, ipnet.FingerprintAddr(addr))
+
+	if a.eventHandler.OnPermissionDeleted != nil {
+		if u, ok := addr.(*net.UDPAddr); ok {
+			a.eventHandler.OnPermissionDeleted(a.fiveTuple.SrcAddr, a.fiveTuple.DstAddr,
+				a.fiveTuple.Protocol.String(), a.username, a.realm,
+				a.RelayAddr, u.IP)
+		}
+	}
+}
+
+// ListPermissions returns the permissions associated with an allocation.
+func (a *Allocation) ListPermissions() []*Permission {
+	ps := []*Permission{}
+	a.permissionsLock.RLock()
+	defer a.permissionsLock.RUnlock()
+	for _, p := range a.permissions {
+		ps = append(ps, p)
+	}
+
+	return ps
 }
 
 // AddChannelBind adds a new ChannelBind to the allocation, it also updates the
@@ -114,6 +150,12 @@ func (a *Allocation) AddChannelBind(chanBind *ChannelBind, lifetime time.Duratio
 
 		// Channel binds also refresh permissions.
 		a.AddPermission(NewPermission(chanBind.Peer, a.log))
+
+		if a.eventHandler.OnChannelCreated != nil {
+			a.eventHandler.OnChannelCreated(a.fiveTuple.SrcAddr, a.fiveTuple.DstAddr,
+				a.fiveTuple.Protocol.String(), a.username, a.realm,
+				a.RelayAddr, chanBind.Peer, uint16(chanBind.Number))
+		}
 	} else {
 		channelByNumber.refresh(lifetime)
 
@@ -131,6 +173,12 @@ func (a *Allocation) RemoveChannelBind(number proto.ChannelNumber) bool {
 
 	for i := len(a.channelBindings) - 1; i >= 0; i-- {
 		if a.channelBindings[i].Number == number {
+			if a.eventHandler.OnChannelDeleted != nil {
+				a.eventHandler.OnChannelDeleted(a.fiveTuple.SrcAddr, a.fiveTuple.DstAddr,
+					a.fiveTuple.Protocol.String(), a.username, a.realm,
+					a.RelayAddr, a.channelBindings[i].Peer, uint16(a.channelBindings[i].Number))
+			}
+
 			a.channelBindings = append(a.channelBindings[:i], a.channelBindings[i+1:]...)
 
 			return true
@@ -166,6 +214,16 @@ func (a *Allocation) GetChannelByAddr(addr net.Addr) *ChannelBind {
 	return nil
 }
 
+// ListChannelBindings returns the channel bindings associated with an allocation.
+func (a *Allocation) ListChannelBindings() []*ChannelBind {
+	cs := []*ChannelBind{}
+	a.channelBindingsLock.RLock()
+	defer a.channelBindingsLock.RUnlock()
+	cs = append(cs, a.channelBindings...)
+
+	return cs
+}
+
 // Refresh updates the allocations lifetime.
 func (a *Allocation) Refresh(lifetime time.Duration) {
 	if !a.lifetimeTimer.Reset(lifetime) {
@@ -201,17 +259,15 @@ func (a *Allocation) Close() error {
 
 	a.lifetimeTimer.Stop()
 
-	a.permissionsLock.RLock()
-	for _, p := range a.permissions {
+	for _, p := range a.ListPermissions() {
+		a.RemovePermission(p.Addr)
 		p.lifetimeTimer.Stop()
 	}
-	a.permissionsLock.RUnlock()
 
-	a.channelBindingsLock.RLock()
-	for _, c := range a.channelBindings {
+	for _, c := range a.ListChannelBindings() {
+		a.RemoveChannelBind(c.Number)
 		c.lifetimeTimer.Stop()
 	}
-	a.channelBindingsLock.RUnlock()
 
 	return a.RelaySocket.Close()
 }
diff --git a/internal/allocation/allocation_manager.go b/internal/allocation/allocation_manager.go
index a3b011f..02d11d0 100644
--- a/internal/allocation/allocation_manager.go
+++ b/internal/allocation/allocation_manager.go
@@ -18,6 +18,7 @@ type ManagerConfig struct {
 	AllocatePacketConn func(network string, requestedPort int) (net.PacketConn, net.Addr, error)
 	AllocateConn       func(network string, requestedPort int) (net.Conn, net.Addr, error)
 	PermissionHandler  func(sourceAddr net.Addr, peerIP net.IP) bool
+	EventHandler       EventHandler
 }
 
 type reservation struct {
@@ -36,6 +37,7 @@ type Manager struct {
 	allocatePacketConn func(network string, requestedPort int) (net.PacketConn, net.Addr, error)
 	allocateConn       func(network string, requestedPort int) (net.Conn, net.Addr, error)
 	permissionHandler  func(sourceAddr net.Addr, peerIP net.IP) bool
+	EventHandler       EventHandler
 }
 
 // NewManager creates a new instance of Manager.
@@ -55,6 +57,7 @@ func NewManager(config ManagerConfig) (*Manager, error) {
 		allocatePacketConn: config.AllocatePacketConn,
 		allocateConn:       config.AllocateConn,
 		permissionHandler:  config.PermissionHandler,
+		EventHandler:       config.EventHandler,
 	}, nil
 }
 
@@ -94,6 +97,7 @@ func (m *Manager) CreateAllocation(
 	turnSocket net.PacketConn,
 	requestedPort int,
 	lifetime time.Duration,
+	username, realm string,
 ) (*Allocation, error) {
 	switch {
 	case fiveTuple == nil:
@@ -111,7 +115,9 @@ func (m *Manager) CreateAllocation(
 	if alloc := m.GetAllocation(fiveTuple); alloc != nil {
 		return nil, fmt.Errorf("%w: %v", errDupeFiveTuple, fiveTuple)
 	}
-	alloc := NewAllocation(turnSocket, fiveTuple, m.log)
+	alloc := NewAllocation(turnSocket, fiveTuple, m.EventHandler, m.log)
+	alloc.username = username
+	alloc.realm = realm
 
 	conn, relayAddr, err := m.allocatePacketConn("udp4", requestedPort)
 	if err != nil {
@@ -131,6 +137,11 @@ func (m *Manager) CreateAllocation(
 	m.allocations[fiveTuple.Fingerprint()] = alloc
 	m.lock.Unlock()
 
+	if m.EventHandler.OnAllocationCreated != nil {
+		m.EventHandler.OnAllocationCreated(fiveTuple.SrcAddr, fiveTuple.DstAddr,
+			fiveTuple.Protocol.String(), username, realm, relayAddr, requestedPort)
+	}
+
 	go alloc.packetHandler(m)
 
 	return alloc, nil
@@ -152,6 +163,11 @@ func (m *Manager) DeleteAllocation(fiveTuple *FiveTuple) {
 	if err := allocation.Close(); err != nil {
 		m.log.Errorf("Failed to close allocation: %v", err)
 	}
+
+	if m.EventHandler.OnAllocationDeleted != nil {
+		m.EventHandler.OnAllocationDeleted(fiveTuple.SrcAddr, fiveTuple.DstAddr,
+			fiveTuple.Protocol.String(), allocation.username, allocation.realm)
+	}
 }
 
 // CreateReservation stores the reservation for the token+port.
diff --git a/internal/allocation/event_handler.go b/internal/allocation/event_handler.go
new file mode 100644
index 0000000..1d9bd3f
--- /dev/null
+++ b/internal/allocation/event_handler.go
@@ -0,0 +1,47 @@
+// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
+// SPDX-License-Identifier: MIT
+
+package allocation
+
+import (
+	"net"
+)
+
+// EventHandler is a set of callbacks that the server will call at certain hook points during an
+// allocation's lifecycle. All events are reported with the context that identifies the allocation
+// triggering the event (source and destination address, protocol, username and realm used for
+// authenticating the allocation), plus additional callback specific parameters. It is OK to handle
+// only a subset of the callbacks.
+type EventHandler struct {
+	// OnAuth is called after an authentication request has been processed with the TURN method
+	// triggering the authentication request (either "Allocate", "Refresh" "CreatePermission",
+	// or "ChannelBind"), and the verdict is the authentication result.
+	OnAuth func(srcAddr, dstAddr net.Addr, protocol, username, realm string, method string, verdict bool)
+	// OnAllocationCreated is called after a new allocation has been made. The relayAddr
+	// argument specifies the relay address and requestedPort is the port requested by the
+	// client (if any).
+	OnAllocationCreated func(srcAddr, dstAddr net.Addr, protocol, username, realm string,
+		relayAddr net.Addr, requestedPort int)
+	// OnAllocationDeleted is called after an allocation has been removed.
+	OnAllocationDeleted func(srcAddr, dstAddr net.Addr, protocol, username, realm string)
+	// OnAllocationError is called when the readloop hdndling an allocation exits with an
+	// error with an error message.
+	OnAllocationError func(srcAddr, dstAddr net.Addr, protocol, message string)
+	// OnPermissionCreated is called after a new permission has been made to an IP address.
+	OnPermissionCreated func(srcAddr, dstAddr net.Addr, protocol, username, realm string,
+		relayAddr net.Addr, peer net.IP)
+	// OnPermissionDeleted is called after a permission for a given IP address has been
+	// removed.
+	OnPermissionDeleted func(srcAddr, dstAddr net.Addr, protocol, username, realm string,
+		relayAddr net.Addr, peer net.IP)
+	// OnChannelCreated is called after a new channel has been made. The relay address, the
+	// peer address and the channel number can be used to uniquely identify the channel
+	// created.
+	OnChannelCreated func(srcAddr, dstAddr net.Addr, protocol, username, realm string,
+		relayAddr, peer net.Addr, channelNumber uint16)
+	// OnChannelDeleted is called after a channel has been removed from the server. The relay
+	// address, the peer address and the channel number can be used to uniquely identify the
+	// channel deleted.
+	OnChannelDeleted func(srcAddr, dstAddr net.Addr, protocol, username, realm string,
+		relayAddr, peer net.Addr, channelNumber uint16)
+}
diff --git a/internal/allocation/five_tuple.go b/internal/allocation/five_tuple.go
index 1476161..b9eba87 100644
--- a/internal/allocation/five_tuple.go
+++ b/internal/allocation/five_tuple.go
@@ -16,6 +16,17 @@ const (
 	TCP
 )
 
+func (p Protocol) String() string {
+	switch p {
+	case UDP:
+		return "UDP"
+	case TCP:
+		return "TCP"
+	default:
+		return ""
+	}
+}
+
 // FiveTuple is the combination (client IP address and port, server IP
 // address and port, and transport protocol (currently one of UDP,
 // TCP, or TLS)) used to communicate between the client and the
diff --git a/internal/server/server.go b/internal/server/server.go
index 4f5c314..750cb70 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -27,7 +27,8 @@ type Request struct {
 	NonceHash         *NonceHash
 
 	// User Configuration
-	AuthHandler        func(username string, realm string, srcAddr net.Addr) (key []byte, ok bool)
+	AuthHandler func(username string, realm string, srcAddr net.Addr) (key []byte, ok bool)
+
 	Log                logging.LeveledLogger
 	Realm              string
 	ChannelBindTimeout time.Duration
diff --git a/internal/server/turn.go b/internal/server/turn.go
index d20c5dc..da2c18e 100644
--- a/internal/server/turn.go
+++ b/internal/server/turn.go
@@ -145,6 +145,12 @@ func handleAllocateRequest(req Request, stunMsg *stun.Message) error { //nolint:
 		}
 	}
 
+	// Parse realm and username (already checked in authenticateRequest)
+	realmAttr := &stun.Realm{}
+	_ = realmAttr.GetFrom(stunMsg)
+	usernameAttr := &stun.Username{}
+	_ = usernameAttr.GetFrom(stunMsg)
+
 	// 7. At any point, the server MAY choose to reject the request with a
 	//    486 (Allocation Quota Reached) error if it feels the client is
 	//    trying to exceed some locally defined allocation quota.  The
@@ -161,7 +167,10 @@ func handleAllocateRequest(req Request, stunMsg *stun.Message) error { //nolint:
 		fiveTuple,
 		req.Conn,
 		requestedPort,
-		lifetimeDuration)
+		lifetimeDuration,
+		usernameAttr.String(),
+		realmAttr.String(),
+	)
 	if err != nil {
 		return buildAndSendErr(req.Conn, req.SrcAddr, err, insufficientCapacityMsg...)
 	}
diff --git a/internal/server/util.go b/internal/server/util.go
index a16bf43..65ce103 100644
--- a/internal/server/util.go
+++ b/internal/server/util.go
@@ -114,12 +114,41 @@ func authenticateRequest(req Request, stunMsg *stun.Message, callingMethod stun.
 	}
 
 	if err := stun.MessageIntegrity(ourKey).Check(stunMsg); err != nil {
+		genAuthEvent(req, stunMsg, callingMethod, false)
+
 		return nil, false, buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...)
 	}
 
+	genAuthEvent(req, stunMsg, callingMethod, true)
+
 	return stun.MessageIntegrity(ourKey), true, nil
 }
 
+func genAuthEvent(req Request, stunMsg *stun.Message, callingMethod stun.Method, verdict bool) {
+	if req.AllocationManager.EventHandler.OnAuth == nil {
+		return
+	}
+
+	realmAttr := &stun.Realm{}
+	if err := realmAttr.GetFrom(stunMsg); err != nil {
+		return
+	}
+
+	usernameAttr := &stun.Username{}
+	if err := usernameAttr.GetFrom(stunMsg); err != nil {
+		return
+	}
+
+	transportAttr := &proto.RequestedTransport{}
+	if err := transportAttr.GetFrom(stunMsg); err != nil {
+		transportAttr = &proto.RequestedTransport{Protocol: proto.ProtoUDP}
+	}
+
+	req.AllocationManager.EventHandler.OnAuth(req.SrcAddr, req.Conn.LocalAddr(),
+		transportAttr.Protocol.String(), usernameAttr.String(), realmAttr.String(),
+		callingMethod.String(), verdict)
+}
+
 func allocationLifeTime(m *stun.Message) time.Duration {
 	lifetimeDuration := proto.DefaultLifetime
 
diff --git a/server.go b/server.go
index 5d03643..b3d4053 100644
--- a/server.go
+++ b/server.go
@@ -27,6 +27,7 @@ type Server struct {
 	realm              string
 	channelBindTimeout time.Duration
 	nonceHash          *server.NonceHash
+	eventHandler       EventHandler
 
 	packetConnConfigs  []PacketConnConfig
 	listenerConfigs    []ListenerConfig
@@ -64,6 +65,7 @@ func NewServer(config ServerConfig) (*Server, error) { //nolint:gocognit,cyclop
 		listenerConfigs:    config.ListenerConfigs,
 		nonceHash:          nonceHash,
 		inboundMTU:         mtu,
+		eventHandler:       config.EventHandler,
 	}
 
 	if server.channelBindTimeout == 0 {
@@ -196,6 +198,7 @@ func (s *Server) createAllocationManager(
 		AllocatePacketConn: addrGenerator.AllocatePacketConn,
 		AllocateConn:       addrGenerator.AllocateConn,
 		PermissionHandler:  handler,
+		EventHandler:       s.eventHandler,
 		LeveledLogger:      s.log,
 	})
 	if err != nil {
@@ -233,6 +236,9 @@ func (s *Server) readLoop(conn net.PacketConn, allocationManager *allocation.Man
 			ChannelBindTimeout: s.channelBindTimeout,
 			NonceHash:          s.nonceHash,
 		}); err != nil {
+			if s.eventHandler.OnAllocationError != nil {
+				s.eventHandler.OnAllocationError(addr, conn.LocalAddr(), allocation.UDP.String(), err.Error())
+			}
 			s.log.Debugf("Failed to handle datagram: %v", err)
 		}
 	}
diff --git a/server_config.go b/server_config.go
index 8f27614..9ccba50 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/allocation"
 )
 
 // RelayAddressGenerator is used to generate a RelayAddress when creating an allocation.
@@ -108,6 +109,10 @@ func GenerateAuthKey(username, realm, password string) []byte {
 	return h.Sum(nil)
 }
 
+// EventHandler is a set of callbacks that the server will call at certain hook points during an
+// allocation's lifecycle.
+type EventHandler = allocation.EventHandler
+
 // ServerConfig configures the Pion TURN Server.
 type ServerConfig struct {
 	// PacketConnConfigs and ListenerConfigs are a list of all the turn listeners
@@ -125,6 +130,9 @@ type ServerConfig struct {
 	// allowing users to customize Pion TURN with custom behavior
 	AuthHandler AuthHandler
 
+	// EventHandlers is a set of callbacks for tracking allocation lifecycle.
+	EventHandler EventHandler
+
 	// ChannelBindTimeout sets the lifetime of channel binding. Defaults to 10 minutes.
 	ChannelBindTimeout time.Duration
 



Test patch:
diff --git a/internal/allocation/allocation_manager_test.go b/internal/allocation/allocation_manager_test.go
index 1beb8cd..9bf74d7 100644
--- a/internal/allocation/allocation_manager_test.go
+++ b/internal/allocation/allocation_manager_test.go
@@ -52,15 +52,15 @@ func subTestCreateInvalidAllocation(t *testing.T, turnSocket net.PacketConn) {
 	m, err := newTestManager()
 	assert.NoError(t, err)
 
-	a, err := m.CreateAllocation(nil, turnSocket, 0, proto.DefaultLifetime)
+	a, err := m.CreateAllocation(nil, turnSocket, 0, proto.DefaultLifetime, "", "")
 	assert.Nil(t, a, "Illegally created allocation with nil FiveTuple")
 	assert.Error(t, err, "Illegally created allocation with nil FiveTuple")
 
-	a, err = m.CreateAllocation(randomFiveTuple(), nil, 0, proto.DefaultLifetime)
+	a, err = m.CreateAllocation(randomFiveTuple(), nil, 0, proto.DefaultLifetime, "", "")
 	assert.Nil(t, a, "Illegally created allocation with nil turnSocket")
 	assert.Error(t, err, "Illegally created allocation with nil turnSocket")
 
-	a, err = m.CreateAllocation(randomFiveTuple(), turnSocket, 0, 0)
+	a, err = m.CreateAllocation(randomFiveTuple(), turnSocket, 0, 0, "", "")
 	assert.Nil(t, a, "Illegally created allocation with 0 lifetime")
 	assert.Error(t, err, "Illegally created allocation with 0 lifetime")
 }
@@ -73,7 +73,7 @@ func subTestCreateAllocation(t *testing.T, turnSocket net.PacketConn) {
 	assert.NoError(t, err)
 
 	fiveTuple := randomFiveTuple()
-	a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime)
+	a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime, "", "")
 	assert.NotNil(t, a, "Failed to create allocation")
 	assert.NoError(t, err, "Failed to create allocation")
 
@@ -89,11 +89,11 @@ func subTestCreateAllocationDuplicateFiveTuple(t *testing.T, turnSocket net.Pack
 	assert.NoError(t, err)
 
 	fiveTuple := randomFiveTuple()
-	a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime)
+	a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime, "", "")
 	assert.NotNil(t, a, "Failed to create allocation")
 	assert.NoError(t, err, "Failed to create allocation")
 
-	a, err = m.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime)
+	a, err = m.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime, "", "")
 	assert.Nil(t, a, "Was able to create allocation with same FiveTuple twice")
 	assert.Error(t, err, "Was able to create allocation with same FiveTuple twice")
 }
@@ -105,7 +105,7 @@ func subTestDeleteAllocation(t *testing.T, turnSocket net.PacketConn) {
 	assert.NoError(t, err)
 
 	fiveTuple := randomFiveTuple()
-	a, err := manager.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime)
+	a, err := manager.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime, "", "")
 	assert.NotNil(t, a, "Failed to create allocation")
 	assert.NoError(t, err, "Failed to create allocation")
 
@@ -130,7 +130,7 @@ func subTestAllocationTimeout(t *testing.T, turnSocket net.PacketConn) {
 	for index := range allocations {
 		fiveTuple := randomFiveTuple()
 
-		a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, lifetime)
+		a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, lifetime, "", "")
 		assert.NoErrorf(t, err, "Failed to create allocation with %v", fiveTuple)
 
 		allocations[index] = a
@@ -152,9 +152,9 @@ func subTestManagerClose(t *testing.T, turnSocket net.PacketConn) {
 
 	allocations := make([]*Allocation, 2)
 
-	a1, _ := manager.CreateAllocation(randomFiveTuple(), turnSocket, 0, time.Second)
+	a1, _ := manager.CreateAllocation(randomFiveTuple(), turnSocket, 0, time.Second, "", "")
 	allocations[0] = a1
-	a2, _ := manager.CreateAllocation(randomFiveTuple(), turnSocket, 0, time.Minute)
+	a2, _ := manager.CreateAllocation(randomFiveTuple(), turnSocket, 0, time.Minute, "", "")
 	allocations[1] = a2
 
 	// Make a1 timeout
diff --git a/internal/allocation/allocation_test.go b/internal/allocation/allocation_test.go
index 1259fae..a2461a2 100644
--- a/internal/allocation/allocation_test.go
+++ b/internal/allocation/allocation_test.go
@@ -48,7 +48,7 @@ func TestAllocation(t *testing.T) {
 func subTestGetPermission(t *testing.T) {
 	t.Helper()
 
-	alloc := NewAllocation(nil, nil, nil)
+	alloc := NewAllocation(nil, nil, EventHandler{}, nil)
 
 	addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:3478")
 	assert.NoError(t, err)
@@ -86,7 +86,7 @@ func subTestGetPermission(t *testing.T) {
 func subTestAddPermission(t *testing.T) {
 	t.Helper()
 
-	alloc := NewAllocation(nil, nil, nil)
+	alloc := NewAllocation(nil, nil, EventHandler{}, nil)
 
 	addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:3478")
 	assert.NoError(t, err)
@@ -105,7 +105,7 @@ func subTestAddPermission(t *testing.T) {
 func subTestRemovePermission(t *testing.T) {
 	t.Helper()
 
-	alloc := NewAllocation(nil, nil, nil)
+	alloc := NewAllocation(nil, nil, EventHandler{}, nil)
 
 	addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:3478")
 	assert.NoError(t, err)
@@ -128,7 +128,7 @@ func subTestRemovePermission(t *testing.T) {
 func subTestAddChannelBind(t *testing.T) {
 	t.Helper()
 
-	alloc := NewAllocation(nil, nil, nil)
+	alloc := NewAllocation(nil, nil, EventHandler{}, nil)
 
 	addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:3478")
 	assert.NoError(t, err)
@@ -152,7 +152,7 @@ func subTestAddChannelBind(t *testing.T) {
 func subTestGetChannelByNumber(t *testing.T) {
 	t.Helper()
 
-	alloc := NewAllocation(nil, nil, nil)
+	alloc := NewAllocation(nil, nil, EventHandler{}, nil)
 
 	addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:3478")
 	assert.NoError(t, err)
@@ -171,7 +171,7 @@ func subTestGetChannelByNumber(t *testing.T) {
 func subTestGetChannelByAddr(t *testing.T) {
 	t.Helper()
 
-	alloc := NewAllocation(nil, nil, nil)
+	alloc := NewAllocation(nil, nil, EventHandler{}, nil)
 
 	addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:3478")
 	assert.NoError(t, err)
@@ -191,7 +191,7 @@ func subTestGetChannelByAddr(t *testing.T) {
 func subTestRemoveChannelBind(t *testing.T) {
 	t.Helper()
 
-	alloc := NewAllocation(nil, nil, nil)
+	alloc := NewAllocation(nil, nil, EventHandler{}, nil)
 
 	addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:3478")
 	assert.NoError(t, err)
@@ -212,7 +212,7 @@ func subTestRemoveChannelBind(t *testing.T) {
 func subTestAllocationRefresh(t *testing.T) {
 	t.Helper()
 
-	alloc := NewAllocation(nil, nil, nil)
+	alloc := NewAllocation(nil, nil, EventHandler{}, nil)
 
 	var wg sync.WaitGroup
 	wg.Add(1)
@@ -234,7 +234,7 @@ func subTestAllocationClose(t *testing.T) {
 	l, err := net.ListenPacket(network, "0.0.0.0:0")
 	assert.NoError(t, err)
 
-	alloc := NewAllocation(nil, nil, nil)
+	alloc := NewAllocation(nil, nil, EventHandler{}, nil)
 	alloc.RelaySocket = l
 	// Add mock lifetimeTimer
 	alloc.lifetimeTimer = time.AfterFunc(proto.DefaultLifetime, func() {})
@@ -285,7 +285,7 @@ func subTestPacketHandler(t *testing.T) {
 	alloc, err := manager.CreateAllocation(&FiveTuple{
 		SrcAddr: clientListener.LocalAddr(),
 		DstAddr: turnSocket.LocalAddr(),
-	}, turnSocket, 0, proto.DefaultLifetime)
+	}, turnSocket, 0, proto.DefaultLifetime, "", "")
 
 	assert.NoError(t, err, "should succeed")
 
@@ -345,16 +345,16 @@ func subTestPacketHandler(t *testing.T) {
 func subTestResponseCache(t *testing.T) {
 	t.Helper()
 
-	a := NewAllocation(nil, nil, nil)
+	alloc := NewAllocation(nil, nil, EventHandler{}, nil)
 	transactionID := [stun.TransactionIDSize]byte{1, 2, 3}
 	responseAttrs := []stun.Setter{
 		&proto.Lifetime{
 			Duration: proto.DefaultLifetime,
 		},
 	}
-	a.SetResponseCache(transactionID, responseAttrs)
+	alloc.SetResponseCache(transactionID, responseAttrs)
 
-	cacheID, cacheAttr := a.GetResponseCache()
+	cacheID, cacheAttr := alloc.GetResponseCache()
 	assert.Equal(t, transactionID, cacheID)
 	assert.Equal(t, responseAttrs, cacheAttr)
 }
diff --git a/internal/allocation/channel_bind_test.go b/internal/allocation/channel_bind_test.go
index 253c9ab..ce25557 100644
--- a/internal/allocation/channel_bind_test.go
+++ b/internal/allocation/channel_bind_test.go
@@ -37,7 +37,7 @@ func TestChannelBindReset(t *testing.T) {
 }
 
 func newChannelBind(lifetime time.Duration) *ChannelBind {
-	a := NewAllocation(nil, nil, nil)
+	a := NewAllocation(nil, nil, EventHandler{}, nil)
 
 	addr, _ := net.ResolveUDPAddr("udp", "0.0.0.0:0")
 	c := &ChannelBind{
diff --git a/internal/server/turn_test.go b/internal/server/turn_test.go
index bc7b44b..277d255 100644
--- a/internal/server/turn_test.go
+++ b/internal/server/turn_test.go
@@ -91,7 +91,7 @@ func TestAllocationLifeTime(t *testing.T) {
 
 		fiveTuple := &allocation.FiveTuple{SrcAddr: req.SrcAddr, DstAddr: req.Conn.LocalAddr(), Protocol: allocation.UDP}
 
-		_, err = req.AllocationManager.CreateAllocation(fiveTuple, req.Conn, 0, time.Hour)
+		_, err = req.AllocationManager.CreateAllocation(fiveTuple, req.Conn, 0, time.Hour, "", "")
 		assert.NoError(t, err)
 
 		assert.NotNil(t, req.AllocationManager.GetAllocation(fiveTuple))
diff --git a/server_test.go b/server_test.go
index 8865e66..b191963 100644
--- a/server_test.go
+++ b/server_test.go
@@ -9,6 +9,7 @@ package turn
 import (
 	"fmt"
 	"net"
+	"sync/atomic"
 	"syscall"
 	"testing"
 	"time"
@@ -21,6 +22,38 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+const (
+	timeout  = 200 * time.Millisecond
+	interval = 50 * time.Millisecond
+	stunAddr = "1.2.3.4:3478"
+	turnAddr = "1.2.3.4:3478"
+)
+
+type EventHandlerType int
+
+const (
+	unknownEvent EventHandlerType = iota
+	onAuth
+	onAllocationCreated
+	onAllocationDeleted
+	onAllocationError
+	onPermissionCreated
+	onPermissionDeleted
+	onChannelCreated
+	onChannelDeleted
+)
+
+// EventHandlerArgs is a set of arguments passed from the low-level event callbacks to the server.
+type eventHandlerArgs struct {
+	Type                                   EventHandlerType
+	srcAddr, dstAddr, relayAddr            net.Addr
+	protocol                               string
+	username, realm, method, message, peer string
+	verdict                                bool
+	requestedPort                          int
+	channelNumber                          uint16
+}
+
 func TestServer(t *testing.T) { //nolint:maintidx
 	lim := test.TimeOut(time.Second * 30)
 	defer lim.Stop()
@@ -226,12 +259,12 @@ func TestServer(t *testing.T) { //nolint:maintidx
 				{
 					PacketConn: udpListener,
 					RelayAddressGenerator: &RelayAddressGeneratorStatic{
-						RelayAddress: net.ParseIP("127.0.0.1"),
+						RelayAddress: net.IPv4(127, 0, 0, 1),
 						Address:      "0.0.0.0",
 					},
 					PermissionHandler: func(src net.Addr, peer net.IP) bool {
 						return src.String() == "127.0.0.1:54321" &&
-							peer.Equal(net.ParseIP("127.0.0.4"))
+							peer.Equal(net.IPv4(127, 0, 0, 4))
 					},
 				},
 			},
@@ -381,8 +414,11 @@ func (v *VNet) Close() error {
 	return v.wan.Stop()
 }
 
-func buildVNet() (*VNet, error) { //nolint:cyclop
+func buildVNet(handler *EventHandler) (*VNet, error) { //nolint:cyclop
 	loggerFactory := logging.NewDefaultLoggerFactory()
+	if handler == nil {
+		handler = &EventHandler{}
+	}
 
 	// WAN
 	wan, err := vnet.NewRouter(&vnet.RouterConfig{
@@ -451,11 +487,15 @@ func buildVNet() (*VNet, error) { //nolint:cyclop
 	// Start server...
 	credMap := map[string][]byte{"user": GenerateAuthKey("user", "pion.ly", "pass")}
 
-	udpListener, err := net0.ListenPacket("udp4", "0.0.0.0:3478")
+	udpListener, err := net0.ListenPacket("udp4", "1.2.3.4:3478")
 	if err != nil {
 		return nil, err
 	}
 
+	if handler == nil {
+		handler = &EventHandler{}
+	}
+
 	server, err := NewServer(ServerConfig{
 		AuthHandler: func(username, _ string, _ net.Addr) (key []byte, ok bool) {
 			if pw, ok := credMap[username]; ok {
@@ -464,7 +504,8 @@ func buildVNet() (*VNet, error) { //nolint:cyclop
 
 			return nil, false
 		},
-		Realm: "pion.ly",
+		Realm:        "pion.ly",
+		EventHandler: *handler,
 		PacketConnConfigs: []PacketConnConfig{
 			{
 				PacketConn: udpListener,
@@ -503,7 +544,141 @@ func buildVNet() (*VNet, error) { //nolint:cyclop
 	}, nil
 }
 
-func TestServerVNet(t *testing.T) {
+func testEventHandler(ch chan eventHandlerArgs, authCounter *atomic.Int32) *EventHandler {
+	return &EventHandler{
+		OnAuth: func(srcAddr, dstAddr net.Addr,
+			protocol, username, realm string,
+			method string,
+			verdict bool,
+		) {
+			if ch != nil {
+				ch <- eventHandlerArgs{
+					Type: onAuth, srcAddr: srcAddr, dstAddr: dstAddr,
+					protocol: protocol, username: username, realm: realm,
+					method: method, verdict: verdict,
+				}
+			}
+			authCounter.Add(1)
+		},
+		OnAllocationCreated: func(srcAddr, dstAddr net.Addr,
+			protocol, username, realm string,
+			relayAddr net.Addr,
+			requestedPort int,
+		) {
+			if ch != nil {
+				ch <- eventHandlerArgs{
+					Type: onAllocationCreated, srcAddr: srcAddr, dstAddr: dstAddr,
+					protocol: protocol, username: username, realm: realm,
+					relayAddr: relayAddr, requestedPort: requestedPort,
+				}
+			}
+		},
+		OnAllocationDeleted: func(srcAddr, dstAddr net.Addr, protocol, username, realm string) {
+			if ch != nil {
+				ch <- eventHandlerArgs{
+					Type: onAllocationDeleted, srcAddr: srcAddr, dstAddr: dstAddr,
+					protocol: protocol, username: username, realm: realm,
+				}
+			}
+		},
+		OnAllocationError: func(srcAddr, dstAddr net.Addr, protocol, message string) {
+			if ch != nil {
+				ch <- eventHandlerArgs{
+					Type: onAllocationError, srcAddr: srcAddr, dstAddr: dstAddr,
+					protocol: protocol, message: message,
+				}
+			}
+		},
+		OnPermissionCreated: func(srcAddr, dstAddr net.Addr,
+			protocol, username, realm string,
+			relayAddr net.Addr,
+			peer net.IP,
+		) {
+			if ch != nil {
+				ch <- eventHandlerArgs{
+					Type: onPermissionCreated, srcAddr: srcAddr, dstAddr: dstAddr,
+					protocol: protocol, username: username, realm: realm,
+					relayAddr: relayAddr, peer: peer.String(),
+				}
+			}
+		},
+		OnPermissionDeleted: func(srcAddr, dstAddr net.Addr,
+			protocol, username, realm string,
+			relayAddr net.Addr,
+			peer net.IP,
+		) {
+			if ch != nil {
+				ch <- eventHandlerArgs{
+					Type: onPermissionDeleted, srcAddr: srcAddr, dstAddr: dstAddr,
+					protocol: protocol, username: username, realm: realm,
+					relayAddr: relayAddr, peer: peer.String(),
+				}
+			}
+		},
+		OnChannelCreated: func(srcAddr, dstAddr net.Addr,
+			protocol, username, realm string,
+			relayAddr, peer net.Addr,
+			channelNumber uint16,
+		) {
+			if ch != nil {
+				ch <- eventHandlerArgs{
+					Type: onChannelCreated, srcAddr: srcAddr, dstAddr: dstAddr,
+					protocol: protocol, username: username, realm: realm,
+					relayAddr: relayAddr, peer: peer.String(),
+					channelNumber: channelNumber,
+				}
+			}
+		},
+		OnChannelDeleted: func(srcAddr, dstAddr net.Addr,
+			protocol, username, realm string,
+			relayAddr, peer net.Addr,
+			channelNumber uint16,
+		) {
+			if ch != nil {
+				ch <- eventHandlerArgs{
+					Type: onChannelDeleted, srcAddr: srcAddr, dstAddr: dstAddr,
+					protocol: protocol, username: username, realm: realm,
+					relayAddr: relayAddr, peer: peer.String(),
+					channelNumber: channelNumber,
+				}
+			}
+		},
+	}
+}
+
+func expectEvent(ch chan eventHandlerArgs) (eventHandlerArgs, bool) {
+	select {
+	case res := <-ch:
+		return res, true
+	case <-time.After(timeout):
+		return eventHandlerArgs{}, false
+	}
+}
+
+func checkAllocationEvent(t *testing.T, event eventHandlerArgs) {
+	t.Helper()
+	udpAddr, ok := event.srcAddr.(*net.UDPAddr)
+	assert.True(t, ok)
+	assert.Equal(t, udpAddr.IP.String(), "5.6.7.8")
+
+	udpAddr, ok = event.dstAddr.(*net.UDPAddr)
+	assert.True(t, ok)
+	assert.Equal(t, udpAddr.IP.String(), "1.2.3.4")
+
+	assert.Equal(t, "UDP", event.protocol)
+	assert.Equal(t, "user", event.username)
+	assert.Equal(t, "pion.ly", event.realm)
+}
+
+func checkAuthEvent(t *testing.T, event eventHandlerArgs, method string, verdict bool) {
+	t.Helper()
+	assert.Equal(t, onAuth, event.Type, "should receive an OnAuth event")
+	checkAllocationEvent(t, event)
+	assert.Equal(t, event.method, method)
+	assert.Equal(t, event.verdict, verdict)
+}
+
+func TestServerVNet(t *testing.T) { //nolint:maintidx
 	lim := test.TimeOut(time.Second * 30)
 	defer lim.Stop()
 
@@ -514,7 +689,7 @@ func TestServerVNet(t *testing.T) {
 	log := loggerFactory.NewLogger("test")
 
 	t.Run("SendBindingRequest", func(t *testing.T) {
-		v, err := buildVNet()
+		v, err := buildVNet(nil)
 		assert.NoError(t, err)
 		defer func() {
 			assert.NoError(t, v.Close())
@@ -526,8 +701,6 @@ func TestServerVNet(t *testing.T) {
 			assert.NoError(t, lconn.Close())
 		}()
 
-		stunAddr := "1.2.3.4:3478"
-
 		log.Debug("creating a client.")
 		client, err := NewClient(&ClientConfig{
 			STUNServerAddr: stunAddr,
@@ -549,6 +722,207 @@ func TestServerVNet(t *testing.T) {
 		// to the LAN router.
 		assert.True(t, udpAddr.IP.Equal(net.IPv4(5, 6, 7, 8)), "should match")
 	})
+
+	t.Run("AllocationLifecycle", func(t *testing.T) {
+		counter := &atomic.Int32{}
+		events := make(chan eventHandlerArgs, 5)
+		defer close(events)
+		virtNet, err := buildVNet(testEventHandler(events, counter))
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, virtNet.Close())
+		}()
+
+		lconn, err := virtNet.netL0.ListenPacket("udp4", "0.0.0.0:0")
+		assert.NoError(t, err, "should succeed")
+		defer func() {
+			assert.NoError(t, lconn.Close())
+		}()
+
+		log.Debug("creating a client.")
+		client, err := NewClient(&ClientConfig{
+			TURNServerAddr: turnAddr,
+			Conn:           lconn,
+			Username:       "user",
+			Password:       "pass",
+			Realm:          "pion.ly",
+			LoggerFactory:  loggerFactory,
+		})
+		assert.NoError(t, err, "should succeed")
+		assert.NoError(t, client.Listen(), "should succeed")
+		defer client.Close()
+
+		log.Debug("sending an allocate request.")
+		relayConn, err := client.Allocate()
+		assert.NoError(t, err, "should succeed")
+
+		event, ok := expectEvent(events)
+		assert.True(t, ok, "should receive an event")
+		checkAuthEvent(t, event, "Allocate", true)
+
+		event, ok = expectEvent(events)
+		assert.True(t, ok, "should receive an event")
+		assert.Equal(t, onAllocationCreated, event.Type, "should receive an OnAllocationCreated event")
+		checkAllocationEvent(t, event)
+		assert.Equal(t, relayConn.LocalAddr().String(), event.relayAddr.String())
+		assert.Equal(t, 0, event.requestedPort)
+
+		log.Debug("Sending test packet")
+		peerAddr := &net.UDPAddr{IP: net.IPv4(1, 2, 3, 5), Port: 80}
+		_, err = relayConn.WriteTo([]byte("test"), peerAddr)
+		assert.NoError(t, err, "should succeed")
+
+		event, ok = expectEvent(events)
+		assert.True(t, ok, "should receive an event")
+		checkAuthEvent(t, event, "CreatePermission", true)
+
+		event, ok = expectEvent(events)
+		assert.True(t, ok, "should receive an event")
+		assert.Equal(t, onPermissionCreated, event.Type, "should receive an OnPermissionCreated event")
+		checkAllocationEvent(t, event)
+		assert.Equal(t, relayConn.LocalAddr().String(), event.relayAddr.String())
+		assert.Equal(t, 0, event.requestedPort)
+		assert.Equal(t, "1.2.3.5", event.peer)
+
+		log.Debug("Forcing the creation of a channel")
+		_, err = relayConn.WriteTo([]byte("test"), peerAddr)
+		assert.NoError(t, err, "should succeed")
+
+		event, ok = expectEvent(events)
+		assert.True(t, ok, "should receive an event")
+		checkAuthEvent(t, event, "ChannelBind", true)
+
+		event, ok = expectEvent(events)
+		assert.True(t, ok, "should receive an event")
+		assert.Equal(t, onChannelCreated, event.Type, "should receive an OnChannelCreated event")
+		checkAllocationEvent(t, event)
+		assert.Equal(t, relayConn.LocalAddr().String(), event.relayAddr.String())
+
+		// obtain the channel id
+		a := virtNet.server.allocationManagers[0].GetAllocation(&allocation.FiveTuple{
+			Protocol: allocation.UDP,
+			SrcAddr:  event.srcAddr,
+			DstAddr:  event.dstAddr,
+		})
+		assert.NotNil(t, a)
+		channelBind := a.GetChannelByAddr(peerAddr)
+		assert.NotNil(t, channelBind)
+		assert.Equal(t, channelBind.Number, proto.ChannelNumber(event.channelNumber))
+
+		log.Debug("Closing relay connection")
+		assert.NoError(t, relayConn.Close(), "relay conn close should succeed")
+
+		event, ok = expectEvent(events)
+		assert.True(t, ok, "should receive an event")
+		assert.Equal(t, onAuth, event.Type, "should receive an OnAuth event")
+		checkAuthEvent(t, event, "Refresh", true)
+
+		event, ok = expectEvent(events)
+		assert.True(t, ok, "should receive an event")
+		assert.Equal(t, onPermissionDeleted, event.Type, "should receive an OnPermissionDeleted event")
+		checkAllocationEvent(t, event)
+		assert.Equal(t, relayConn.LocalAddr().String(), event.relayAddr.String())
+		assert.Equal(t, "1.2.3.5", event.peer)
+
+		event, ok = expectEvent(events)
+		assert.True(t, ok, "should receive an event")
+		assert.Equal(t, onChannelDeleted, event.Type, "should receive an OnChannelDeleted event")
+		checkAllocationEvent(t, event)
+		assert.Equal(t, relayConn.LocalAddr().String(), event.relayAddr.String())
+		assert.Equal(t, channelBind.Number, proto.ChannelNumber(event.channelNumber))
+
+		event, ok = expectEvent(events)
+		assert.True(t, ok, "should receive an event")
+		assert.Equal(t, onAllocationDeleted, event.Type, "should receive an OnAllocationDeleted event")
+		checkAllocationEvent(t, event)
+
+		assert.Eventually(t, func() bool { return counter.Load() == 4 }, timeout, interval)
+	})
+
+	t.Run("AuthEventHandlerSuccess", func(t *testing.T) {
+		counter := &atomic.Int32{}
+		virtNet, err := buildVNet(testEventHandler(nil, counter))
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, virtNet.Close())
+		}()
+
+		lconn, err := virtNet.netL0.ListenPacket("udp4", "0.0.0.0:0")
+		assert.NoError(t, err, "should succeed")
+		defer func() {
+			assert.NoError(t, lconn.Close())
+		}()
+
+		log.Debug("creating a client.")
+		client, err := NewClient(&ClientConfig{
+			TURNServerAddr: turnAddr,
+			Conn:           lconn,
+			Username:       "user",
+			Password:       "pass",
+			Realm:          "pion.ly",
+			LoggerFactory:  loggerFactory,
+		})
+		assert.NoError(t, err, "should succeed")
+		assert.NoError(t, client.Listen(), "should succeed")
+		defer client.Close()
+
+		log.Debug("sending an allocate request.")
+		relayConn, err := client.Allocate()
+		assert.NoError(t, err, "should succeed")
+
+		log.Debug("Closing relay connection")
+		assert.NoError(t, relayConn.Close(), "relay conn close should succeed")
+
+		assert.Eventually(t, func() bool { return counter.Load() == 2 }, timeout, interval)
+	})
+
+	t.Run("AuthEventHandlerFailure", func(t *testing.T) {
+		counter := &atomic.Int32{}
+		events := make(chan eventHandlerArgs, 5)
+		defer close(events)
+		virtNet, err := buildVNet(testEventHandler(events, counter))
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, virtNet.Close())
+		}()
+
+		lconn, err := virtNet.netL0.ListenPacket("udp4", "0.0.0.0:0")
+		assert.NoError(t, err, "should succeed")
+		defer func() {
+			assert.NoError(t, lconn.Close())
+		}()
+
+		log.Debug("creating a client.")
+		client, err := NewClient(&ClientConfig{
+			TURNServerAddr: turnAddr,
+			Conn:           lconn,
+			Username:       "user",
+			Password:       "wrong-pass",
+			Realm:          "pion.ly",
+			LoggerFactory:  loggerFactory,
+		})
+		assert.NoError(t, err, "should succeed")
+		assert.NoError(t, client.Listen(), "should succeed")
+		defer client.Close()
+
+		log.Debug("sending an allocate request.")
+		_, err = client.Allocate()
+		assert.Error(t, err, "should not succeed")
+
+		event, ok := expectEvent(events)
+		assert.True(t, ok, "should receive an event")
+		checkAuthEvent(t, event, "Allocate", false)
+
+		event, ok = expectEvent(events)
+		assert.True(t, ok, "should receive an event")
+		assert.Equal(t, onAllocationError, event.Type, "should receive an OnAllocationError event")
+		udpAddr, ok := event.srcAddr.(*net.UDPAddr)
+		assert.True(t, ok)
+		assert.Equal(t, udpAddr.IP.String(), "5.6.7.8")
+		assert.Equal(t, "UDP", event.protocol)
+
+		assert.Eventually(t, func() bool { return counter.Load() == 1 }, timeout, interval)
+	})
 }
 
 func TestConsumeSingleTURNFrame(t *testing.T) {