hans-3.0.2: src/Hans/IP4/ArpTable.hs
{-# LANGUAGE RecordWildCards #-}
module Hans.IP4.ArpTable (
-- * Arp Table
ArpTable(), newArpTable,
-- ** Update
addEntry,
markUnreachable,
-- ** Query
lookupEntry,
resolveAddr, QueryResult(..),
WaitStrategy(), blockingStrategy, writeChanStrategy
) where
import Hans.Config (Config(..))
import Hans.Device.Types (DeviceStats,updateError,statTX)
import Hans.Ethernet (Mac)
import qualified Hans.HashTable as HT
import Hans.IP4.Packet (IP4)
import Hans.Threads (forkNamed)
import Hans.Time (toUSeconds)
import Control.Concurrent
(threadDelay,MVar,newEmptyMVar,ThreadId,tryPutMVar)
import qualified Control.Concurrent.BoundedChan as BC
import Control.Monad (forever)
import Data.Time.Clock
(UTCTime,NominalDiffTime,addUTCTime,getCurrentTime)
-- | The Arp table consists of a map of IP4 to Mac, as well as a heap that
-- orders the IP4 addresses according to when their corresponding entries should
-- be expired.
--
-- NOTE: There's currently no way to limit the memory use of the arp table, but
-- that might not be a huge problem as the entries are added based on requests
-- from higher layers.
--
-- INVARIANT: there should never be entries in the map that aren't also in the
-- heap.
data ArpTable = ArpTable { atMacs :: !(HT.HashTable IP4 Entry)
, atLifetime :: !NominalDiffTime
, atPurgeThread :: !ThreadId
}
data Entry = Waiting [Maybe Mac -> IO ()]
| Present !UTCTime !Mac
newArpTable :: Config -> IO ArpTable
newArpTable Config { .. } =
do atMacs <- HT.newHashTable cfgArpTableSize
atPurgeThread <- forkNamed "Arp Purge Thread"
(purgeArpTable cfgArpTableLifetime atMacs)
return ArpTable { atLifetime = cfgArpTableLifetime, .. }
-- | Loops forever, delaying until the next arp table entry needs to be purged.
-- If no entries exist, it waits for the maximum entry lifetime before checking
-- again.
purgeArpTable :: NominalDiffTime -> HT.HashTable IP4 Entry -> IO ()
purgeArpTable lifetime table = forever $
do now <- getCurrentTime
HT.filterHashTable (update now) table
threadDelay delay
where
update _ _ Waiting{} = False
update now _ (Present expire _) = expire < now
delay = toUSeconds lifetime
-- | Lookup an entry in the Arp table.
lookupEntry :: ArpTable -> IP4 -> IO (Maybe Mac)
lookupEntry ArpTable { .. } spa =
do mb <- HT.lookup spa atMacs
case mb of
Just (Present _ mac) -> return (Just mac)
_ -> return Nothing
-- | Insert an entry into the Arp table, and unblock any waiting actions.
addEntry :: ArpTable -> IP4 -> Mac -> IO ()
addEntry ArpTable { .. } spa sha =
do now <- getCurrentTime
let end = addUTCTime atLifetime now
waiters <- HT.alter (update end) spa atMacs
-- NOTE: as we don't allow user-supplied IO actions to be registered as
-- callbacks, we don't catch any exceptions here; the only way that an
-- action can end up in this list is through the constructors for the
-- WaitStrategy type defined below.
--
-- If it turns out that it's significantly impacting the performance of the
-- fast path, we should consider forking a thread to run the waiters.
mapM_ ($ Just sha) waiters
where
update expire (Just (Waiting ks)) = (Just (Present expire sha), ks)
update expire (Just Present{}) = (Just (Present expire sha), [])
update expire Nothing = (Just (Present expire sha), [])
-- | If nobody has responded to queries for this address, notify any waiters
-- that there is no mac associated.
--
-- NOTE: in the future, it might be nice to keep an entry in the table that
-- indicates that this host is unreachable.
markUnreachable :: ArpTable -> IP4 -> IO ()
markUnreachable ArpTable { .. } addr =
do waiters <- HT.alter update addr atMacs
-- See the note in addEntry about why we don't need to sandbox the
-- callbacks.
mapM_ ($ Nothing) waiters
where
update (Just (Waiting ks)) = (Nothing, ks)
update ent@(Just Present{}) = (ent, [])
update Nothing = (Nothing,[])
newtype WaitStrategy res = WaitStrategy { getWaiter :: IO (Maybe Mac -> IO (), res) }
-- | Have the entry block on a
blockingStrategy :: WaitStrategy (MVar (Maybe Mac))
blockingStrategy = WaitStrategy $
do mvar <- newEmptyMVar
let write mb = do _ <- tryPutMVar mvar mb
return ()
return (write, mvar)
-- | Write the discovered Mac to a bounded channel, passing it through a
-- filtering function first.
writeChanStrategy :: Maybe DeviceStats -> (Maybe Mac -> Maybe msg) -> BC.BoundedChan msg
-> WaitStrategy ()
writeChanStrategy mbStats f chan = WaitStrategy (return (handler,()))
where
handler mb =
case f mb of
Just msg -> do written <- BC.tryWriteChan chan msg
case mbStats of
Just stats | not written -> updateError statTX stats
_ -> return ()
-- XXX should this update a stat?
Nothing -> return ()
data QueryResult res = Known !Mac
| Unknown !Bool res
-- | Returns either the address, or an empty 'MVar' that will eventually contain
-- the 'Mac', or 'Nothing' if the Arp request fails.
resolveAddr :: ArpTable -> IP4 -> WaitStrategy res -> IO (QueryResult res)
resolveAddr arp addr strategy =
do mb <- lookupEntry arp addr
case mb of
Just mac -> return (Known mac)
Nothing -> registerWaiter arp addr strategy
-- | Register to wait on an entry in the table.
registerWaiter :: ArpTable -> IP4 -> WaitStrategy res -> IO (QueryResult res)
registerWaiter ArpTable { .. } addr strategy =
do waiter <- getWaiter strategy
HT.alter (update waiter) addr atMacs
where
update (w,r) (Just (Waiting ws)) = (Just (Waiting (w:ws)), Unknown False r)
update _ ent@(Just (Present _ mac)) = (ent, Known mac)
update (w,r) Nothing = (Just (Waiting [w]), Unknown True r)