{-# 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)