{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -- | Best current practice library for UDP clients and servers. -- -- * Efficient receiving function without memory copy -- * Proper buffer size -- * Type-safe APIs -- * TCP-like APIs (creating a UDP connection from a listing socket) in the server side -- * Auto migration (network interface selection) in the client side -- -- The 'Network.Socket.ByteString.recv' family in -- "Network.Socket.ByteString" uses 'createAndTrim' -- internaly. So, one buffer is allocated before corresponding -- system calls are called. Then another buffer is allocated -- according to the input size and the input is copied. -- Receiving functions provided by this library uses 'createUptoN' -- to avoid the memory copy. -- -- Recent application protocols are designed to avoid IP -- fragmentation. So, the UDP payload size is never over 1,500. -- This library uses 2,048 for the buffer size. This size ensures -- no global locking when allocating 'ByteString' (i.e. a buffer). -- -- To know the background of TCP-like API in the server side, see: -- -- * https://kazu-yamamoto.hatenablog.jp/entry/2022/02/25/153122 -- -- To know the background of auto migration in the client side, see: -- -- * https://kazu-yamamoto.hatenablog.jp/entry/2021/06/29/134930 -- * https://www.iij.ad.jp/en/dev/iir/pdf/iir_vol52_focus2_EN.pdf (Sec 3.9) module Network.UDP ( -- * Sockets used by clients and servers after accept UDPSocket(..) , clientSocket , recv , recvBuf , send , sendBuf -- * Server's wildcard socket , ListenSocket(..) , serverSocket , ClientSockAddr(..) , recvFrom , sendTo -- * Server's connected socket , accept -- * Closing , stop , close -- * Misc , natRebinding ) where import Control.Concurrent import qualified Control.Exception as E import Control.Monad import Data.ByteString (ByteString) import Data.IP hiding (addr) import Data.Maybe (fromJust) import Data.Word (Word8) import Foreign.Ptr (Ptr) import qualified GHC.IO.Exception as E import qualified Network.Socket as NS import Network.Socket hiding (accept, close, sendBuf, recvBuf) import qualified Network.Socket.ByteString as NSB import qualified System.IO.Error as E import Network.UDP.Types import qualified Network.UDP.Recv as R ---------------------------------------------------------------- anySockAddr :: SockAddr -> SockAddr anySockAddr (SockAddrInet p _) = SockAddrInet p 0 anySockAddr (SockAddrInet6 p f _ s) = SockAddrInet6 p f (0,0,0,0) s anySockAddr _ = error "anySockAddr" isAnySockAddr :: SockAddr -> Bool isAnySockAddr (SockAddrInet _ 0) = True isAnySockAddr (SockAddrInet6 _ _ (0,0,0,0) _) = True isAnySockAddr _ = False ---------------------------------------------------------------- -- | A listening socket for UDP which can be used -- for 'recvFrom' and 'sendTo'. -- Optionally, a connected UDP socket can be created -- with 'accept' as an emulation of TCP. data ListenSocket = ListenSocket { listenSocket :: Socket , mySockAddr :: SockAddr , wildcard :: Bool -- ^ 'True' for wildcard. 'False' for interface-specific. } deriving (Eq, Show) -- | A UDP socket which are used with 'recv' and 'send'. data UDPSocket = UDPSocket { udpSocket :: Socket , peerSockAddr :: SockAddr -- ^ Used for a unconnected socket naturally. Used for a connected sockdet for checking , connected :: Bool } deriving (Eq, Show) -- | A client socket address from the server point of view. data ClientSockAddr = ClientSockAddr SockAddr [Cmsg] deriving (Eq, Show) ---------------------------------------------------------------- -- | Creating a listening UDP socket. serverSocket :: (IP, PortNumber) -> IO ListenSocket serverSocket ip = E.bracketOnError open NS.close $ \s -> do setSocketOption s ReuseAddr 1 withFdSocket s setCloseOnExecIfNeeded #if !defined(openbsd_HOST_OS) when (family == AF_INET6) $ setSocketOption s IPv6Only 1 #endif bind s sa let wild = isAnySockAddr sa when wild $ do let opt = decideOption sa setSocketOption s opt 1 return $ ListenSocket s sa wild where sa = toSockAddr ip family = sockAddrFamily sa open = socket family Datagram defaultProtocol ---------------------------------------------------------------- -- | Receiving data with a listening UDP socket. -- For a wildcard socket, recvmsg() is called. -- For an interface specific socket, recvfrom() is called. recvFrom :: ListenSocket -> IO (ByteString, ClientSockAddr) recvFrom ListenSocket{..} | wildcard = do (bs,sa,cmsg,_) <- R.recvMsg listenSocket properUDPSize properCMSGSize 0 return (bs, ClientSockAddr sa cmsg) | otherwise = do (bs,sa) <- R.recvFrom listenSocket properUDPSize return (bs, ClientSockAddr sa []) -- | Sending data with a listening UDP socket. -- For a wildcard socket, sendmsg() is called. -- For an interface specific socket, sento() is called. sendTo :: ListenSocket -> ByteString -> ClientSockAddr -> IO () sendTo ListenSocket{..} bs (ClientSockAddr sa cmsgs) | wildcard = void $ NSB.sendMsg listenSocket sa [bs] cmsgs 0 | otherwise = void $ NSB.sendTo listenSocket bs sa ---------------------------------------------------------------- -- | Creating a connected UDP socket like TCP's accept(). accept :: ListenSocket -> ClientSockAddr -> IO UDPSocket accept ListenSocket{..} (ClientSockAddr peersa cmsgs) = E.bracketOnError open NS.close $ \s -> do setSocketOption s ReuseAddr 1 withFdSocket s setCloseOnExecIfNeeded let mysa' | wildcard = getMySockAddr mySockAddr cmsgs | otherwise = anySockAddr mySockAddr -- wildcard: (UDP, *.443, *:*) -> (UDP, 127.0.0.1:443, *:*) -- otherwise: (UDP, 127.0.0.1:443, *:*) -> (UDP, *:443, *:*) bind s mysa' -- bind and connect is not atomic -- So, bind may results in EADDRINUSE `E.catch` postphone (bind s mysa') connect s peersa -- (UDP, 127.0.0.1:443, pa:pp) return $ UDPSocket s peersa True where postphone action e | E.ioeGetErrorType e == E.ResourceBusy = threadDelay 10000 >> action | otherwise = E.throwIO e family = sockAddrFamily mySockAddr open = socket family Datagram defaultProtocol ---------------------------------------------------------------- -- | Creating a unconnected UDP socket. clientSocket :: HostName -> ServiceName -> Bool -> IO UDPSocket clientSocket host port conn = do addr <- head <$> NS.getAddrInfo (Just hints) (Just host) (Just port) E.bracketOnError (NS.openSocket addr) NS.close $ \s -> do let sa = addrAddress addr when conn $ NS.connect s sa return $ UDPSocket s sa conn where hints = NS.defaultHints { addrSocketType = Datagram } -- | Sending data with a UDP socket. -- If the socket is connected, send() is called. -- Otherwise, sento() is called. send :: UDPSocket -> (ByteString -> IO ()) send UDPSocket{..} | connected = \bs -> void $ NSB.send udpSocket bs | otherwise = \bs -> void $ NSB.sendTo udpSocket bs peerSockAddr -- | Receiving data with a UDP socket. -- If the socket is connected, recv() is called. -- Otherwise, recvfrom() is called. recv :: UDPSocket -> IO ByteString recv UDPSocket{..} | connected = R.recv udpSocket properUDPSize | otherwise = go where go = do (bs, sa) <- R.recvFrom udpSocket properUDPSize if sa == peerSockAddr then return bs else go -- | Sending data in a buffer with a UDP socket. -- If the socket is connected, send() is called. -- Otherwise, sento() is called. sendBuf :: UDPSocket -> (Ptr Word8 -> Int -> IO ()) sendBuf UDPSocket{..} ptr siz | connected = void $ NS.sendBuf udpSocket ptr siz | otherwise = void $ NS.sendBufTo udpSocket ptr siz peerSockAddr -- | Receiving data in a buffer with a UDP socket. -- If the socket is connected, recv() is called. -- Otherwise, recvfrom() is called. recvBuf :: UDPSocket -> (Ptr Word8 -> Int -> IO Int) recvBuf UDPSocket{..} ptr siz | connected = NS.recvBuf udpSocket ptr siz | otherwise = go where go = do (len,sa) <- NS.recvBufFrom udpSocket ptr siz if sa == peerSockAddr then return len else go -- | Closing a socket. stop :: ListenSocket -> IO () stop (ListenSocket s _ _) = NS.close s -- | Closing a socket. close :: UDPSocket -> IO () close (UDPSocket s _ _) = NS.close s -- | Emulation of NAT rebiding in the client side. -- This is mainly used for test purposes. natRebinding :: UDPSocket -> IO UDPSocket natRebinding (UDPSocket _ sa conn) = E.bracketOnError open NS.close $ \s -> do when conn $ NS.connect s sa return $ UDPSocket s sa conn where family = sockAddrFamily sa open = NS.socket family Datagram NS.defaultProtocol ---------------------------------------------------------------- decideOption :: SockAddr -> SocketOption decideOption SockAddrInet{} = RecvIPv4PktInfo decideOption SockAddrInet6{} = RecvIPv6PktInfo decideOption _ = error "decideOption" -- | Obtaining my sockaddr for a wildcard socket from cmsgs. getMySockAddr :: SockAddr -> [Cmsg] -> SockAddr getMySockAddr (SockAddrInet p _) cmsgs = SockAddrInet p addr where pktinfo = fromJust $ lookupCmsg CmsgIdIPv4PktInfo cmsgs IPv4PktInfo _ _ addr = fromJust $ decodeCmsg pktinfo getMySockAddr (SockAddrInet6 p f _ sc) cmsgs = SockAddrInet6 p f addr sc where pktinfo = fromJust $ lookupCmsg CmsgIdIPv6PktInfo cmsgs IPv6PktInfo _ addr = fromJust $ decodeCmsg pktinfo getMySockAddr _ _ = error "getMySockAddr"