gbnet-hs-0.2.2.0: src/GBNet/Peer/Handshake.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedLabels #-}
-- |
-- Module : GBNet.Peer.Handshake
-- Description : Connection handshake state machine
--
-- Handles connection request, challenge, response, accepted, and disconnect
-- packets during the handshake protocol.
module GBNet.Peer.Handshake
( handleConnectionRequest,
handleNewConnectionRequest,
handleConnectionChallenge,
handleConnectionResponse,
handleConnectionAccepted,
handleDisconnect,
)
where
import qualified Data.ByteString as BS
import qualified Data.Map.Strict as Map
import GBNet.Class (MonoTime)
import GBNet.Config (NetworkConfig (..))
import GBNet.Connection
( DisconnectReason (..),
newConnection,
)
import qualified GBNet.Connection as Conn
import GBNet.Packet (Packet (..), PacketType (..))
import GBNet.Peer.Internal
import GBNet.Peer.Protocol
( DenyReason (..),
decodeSalt,
encodeDenyReason,
encodeSalt,
sockAddrToKey,
)
import GBNet.Security (rateLimiterAllow)
import GBNet.Util (nextRandom)
import Optics ((%~), (&), (.~))
-- | Handle incoming connection request (pure).
handleConnectionRequest :: PeerId -> MonoTime -> NetPeer -> ([PeerEvent], NetPeer)
handleConnectionRequest peerId now peer =
case (Map.member peerId (npConnections peer), Map.lookup peerId (npPending peer)) of
(True, _) ->
-- Already connected, resend accept
([], queueControlPacket ConnectionAccepted BS.empty peerId peer)
(_, Just p) ->
-- Already pending, resend challenge with stored salt
let saltPayload = encodeSalt (pcServerSalt p)
in ([], queueControlPacket ConnectionChallenge saltPayload peerId peer)
(False, Nothing) ->
handleNewConnectionRequest peerId now peer
-- | Handle a genuinely new connection request after checking existing state (pure).
handleNewConnectionRequest :: PeerId -> MonoTime -> NetPeer -> ([PeerEvent], NetPeer)
handleNewConnectionRequest peerId now peer =
let addrKey = sockAddrToKey (unPeerId peerId)
(allowed, limiter) = rateLimiterAllow addrKey now (npRateLimiter peer)
peer1 = peer & #npRateLimiter .~ limiter
pendingSize = Map.size (npPending peer1)
connSize = Map.size (npConnections peer1)
maxClients = ncMaxClients (npConfig peer1)
in if
| not allowed ->
([], peer1 & #npRateLimitDrops %~ (+ 1))
| pendingSize >= maxClients ->
([], peer1 & #npRateLimitDrops %~ (+ 1))
| connSize >= maxClients ->
let reason = encodeDenyReason DenyServerFull
in ([], queueControlPacket ConnectionDenied reason peerId peer1)
| otherwise ->
let (salt, rng) = nextRandom (npRngState peer1)
newPend =
PendingConnection
{ pcDirection = Inbound,
pcServerSalt = salt,
pcClientSalt = 0,
pcCreatedAt = now,
pcRetryCount = 0,
pcLastRetry = now
}
peer2 =
peer1
& #npPending
%~ Map.insert peerId newPend
& #npRngState
.~ rng
saltPayload = encodeSalt salt
in ([], queueControlPacket ConnectionChallenge saltPayload peerId peer2)
-- | Handle connection challenge (we're outbound, received their challenge) (pure).
handleConnectionChallenge :: PeerId -> Packet -> MonoTime -> NetPeer -> ([PeerEvent], NetPeer)
handleConnectionChallenge peerId pkt _now peer =
case Map.lookup peerId (npPending peer) of
Nothing -> ([], peer)
Just p
| pcDirection p /= Outbound -> ([], peer)
| otherwise ->
case decodeSalt (pktPayload pkt) of
Nothing -> ([], peer)
Just serverSalt ->
let updated = p & #pcServerSalt .~ serverSalt
saltPayload = encodeSalt (pcClientSalt updated)
in ( [],
queueControlPacket ConnectionResponse saltPayload peerId $
peer & #npPending %~ Map.insert peerId updated
)
-- | Handle connection response (we're inbound, received their response) (pure).
handleConnectionResponse :: PeerId -> Packet -> MonoTime -> NetPeer -> ([PeerEvent], NetPeer)
handleConnectionResponse peerId pkt now peer =
case Map.lookup peerId (npPending peer) of
Nothing -> ([], peer)
Just p
| pcDirection p /= Inbound -> ([], peer)
| otherwise ->
case decodeSalt (pktPayload pkt) of
Nothing -> ([], peer)
Just clientSalt
| clientSalt == 0 || clientSalt == pcServerSalt p ->
let reason = encodeDenyReason DenyInvalidChallenge
in ( [],
queueControlPacket ConnectionDenied reason peerId $
removePending peerId peer
)
| otherwise ->
let conn =
Conn.markConnected now $
Conn.touchRecvTime now $
newConnection (npConfig peer) clientSalt now
promoted =
peer
& #npConnections
%~ Map.insert peerId conn
& #npPending
%~ Map.delete peerId
in ([PeerConnected peerId Inbound], queueControlPacket ConnectionAccepted BS.empty peerId promoted)
-- | Handle connection accepted (we're outbound, they accepted) (pure).
handleConnectionAccepted :: PeerId -> MonoTime -> NetPeer -> ([PeerEvent], NetPeer)
handleConnectionAccepted peerId now peer =
case Map.lookup peerId (npPending peer) of
Nothing -> ([], peer)
Just p
| pcDirection p /= Outbound -> ([], peer)
| otherwise ->
let conn =
Conn.markConnected now $
Conn.touchRecvTime now $
newConnection (npConfig peer) (pcClientSalt p) now
promoted =
peer
& #npConnections
%~ Map.insert peerId conn
& #npPending
%~ Map.delete peerId
in ([PeerConnected peerId Outbound], promoted)
-- | Handle disconnect packet (pure).
handleDisconnect :: PeerId -> NetPeer -> ([PeerEvent], NetPeer)
handleDisconnect peerId peer =
if Map.member peerId (npConnections peer)
then
let disconnected = cleanupPeer peerId (peer & #npConnections %~ Map.delete peerId)
in ([PeerDisconnected peerId ReasonRequested], disconnected)
else ([], removePending peerId peer)