ping-0.1.0.0: src/Network/Icmp/Ping/Hosts.hs
{-# language BangPatterns #-}
{-# language BinaryLiterals #-}
{-# language LambdaCase #-}
{-# language MagicHash #-}
{-# language ScopedTypeVariables #-}
{-# language UnboxedTuples #-}
{-# language EmptyCase #-}
module Network.Icmp.Ping.Hosts
( hosts
, range
) where
-- TODO: The functions in this module currently do not finish
-- promptly when they have the opportunity to. If all hosts
-- respond, it would be better to go ahead and finish rather
-- than waiting around. The use of adjustManyInline makes this
-- a little trickier than it would otherwise be.
import Control.Applicative ((<|>))
import Control.Concurrent (threadWaitReadSTM,threadWaitWriteSTM)
import Control.Concurrent.STM.TVar (readTVar,registerDelay)
import Control.Exception (onException,mask)
import Data.Bits (unsafeShiftL, unsafeShiftR, (.&.), (.|.), testBit)
import Data.Functor (($>))
import Data.Primitive (PrimArray,MutableByteArray)
import Data.Word (Word64,Word8,Word16,Word32)
import Foreign.C.Error (Errno(..),eACCES)
import Foreign.C.Types (CSize(..))
import GHC.Clock (getMonotonicTimeNSec)
import GHC.Exts (RealWorld)
import GHC.IO (IO(..))
import Net.Types (IPv4(..),IPv4Range)
import Network.Icmp.Marshal (peekIcmpHeaderPayload,peekIcmpHeaderType)
import Network.Icmp.Marshal (peekIcmpHeaderSequenceNumber)
import Network.Icmp.Marshal (sizeOfIcmpHeader,pokeIcmpHeader)
import Network.Icmp.Common (IcmpException(..))
import Posix.Socket (SocketAddressInternet(..))
import System.Endian (toBE32)
import System.Posix.Types (Fd(..))
import Unsafe.Coerce (unsafeCoerce)
import qualified Control.Monad.STM as STM
import qualified Data.Map.Unboxed.Unboxed as MUU
import qualified Data.Primitive as PM
import qualified Data.Set.Unboxed as SU
import qualified Linux.Socket as SCK
import qualified Posix.Socket as SCK
import qualified Net.IPv4 as IPv4
debug :: String -> IO ()
debug _ = pure ()
-- debug = putStrLn
fullPacketSize :: Int
fullPacketSize = sizeOfIcmpHeader + 4
-- Wait up to a specified maximum number of microseconds
-- for a socket to have data ready to be read. Returns
-- True if there is something on the buffer to be read
-- and False if nothing showed up in time.
waitForRead ::
Int -- Maximum number of microseconds to wait.
-> Fd -- Socket
-> IO Bool
waitForRead !maxWaitTime !sock = do
(isReadyAction,deregister) <- threadWaitReadSTM sock
delay <- registerDelay maxWaitTime
isContentReady <- STM.atomically $
(isReadyAction $> True)
<|>
(do isDone <- readTVar delay
STM.check isDone
pure False
)
deregister
pure isContentReady
-- Wait for read or write to be available on the socket.
-- Returns True if read became available and False if
-- write became available.
waitForReadWrite :: Fd -> IO Bool
waitForReadWrite sock = do
(isReadyRead,deregisterRead) <- threadWaitReadSTM sock
(isReadyWrite,deregisterWrite) <- threadWaitWriteSTM sock
r <- STM.atomically ((isReadyRead $> True) <|> (isReadyWrite $> False))
deregisterRead
deregisterWrite
pure r
-- | Ping a range of hosts simultaneously.
range ::
Int -- ^ Microseconds to wait for response
-> IPv4Range -- ^ Range
-> IO (Either IcmpException (MUU.Map IPv4 Word64)) -- ^ Elapsed nanoseconds for responding hosts
range !pause !r = hosts pause $ coerceIPv4Set
(SU.enumFromTo
(getIPv4 (IPv4.lowerInclusive r))
(getIPv4 (IPv4.upperInclusive r))
)
-- The existence of this function is a little disappointing. I suspect that
-- there is a better way to do this (probably by writing version of
-- Data.Set.Unboxed.enumFromTo that works without a Num constraint),
-- but I am choosing the easiest path for now.
--
-- TODO: There is a better way. I need to rewrite
-- Data.Primitive.Contiguous.fromList to be compatible with
-- list fusion. Then Data.Set.Unboxed.enumFromTo can use
-- that, and everything should work out alright. Well, we
-- still must perform an extra check to ensure that the
-- enum instance is compatible with the Ord instance,
-- but that's not too bad.
coerceIPv4Set :: SU.Set Word32 -> SU.Set IPv4
coerceIPv4Set = unsafeCoerce
-- | Ping a set of hosts simultaneously. Performs one ping
-- for each host and reports the elapsed nanoseconds for the
-- response. If a key is missing from the resulting map, it
-- indicates that a response was not received from that host.
hosts ::
Int -- ^ Microseconds to wait for response
-> SU.Set IPv4 -- ^ Hosts
-> IO (Either IcmpException (MUU.Map IPv4 Word64)) -- ^ Elapsed nanoseconds for responding hosts
hosts !pause !theHosts = do
mask $ \restore -> SCK.socket SCK.internet SCK.datagram SCK.icmp >>= \case
Left (Errno e) -> pure (Left (IcmpExceptionSocket e))
Right sock -> do
durations <- restore
( do let hostsArr = SU.toArray theHosts
!buffer <- PM.newByteArray fullPacketSize
(m,r) <- MUU.adjustManyInline
(\adjust -> hostsStepA buffer sock pause hostsArr (PM.sizeofPrimArray hostsArr) adjust
) (MUU.fromSet (const initialStatus) theHosts)
pure $ case r of
Left pair -> Left pair
Right _ -> Right
( MUU.mapMaybe
(\w -> case testBit w 47 of
True -> Just (extractTimestamp w)
False -> Nothing
) m
)
)
`onException`
(SCK.unsafeClose sock)
SCK.unsafeClose sock >>= \case
Left (Errno e) -> pure (Left (IcmpExceptionClose e))
Right _ -> pure durations
hostsStepA :: MutableByteArray RealWorld -> Fd -> Int -> PrimArray IPv4 -> Int -> (IPv4 -> (Word64 -> IO Word64) -> IO ()) -> IO (Either IcmpException ())
hostsStepA !buffer !sock !pause !hostsArr !hostsLen adjust = go 0 where
go !ix = if ix < hostsLen
then do
debug "waiting for read-write"
waitForReadWrite sock >>= \case
True -> do
debug "ready for read"
r <- SCK.unsafeReceiveFromMutableByteArray_ sock buffer 0 (intToCSize fullPacketSize) SCK.dontWait
case r of
Left (Errno e) -> pure (Left (IcmpExceptionReceive e))
Right receivedBytes -> if receivedBytes == intToCSize fullPacketSize
then do
payload' <- peekIcmpHeaderPayload buffer
adjust (IPv4 payload') $ \w -> case extractStatus w of
0b01 -> do
sequenceNumber' <- peekIcmpHeaderSequenceNumber buffer
if sequenceNumber' == extractSequenceNumber w
then do
end <- getMonotonicTimeNSec
pure (completeStatus ((end .&. 0x3FFFFFFFFFFF) - extractTimestamp w))
else pure w
_ ->
-- In this case, we did not send an icmp echo request
-- but we received a response (00). Or we've already received
-- a response (1x). Pretty weird, and it
-- suggests foul play, but we simply ignore the response.
pure w
go ix
else do
-- If the repsonse is malformed, we leave the result in
-- the map alone. It will be purged at the end.
go ix
False -> do
debug "ready for write"
let host = PM.indexPrimArray hostsArr ix
PM.setByteArray buffer 0 sizeOfIcmpHeader (0 :: Word8)
pokeIcmpHeader buffer (intToWord16 ix) (getIPv4 host)
let sockaddr = SCK.encodeSocketAddressInternet
(SocketAddressInternet { port = 0, address = toBE32 (getIPv4 host) })
mwriteError <- SCK.unsafeSendToMutableByteArray sock buffer 0 (intToCSize fullPacketSize) SCK.dontWait sockaddr
case mwriteError of
Left (Errno e)
-- When you try to send a packet to a broadcast address, the kernel
-- gives you an EACCES failure. Including a broadcast address in a
-- range is actually somewhat common though. For example, if we try
-- to ping everything on our local network 192.168.1.0/24, we are
-- going to send an ICMP echo request to 192.168.1.0, which the kernel
-- does not like. This is fine though. We just ignore these failures
-- rather than having them abort the entire function.
| Errno e == eACCES -> go (ix + 1)
| otherwise -> pure (Left (IcmpExceptionSend e))
Right sentBytes -> if sentBytes == intToCSize fullPacketSize
then do
start <- getMonotonicTimeNSec
adjust host (\_ -> pure (pendingStatus (intToWord16 ix) start))
go (ix + 1)
else do
-- could not send out the full packet, should not happen
pure (Left (IcmpExceptionSendBytes sentBytes))
else hostsStepB buffer sock pause adjust =<< getMonotonicTimeNSec
-- We start calling this once we run out of hosts to send to. At this point,
-- all responses need to come in soon. We get the current time and count down
-- from this, requiring any outstanding replies to show up within that time
-- frame.
hostsStepB :: MutableByteArray RealWorld -> Fd -> Int -> (IPv4 -> (Word64 -> IO Word64) -> IO ()) -> Word64 -> IO (Either IcmpException ())
hostsStepB !buffer !sock !pause !adjust !initialTime = go initialTime where
go !currentTime = do
debug "Step B iteration"
let remainingMicroseconds = pause - word64ToInt (div (currentTime - initialTime) 1000)
if remainingMicroseconds > 0
then do
isReady <- waitForRead remainingMicroseconds sock
if isReady
then do
r <- SCK.unsafeReceiveFromMutableByteArray_ sock buffer 0 (intToCSize fullPacketSize) SCK.dontWait
case r of
Left (Errno e) -> pure (Left (IcmpExceptionReceive e))
Right receivedBytes -> if receivedBytes == intToCSize fullPacketSize
then do
payload' <- peekIcmpHeaderPayload buffer
end <- getMonotonicTimeNSec
peekIcmpHeaderType buffer >>= \case
0 -> do
adjust (IPv4 payload') $ \w -> case extractStatus w of
0b01 -> do
sequenceNumber' <- peekIcmpHeaderSequenceNumber buffer
if sequenceNumber' == extractSequenceNumber w
then pure (completeStatus ((end .&. 0x3FFFFFFFFFFF) - extractTimestamp w))
else pure w
_ -> pure w
go end
_ -> go end
else go =<< getMonotonicTimeNSec -- response was wrong size
else pure (Right ())
else pure (Right ())
-- We use the lower 46 bits for the timestamp. We use the upper 16
-- for the sequence number. Bits 46 and 47 are set to 1x when we have have
-- received a response successfully. It is 01 when we are awaiting a response. It
-- is 00 when nothing has been sent.
pendingStatus :: Word16 -> Word64 -> Word64
pendingStatus seqNum timestamp =
0x400000000000 .|. (timestamp .&. 0x3FFFFFFFFFFF) .|. (unsafeShiftL (word16ToWord64 seqNum) 48)
completeStatus :: Word64 -> Word64
completeStatus timestamp = 0xC00000000000 .|. timestamp
initialStatus :: Word64
initialStatus = 0
extractStatus :: Word64 -> Word64
extractStatus w =
unsafeShiftR (0xC00000000000 .&. w) 46
extractSequenceNumber :: Word64 -> Word16
extractSequenceNumber w = word64ToWord16 (unsafeShiftR w 48)
extractTimestamp :: Word64 -> Word64
extractTimestamp w = (w .&. 0x3FFFFFFFFFFF)
word64ToWord16 :: Word64 -> Word16
word64ToWord16 = fromIntegral
word16ToWord64 :: Word16 -> Word64
word16ToWord64 = fromIntegral
intToWord16 :: Int -> Word16
intToWord16 = fromIntegral
word64ToInt :: Word64 -> Int
word64ToInt = fromIntegral
intToCSize :: Int -> CSize
intToCSize = fromIntegral