net-spider-rpl (empty) → 0.1.0.0
raw patch · 18 files changed
+1509/−0 lines, 18 filesdep +aesondep +basedep +greskellsetup-changed
Dependencies added: aeson, base, greskell, hashable, hspec, ip, net-spider, net-spider-pangraph, net-spider-rpl, text, time
Files
- ChangeLog.md +5/−0
- LICENSE +30/−0
- README.md +15/−0
- Setup.hs +2/−0
- net-spider-rpl.cabal +67/−0
- src/NetSpider/RPL.hs +59/−0
- src/NetSpider/RPL/Combined.hs +110/−0
- src/NetSpider/RPL/ContikiNG.hs +412/−0
- src/NetSpider/RPL/DAO.hs +95/−0
- src/NetSpider/RPL/DIO.hs +207/−0
- src/NetSpider/RPL/FindingID.hs +144/−0
- src/NetSpider/RPL/IPv6.hs +33/−0
- test/NetSpider/RPL/ContikiNGSpec.hs +259/−0
- test/Spec.hs +1/−0
- test/data/cooja.log +23/−0
- test/data/syslog_nonroot.log +14/−0
- test/data/syslog_root.log +13/−0
- test/data/syslog_sr_tables.log +20/−0
+ ChangeLog.md view
@@ -0,0 +1,5 @@+# Revision history for net-spider-rpl++## 0.1.0.0 -- 2019-05-03++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2019, Toshio Ito++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Toshio Ito nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,15 @@+# net-spider-rpl++NetSpider data model and utility for RPL ( [IPv6 Routing Protocol for Low-Power and Lossy Networks, RFC 6550](https://tools.ietf.org/html/rfc6550) ) networks.++Features:++- NetSpider data model for the DIO graph (i.e. upward graph) of RPL+- NetSpider data model for the DAO graph (i.e. downward graph) of RPL+- Utility function to read and parse log files generated by [Contiki-NG](http://contiki-ng.org/) applications.++See the module document of [NetSpider.RPL](https://hackage.haskell.org/package/net-spider-rpl/docs/NetSpider-RPL.html) for detail.++## Author++Toshio Ito <debug.ito@gmail.com>
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ net-spider-rpl.cabal view
@@ -0,0 +1,67 @@+name: net-spider-rpl+version: 0.1.0.0+author: Toshio Ito <debug.ito@gmail.com>+maintainer: Toshio Ito <debug.ito@gmail.com>+license: BSD3+license-file: LICENSE+synopsis: NetSpider data model and utility for RPL networks+description: NetSpider data model and utility for RPL networks. See "NetSpider.RPL".+category: Database+cabal-version: >= 1.10+build-type: Simple+extra-source-files: README.md, ChangeLog.md,+ test/data/cooja.log,+ test/data/syslog_root.log,+ test/data/syslog_nonroot.log,+ test/data/syslog_sr_tables.log+homepage: https://github.com/debug-ito/net-spider+bug-reports: https://github.com/debug-ito/net-spider/issues++library+ default-language: Haskell2010+ hs-source-dirs: src+ ghc-options: -Wall -fno-warn-unused-imports+ default-extensions: StrictData+ other-extensions: OverloadedStrings, DeriveGeneric+ exposed-modules: NetSpider.RPL,+ NetSpider.RPL.FindingID,+ NetSpider.RPL.DIO,+ NetSpider.RPL.DAO,+ NetSpider.RPL.Combined,+ NetSpider.RPL.ContikiNG+ other-modules: NetSpider.RPL.IPv6+ build-depends: base >=4.11.1.0 && <4.13,+ aeson >=1.3.1.1 && <1.5,+ greskell >=0.2.3.0 && <0.3,+ net-spider >=0.3.0.0 && <0.4,+ net-spider-pangraph >=0.1.0.0 && <0.2,+ text >=1.2.3.1 && <1.3,+ ip >=1.3.0 && <1.5,+ hashable >=1.2.7.0 && <1.3,+ time >=1.8.0.2 && <1.9++-- executable net-spider-rpl+-- default-language: Haskell2010+-- hs-source-dirs: app+-- main-is: Main.hs+-- ghc-options: -Wall -fno-warn-unused-imports+-- -- other-modules: +-- -- default-extensions: +-- -- other-extensions: +-- build-depends: base++test-suite spec+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ hs-source-dirs: test+ ghc-options: -Wall -fno-warn-unused-imports "-with-rtsopts=-M512m"+ main-is: Spec.hs+ -- default-extensions: + other-extensions: OverloadedStrings+ other-modules: NetSpider.RPL.ContikiNGSpec+ build-depends: base, net-spider-rpl, net-spider, text,+ hspec >=2.5.5++source-repository head+ type: git+ location: https://github.com/debug-ito/net-spider.git
+ src/NetSpider/RPL.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module: NetSpider.RPL+-- Description: NetSpider data model and utility for RPL networks+-- Maintainer: Toshio Ito <debug.ito@gmail.com>+--+-- This module defines NetSpider data model and utility for RPL+-- networks.+--+-- RPL ( [IPv6 Routing Protocol for Low-Power and Lossy Networks, RFC 6550](https://tools.ietf.org/html/rfc6550) )+-- is a routing protocol for small wireless devices. Each node+-- independently exchanges routing information with its neighbors over+-- low-power wireless channels. Using those information, the nodes+-- form one or more network topologies called DODAG+-- (Destination-Oriented Directed Acyclic Graph) in a distributed+-- manner.+--+-- The module "NetSpider.RPL.DIO" defines NetSpider data model about+-- DIO (DODAG Information Object). DIO is a control packet of RPL that+-- is necessary to form upward routes. 'FoundNodeDIO' type defines+-- DIO-specific information such as \"rank\". You can input+-- 'FoundNodeDIO's to NetSpider database to construct the upward+-- network.+--+-- The "NetSpider.RPL.DAO" defines another data model+-- 'FoundNodeDAO'. DAO (Destination Advertisement Object) is a control+-- packet of RPL that is necessary to form downward+-- routes. 'FoundNodeDAO' type defines DAO-specific information. You+-- can input 'FoundNodeDAO' to NetSpider database to construct the+-- downward network.+--+-- Because network topologies maintained in 'FoundNodeDIO' and+-- 'FoundNodeDAO' can be different, you have to use different node IDs+-- for them. The module "NetSpider.RPL.FindingID" exports 'FindingID'+-- that distingushes those two types of topologies. The DIO and DAO+-- topologies are obtained from the NetSpider database as different+-- Snapshot graphs.+--+-- The two types of Snapshot graphs can be combined together by+-- upgrading their internal data. "NetSpider.RPL.Combined" module+-- provides functions to combine those graphs.+--+-- The module "NetSpider.RPL.ContikiNG" (not re-exported by this+-- module) is a utility module to make 'FoundNodeDIO's and+-- 'FoundNodeDAO's. It defines utility functions to read and parse log+-- files generated by Contiki-NG applications.+-- [Contiki-NG](http://contiki-ng.org/) is a tiny operating system for+-- wireless devices that supports RPL.+module NetSpider.RPL+ ( module NetSpider.RPL.FindingID,+ module NetSpider.RPL.DIO,+ module NetSpider.RPL.DAO,+ module NetSpider.RPL.Combined+ ) where++import NetSpider.RPL.FindingID+import NetSpider.RPL.DIO+import NetSpider.RPL.DAO+import NetSpider.RPL.Combined
+ src/NetSpider/RPL/Combined.hs view
@@ -0,0 +1,110 @@+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module: NetSpider.RPL.Combined+-- Description: Snapshot graph combining DIO and DAO graphs+-- Maintainer: Toshio Ito <debug.ito@gmail.com>+--+-- This module defines functions and data models that combine DIO+-- (defined in "NetSpider.RPL.DIO") and DAO (defined in+-- "NetSpider.RPL.DAO") graphs.+module NetSpider.RPL.Combined+ ( -- * Functions+ combineNodes,+ combineLinks,+ combineGraphs,+ -- * Types+ CombinedNode(..),+ CombinedLink(..),+ combinedLinkType+ ) where++import Data.Bifunctor (bimap, second)+import Data.Semigroup (Semigroup(..))+import Data.Maybe (isJust)+import Data.Monoid (Monoid(..), First(..))+import GHC.Exts (groupWith)+import qualified NetSpider.Pangraph as Pan+import NetSpider.Snapshot+ ( SnapshotNode, SnapshotLink,+ nodeId, nodeAttributes+ )++import NetSpider.RPL.FindingID (FindingID(..), FindingType(..), IPv6ID, ipv6Only)+import NetSpider.RPL.DIO (DIONode, MergedDIOLink)+import NetSpider.RPL.DAO (DAONode, DAOLink)++-- | Node attributes combining 'DIONode' and 'DAONode'.+data CombinedNode =+ CombinedNode+ { attrsDIO :: Maybe DIONode,+ attrsDAO :: Maybe DAONode+ }+ deriving (Show,Eq,Ord)++-- | Based on instance of 'First'.+instance Semigroup CombinedNode where+ a <> b = CombinedNode dio dao+ where+ dio = getFirst $ (First $ attrsDIO a) <> (First $ attrsDIO b)+ dao = getFirst $ (First $ attrsDAO a) <> (First $ attrsDAO b)++-- | Based on instance of 'First'.+instance Monoid CombinedNode where+ mappend a b = a <> b+ mempty = CombinedNode Nothing Nothing++instance Pan.ToAttributes CombinedNode where+ toAttributes cn = (Pan.toAttributes $ attrsDIO cn)+ ++ (Pan.toAttributes $ attrsDAO cn)++-- | Link attribute combining 'MergedDIOLink' and 'DAOLink'.+data CombinedLink = CombinedDIOLink MergedDIOLink+ | CombinedDAOLink DAOLink+ deriving (Show,Eq,Ord)++instance Pan.ToAttributes CombinedLink where+ toAttributes (CombinedDIOLink ll) =+ ("link_type", "dio") : Pan.toAttributes ll+ toAttributes (CombinedDAOLink sl) =+ ("link_type", "dao") : Pan.toAttributes sl++combinedLinkType :: CombinedLink -> FindingType+combinedLinkType (CombinedDIOLink _) = FindingDIO+combinedLinkType (CombinedDAOLink _) = FindingDAO++combineNodes :: [SnapshotNode FindingID DIONode]+ -> [SnapshotNode FindingID DAONode]+ -> [SnapshotNode IPv6ID CombinedNode]+combineNodes dio_ns dao_ns = concatNodes $ map fromDIO dio_ns ++ map fromDAO dao_ns+ where+ fromDIO = bimap ipv6Only (\ln -> CombinedNode (Just ln) Nothing)+ fromDAO = bimap ipv6Only (\sn -> CombinedNode Nothing (Just sn))+ concatNodes nodes = map merge $ groupWith nodeId nodes+ where+ merge [] = error "Empty group should not happen."+ merge group_nodes@(head_node : _) =+ case mmerged_attr of+ Nothing ->+ -- No node has NodeAttribute. There is no need to merge.+ head_node+ Just merged_attr ->+ case filter hasNodeAttr group_nodes of+ [] -> error "At least one node must have NodeAttributes"+ (representative_node : _) -> second (const merged_attr) representative_node+ where+ mmerged_attr = mconcat $ map nodeAttributes group_nodes+ hasNodeAttr n = isJust $ nodeAttributes n++combineLinks :: [SnapshotLink FindingID MergedDIOLink]+ -> [SnapshotLink FindingID DAOLink]+ -> [SnapshotLink IPv6ID CombinedLink]+combineLinks dio_ls dao_ls = map fromDIO dio_ls ++ map fromDAO dao_ls+ where+ fromDIO = bimap ipv6Only (\ll -> CombinedDIOLink ll)+ fromDAO = bimap ipv6Only (\sl -> CombinedDAOLink sl)++combineGraphs :: ([SnapshotNode FindingID DIONode], [SnapshotLink FindingID MergedDIOLink])+ -> ([SnapshotNode FindingID DAONode], [SnapshotLink FindingID DAOLink])+ -> ([SnapshotNode IPv6ID CombinedNode], [SnapshotLink IPv6ID CombinedLink])+combineGraphs (dio_ns, dio_ls) (dao_ns, dao_ls) =+ (combineNodes dio_ns dao_ns, combineLinks dio_ls dao_ls)
+ src/NetSpider/RPL/ContikiNG.hs view
@@ -0,0 +1,412 @@+-- |+-- Module: NetSpider.RPL.ContikiNG+-- Description: Parser for Contiki-NG logs about RPL+-- Maintainer: Toshio Ito <debug.ito@gmail.com>+--+-- This module exports utility functions to read and parse log files+-- from Contiki-NG applications.+--+-- [Contiki-NG](http://contiki-ng.org/) is a tiny operation system for+-- wireless network devices. It supports RPL.+module NetSpider.RPL.ContikiNG+ ( -- * Parser functions+ parseFile,+ -- * Parser components+ Parser,+ pCoojaLogHead,+ pCoojaLogHead',+ pSyslogHead+ ) where++import Control.Applicative ((<|>), (<$>), (<*>), (*>), (<*), many, optional)+import Control.Exception (Exception, throwIO)+import Control.Monad (void)+import Data.Bits (shift)+import Data.Char (isDigit, isHexDigit, isSpace)+import Data.Int (Int64)+import Data.List (sortOn, reverse)+import Data.Maybe (listToMaybe)+import Data.Monoid ((<>))+import Data.Text (pack)+import qualified Data.Time as Time+import Data.Word (Word16)+import GHC.Exts (groupWith)+import Net.IPv6 (IPv6)+import qualified Net.IPv6 as IPv6+import NetSpider.Found (FoundNode(..), FoundLink(..), LinkState(LinkToTarget))+import NetSpider.Timestamp (Timestamp, fromEpochMillisecond, fromLocalTime, fromZonedTime)+import System.IO (withFile, IOMode(ReadMode), hGetLine, hIsEOF)+import qualified Text.ParserCombinators.ReadP as P+import Text.Read (readEither)++import NetSpider.RPL.FindingID (FindingID(FindingID), FindingType(..))+import NetSpider.RPL.IPv6 (isLinkLocal, setPrefix, getPrefix)+import qualified NetSpider.RPL.DIO as DIO+import NetSpider.RPL.DIO (FoundNodeDIO, dioLinkState)+import qualified NetSpider.RPL.DAO as DAO+import NetSpider.RPL.DAO (FoundNodeDAO)++type Parser = P.ReadP++data ParseError = ParseError String+ deriving (Show,Eq,Ord)++instance Exception ParseError++runParser :: Parser a -> String -> Maybe a+runParser p input = extract $ sortPairs $ P.readP_to_S p input+ where+ sortPairs = sortOn $ \(_, rest) -> length rest+ extract [] = Nothing+ extract ((a,_) : _) = Just a++-- | Read and parse a log file from a Contiki-NG application to make+-- 'FoundNodeDIO' and 'FoundNodeDAO'.+--+-- It assumes that each line of log file has prefix, and that the+-- prefix contains timestamp information. You have to pass the parser+-- for the prefix to this function. For example, if you read a log+-- file generated by Cooja simulator, use 'pCoojaLogHead'' parser.+parseFile :: Parser Timestamp -- ^ Parser for log prefix+ -> FilePath -- ^ File to read+ -> IO ([FoundNodeDIO], [FoundNodeDAO])+parseFile pTimestamp input_file = withFile input_file ReadMode $ onHandle+ where+ onHandle h = go ([], [])+ where+ go (acc_dio, acc_dao) = do+ mentry <- parseOneEntry pTimestamp $ tryGetLine h+ case mentry of+ Nothing -> return (reverse acc_dio, reverse acc_dao)+ Just (PEDIO fl) -> go (fl : acc_dio, acc_dao)+ Just (PEDAO srs) -> go (acc_dio, srs ++ acc_dao)+ Just PEMisc -> go (acc_dio, acc_dao)+ tryGetLine h = do+ eof <- hIsEOF h+ if eof+ then return Nothing+ else Just <$> hGetLine h++data ParseEntry = PEDIO FoundNodeDIO+ | PEDAO [FoundNodeDAO]+ | PEMisc+ deriving (Show,Eq)++-- | Return 'Nothing' when it finishes reading.+parseOneEntry :: Parser Timestamp -> IO (Maybe String) -> IO (Maybe ParseEntry)+parseOneEntry pTimestamp getL = impl+ where+ impl = do+ mline <- getL+ case mline of+ Nothing -> return Nothing+ Just line ->+ case runParser ((,) <$> pTimestamp <*> (pLogHead *> pHeading)) line of+ Nothing -> return $ Just PEMisc+ Just (ts, HDIO addr node) -> proceedDIO ts addr node+ Just (ts, HDAO route_num) -> proceedDAO ts route_num+ withPrefix p = pTimestamp *> pLogHead *> p+ proceedDIO ts addr node = do+ mlinks <- readUntil getL (withPrefix pDIONeighbor) (withPrefix pDIONeighborEnd)+ case mlinks of+ Nothing -> return Nothing+ Just links -> return $ Just $ PEDIO $ makeFoundNodeDIO ts addr node $ map (setAddressPrefix addr) links+ proceedDAO ts route_num = do+ mlinks <- readUntil getL (withPrefix pDAOLink) (withPrefix pDAOLinkEnd)+ case mlinks of+ Nothing -> return Nothing+ Just links -> do+ root_address <- maybe (throwIO $ ParseError "No root address found in SR log") return+ $ getRootAddress links+ return $ Just $ PEDAO $ map (makeDAONodeFromTuple root_address route_num ts) $ groupDAOLinks links+ getRootAddress :: [(IPv6, Maybe (IPv6, Word))] -> Maybe IPv6+ getRootAddress links = fmap fst $ listToMaybe $ filter isRootEntry links+ where+ isRootEntry (_, Nothing) = True+ isRootEntry (_, _) = False+ groupDAOLinks :: [(IPv6, Maybe (IPv6, Word))] -> [(IPv6, [(IPv6, Word)])]+ groupDAOLinks links = map toTuple $ groupWith byParentAddr $ (filterOutRoot =<< links)+ where+ filterOutRoot (_, Nothing) = []+ filterOutRoot (c, Just (p, lt)) = [(c, p, lt)]+ byParentAddr (_, p, _) = p+ toTuple [] = error "groupDAOLinks: this should not happen"+ toTuple entries@((_, p, _) : _) = (p, map extractChildAndLifetime entries)+ extractChildAndLifetime (c, _, lt) = (c, lt)+ makeDAONodeFromTuple root_addr route_num ts (parent_addr, children) =+ makeFoundNodeDAO+ ts (if parent_addr == root_addr then Just route_num else Nothing)+ parent_addr children+ setAddressPrefix self_addr (neighbor_addr, ll) = (modified_addr, ll)+ where+ modified_addr = if isLinkLocal neighbor_addr+ then setPrefix (getPrefix self_addr) neighbor_addr+ else neighbor_addr++readUntil :: IO (Maybe String) -> Parser a -> Parser end -> IO (Maybe [a])+readUntil getL pBody pEnd = go []+ where+ go acc = do+ mline <- getL+ case mline of+ Nothing -> return Nothing+ Just line ->+ case runParser ((Left <$> pEnd) <|> (Right <$> pBody)) line of+ Nothing -> return Nothing+ Just (Left _) -> return $ Just $ reverse acc+ Just (Right body) -> go (body : acc)++makeFoundNodeDIO :: Timestamp -> IPv6 -> DIO.DIONode -> [(IPv6, DIO.DIOLink)] -> FoundNodeDIO+makeFoundNodeDIO ts self_addr node_attr neighbors =+ FoundNode { subjectNode = FindingID FindingDIO self_addr,+ foundAt = ts,+ neighborLinks = map toFoundLink neighbors,+ nodeAttributes = node_attr+ }+ where+ toFoundLink (neighbor_addr, ll) =+ FoundLink { targetNode = FindingID FindingDIO neighbor_addr,+ linkState = dioLinkState ll,+ linkAttributes = ll+ }++makeFoundNodeDAO :: Timestamp -> Maybe Word -> IPv6 -> [(IPv6, Word)] -> FoundNodeDAO+makeFoundNodeDAO ts mroute_num parent_addr children =+ FoundNode { subjectNode = FindingID FindingDAO parent_addr,+ foundAt = ts,+ neighborLinks = map toFoundLink children,+ nodeAttributes = DAO.DAONode mroute_num+ }+ where+ toFoundLink (child_addr, lifetime) =+ FoundLink { targetNode = FindingID FindingDAO child_addr,+ linkState = LinkToTarget,+ linkAttributes = DAO.DAOLink lifetime+ }+++data Heading = HDIO IPv6 DIO.DIONode+ | HDAO Word+ deriving (Show,Eq,Ord)++pHeading :: Parser Heading+pHeading = (fmap (uncurry HDIO) $ pDIONode)+ <|> (HDAO <$> pDAOLogHeader)++isAddressChar :: Char -> Bool+isAddressChar c = isHexDigit c || c == ':'++pAddress :: Parser IPv6+pAddress = fromS =<< P.munch1 isAddressChar+ where+ fromS str =+ case IPv6.decode $ pack str of+ Nothing -> fail ("Invalid IPv6 address: " <> str)+ Just addr -> return addr++data CompactID = CNodeID Int+ | CNodeAddress Word16+ deriving (Show,Eq,Ord)++makeCompactAddress :: CompactID -> IPv6+makeCompactAddress cid =+ case cid of+ CNodeID nid -> IPv6.fromWord32s 0 0 0 (fromIntegral nid)+ CNodeAddress addr -> IPv6.fromWord16s 0 0 0 0 0 0 0 addr++pHexWord16 :: String -> Parser Word16+pHexWord16 input = go 0 input+ where+ go acc [] = return acc+ go acc (c:rest) = do+ c_num <- parseC+ go ((acc `shift` 8) + c_num) rest+ where+ diffWord a b = fromIntegral (fromEnum a - fromEnum b)+ parseC = if c >= '0' && c <= '9'+ then return $ diffWord c '0'+ else if c >= 'a' && c <= 'f'+ then return $ diffWord c 'a'+ else if c >= 'A' && c <= 'F'+ then return $ diffWord c 'A'+ else fail ("Invalid hex number: " <> input)++pCompactID :: Parser CompactID+pCompactID = (fmap CNodeID $ pRead =<< P.count 3 (P.satisfy isDigit))+ <|> (fmap CNodeAddress $ pHexWord16 =<< P.count 4 (P.satisfy isHexDigit))++pCompactAddress :: Parser IPv6+pCompactAddress = do+ void $ P.string "6G-" -- expecting unicast global address+ fmap makeCompactAddress $ pCompactID++-- neighbor address can be logged in a "compact" form.+-- https://github.com/contiki-ng/contiki-ng/blob/develop/os/sys/log.c++pMaybeCompactAddress :: Parser IPv6+pMaybeCompactAddress = pCompactAddress <|> pAddress++pRead :: Read a => String -> Parser a+pRead = either fail return . readEither++pNum :: Read a => Parser a+pNum = pRead =<< P.munch1 isDigit++pDIONode :: Parser (IPv6, DIO.DIONode)+pDIONode = do+ void $ P.string "nbr: own state, addr "+ addr <- pAddress+ void $ P.string ", DAG state: "+ void $ P.munch (\c -> c /= ',')+ void $ P.string ", MOP "+ void $ P.munch isDigit+ void $ P.string " OCP "+ void $ P.munch isDigit+ void $ P.string " rank "+ rank <- pNum+ void $ P.string " max-rank "+ void $ P.munch isDigit+ void $ P.string ", dioint "+ dio_int <- pNum+ let node = DIO.DIONode { DIO.rank = rank,+ DIO.dioInterval = dio_int+ }+ return (addr, node)++pExpectChar :: Char -> Parser Bool+pExpectChar exp_c = fmap (== Just exp_c) $ optional P.get++pDIONeighbor :: Parser (IPv6, DIO.DIOLink)+pDIONeighbor = do+ void $ P.string "nbr: "+ neighbor_addr <- pMaybeCompactAddress+ P.skipSpaces+ neighbor_rank <- pNum+ void $ P.string ", "+ P.skipSpaces+ metric <- pNum+ void $ P.string " => "+ P.skipSpaces+ void $ P.munch isDigit -- rank_via_neighbor+ void $ P.string " -- "+ P.skipSpaces+ void $ P.munch isDigit -- freshness+ void $ pExpectChar ' '+ void $ pExpectChar 'r'+ void $ pExpectChar 'b'+ acceptable <- pExpectChar 'a'+ void $ pExpectChar 'f'+ preferred <- pExpectChar 'p'+ return ( neighbor_addr,+ DIO.DIOLink+ { DIO.neighborType = if preferred+ then DIO.PreferredParent+ else if acceptable+ then DIO.ParentCandidate+ else DIO.OtherNeighbor,+ DIO.neighborRank = neighbor_rank,+ DIO.metric = Just metric+ }+ )++pDIONeighborEnd :: Parser ()+pDIONeighborEnd = void $ P.string "nbr: end of list"++pLogHead :: Parser ()+pLogHead = do+ void $ P.char '['+ void $ P.munch (not . (== ']'))+ void $ P.string "] "++pDAOLogHeader :: Parser Word+pDAOLogHeader = do+ void $ P.string "links: "+ route_num <- pNum+ void $ P.string " routing links in total "+ return route_num++pDAOLink :: Parser (IPv6, Maybe (IPv6, Word))+pDAOLink = do+ void $ P.string "links: "+ child <- pMaybeCompactAddress+ mparent <- optional pParentAndLifetime+ return (child, mparent)+ where+ pParentAndLifetime = (,)+ <$> (P.string " to " *> pMaybeCompactAddress)+ <*> (P.string " (lifetime: " *> pNum <* P.string " seconds)")+ ++pDAOLinkEnd :: Parser ()+pDAOLinkEnd = void $ P.string "links: end of list"++-- | Parse the head of Cooja log line, and return the timestamp and+-- node ID.+pCoojaLogHead :: Parser (Timestamp, Int)+pCoojaLogHead = do+ ts_min <- pNum+ void $ P.string ":"+ ts_sec <- pNum+ void $ P.string "."+ ts_msec <- pNum+ P.skipSpaces+ void $ P.string "ID:"+ node_id <- pNum+ P.skipSpaces+ return (makeTs ts_min ts_sec ts_msec, node_id)+ where+ makeTs :: Int64 -> Int64 -> Int64 -> Timestamp+ makeTs ts_min ts_sec ts_msec = fromEpochMillisecond ((ts_min * 60 + ts_sec) * 1000 + ts_msec)++-- | Same as 'pCoojaLogHead', but it returns the timestamp only.+pCoojaLogHead' :: Parser Timestamp+pCoojaLogHead' = fmap fst pCoojaLogHead++-- | Parser for head of syslog line with its default format.+-- @\"Mmm dd hh:mm:ss HOSTNAME TAG: \"@.+--+-- Because the format does not contain year, you have to pass it to+-- this function.+pSyslogHead :: Integer -- ^ year+ -> Maybe Time.TimeZone -- ^ optional time zone.+ -> Parser Timestamp+pSyslogHead year mtz = do+ ts <- pSyslogTimestamp year mtz+ P.skipSpaces+ void $ P.munch (not . isSpace) -- hostname+ P.skipSpaces+ void $ P.munch (not . isSpace) -- tag+ P.skipSpaces+ return ts++pSyslogTimestamp :: Integer -> Maybe Time.TimeZone -> Parser Timestamp+pSyslogTimestamp year mtz = do+ month <- pMonth+ P.skipSpaces+ day <- pNum+ P.skipSpaces+ hour <- pNum <* P.string ":"+ minute <- pNum <* P.string ":"+ sec <- pNum+ let lt = Time.LocalTime (Time.fromGregorian year month day) (Time.TimeOfDay hour minute sec)+ case mtz of+ Nothing -> return $ fromLocalTime lt+ Just tz -> return $ fromZonedTime $ Time.ZonedTime lt tz+ where+ pMonth = do+ mstr <- P.munch1 (not . isSpace)+ case mstr of+ "Jan" -> return 1+ "Feb" -> return 2+ "Mar" -> return 3+ "Apr" -> return 4+ "May" -> return 5+ "Jun" -> return 6+ "Jul" -> return 7+ "Aug" -> return 8+ "Sep" -> return 9+ "Oct" -> return 10+ "Nov" -> return 11+ "Dec" -> return 12+ _ -> fail ("Invalid for a month: " <> mstr)+
+ src/NetSpider/RPL/DAO.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module: NetSpider.RPL.DAO+-- Description: Node and link information based on DAO (Destination Advertisement Object)+-- Maintainer: Toshio Ito <debug.ito@gmail.com>+--+-- +module NetSpider.RPL.DAO+ ( -- * Types+ FoundNodeDAO,+ DAONode(..),+ DAOLink(..),+ -- * Unifier+ daoUnifierConf+ ) where++import Data.Greskell+ ( gIdentity+ )+ +import Control.Applicative ((<$>), (<*>))+import Data.Aeson (ToJSON(..))+import Data.Greskell+ ( parseOneValue+ )+import Data.Greskell.Extra (writePropertyKeyValues)+import NetSpider.Found (FoundNode)+import NetSpider.Graph (NodeAttributes(..), LinkAttributes(..))+import qualified NetSpider.Pangraph as Pan+import NetSpider.Pangraph.Atom (toAtom, Atom)+import NetSpider.Unify (UnifyStdConfig, lsLinkAttributes, latestLinkSample)+import qualified NetSpider.Unify as Unify++import NetSpider.RPL.FindingID (FindingID)++-- | 'FoundNode' for a network described by DAOs.+type FoundNodeDAO = FoundNode FindingID DAONode DAOLink++-- | Node attributes about DAO.+data DAONode =+ DAONode+ { daoRouteNum :: Maybe Word+ -- ^ Number of DAO routes (downward routes) in the routing table+ -- of the node. Exact meaning of the "number of routes" depends on+ -- implementation.+ --+ -- In Non-Storing mode, 'daoRouteNum' for non-root nodes are+ -- supposed to be 'Nothing', because they don't have a downward+ -- routing table.+ }+ deriving (Show,Eq,Ord)++instance NodeAttributes DAONode where+ writeNodeAttributes dn = writePropertyKeyValues pairs+ where+ pairs = [ ("dao_route_num", toJSON $ daoRouteNum dn)+ ]+ parseNodeAttributes ps = DAONode <$> parseOneValue "dao_route_num" ps++instance Pan.ToAttributes DAONode where+ toAttributes dn = + case daoRouteNum dn of+ Nothing -> []+ Just p -> [("dao_route_num", toAtom $ p)]++-- | Link attributes about DAO.+--+-- In Storing mode of RPL, a 'DAOLink' represents a link between the+-- sender and the receiver of a DAO. In non-storing mode, it's a link+-- between the sender of the DAO and the node specified in the+-- \"parent address\" field of the Transit Information option+-- contained in the DAO.+data DAOLink =+ DAOLink+ { pathLifetimeSec :: Word+ -- ^ Remaining lifetime of this link in seconds.+ --+ -- Lifetime is advertised in Transit Information Option in DAO,+ -- and managed in the routing table.+ }+ deriving (Show,Eq,Ord)++instance LinkAttributes DAOLink where+ writeLinkAttributes dl = writePropertyKeyValues pairs+ where+ pairs = [ ("path_lifetime_sec", toJSON $ pathLifetimeSec dl)+ ]+ parseLinkAttributes ps = DAOLink <$> parseOneValue "path_lifetime_sec" ps++instance Pan.ToAttributes DAOLink where+ toAttributes dl = [ ("path_lifetime_sec", toAtom $ pathLifetimeSec dl) ]++-- | 'UnifyStdConfig' for RPL DAO data.+daoUnifierConf :: UnifyStdConfig FindingID DAONode DAOLink DAOLink ()+daoUnifierConf = Unify.defUnifyStdConfig { Unify.negatesLinkSample = \_ _ -> False }
+ src/NetSpider/RPL/DIO.hs view
@@ -0,0 +1,207 @@+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module: NetSpider.RPL.DIO+-- Description: Node and link information based on DIO (DODAG Information Object)+-- Maintainer: Toshio Ito <debug.ito@gmail.com>+--+-- +module NetSpider.RPL.DIO+ ( -- * Types+ FoundNodeDIO,+ DIONode(..),+ DIOLink(..),+ dioLinkState,+ MergedDIOLink(..),+ Rank,+ NeighborType(..),+ neighborTypeToText,+ neighborTypeFromText,+ -- * Unifier+ dioUnifierConf+ ) where++import Control.Applicative ((<$>), (<*>))+import Data.Aeson (ToJSON(..))+import Data.Bifunctor (bimap)+import Data.Greskell+ ( PropertyMap, Property, GValue, parseOneValue,+ Binder, Walk, SideEffect, Element, Parser+ )+import Data.Greskell.Extra (writePropertyKeyValues)+import Data.Monoid ((<>))+import Data.Text (Text, unpack)+import Data.Word (Word32)+import NetSpider.Found (FoundNode, LinkState(..))+import NetSpider.Graph (NodeAttributes(..), LinkAttributes(..))+import qualified NetSpider.Pangraph as Pan+import NetSpider.Pangraph.Atom (toAtom, Atom)+import NetSpider.Unify (UnifyStdConfig, lsLinkAttributes, latestLinkSample)+import qualified NetSpider.Unify as Unify++import NetSpider.RPL.FindingID (FindingID)++-- | 'FoundNode' for a network described by DIOs.+type FoundNodeDIO = FoundNode FindingID DIONode DIOLink++-- | RPL rank+type Rank = Word++-- | The interval of Trickle timer as decribed as number of doublings+-- of the minimum interval, i.e. log2(I / Imin).+type TrickleInterval = Word++-- | Node attributes about DIO.+data DIONode =+ DIONode+ { rank :: Rank,+ -- ^ RPL rank+ dioInterval :: TrickleInterval+ -- ^ Current interval of Trickle timer for DIO transmission.+ }+ deriving (Show,Eq,Ord)++instance NodeAttributes DIONode where+ writeNodeAttributes ln = writePropertyKeyValues pairs+ where+ pairs = [ ("rank", toJSON $ rank ln),+ ("dio_interval", toJSON $ dioInterval ln)+ ]+ parseNodeAttributes ps = DIONode+ <$> parseOneValue "rank" ps+ <*> parseOneValue "dio_interval" ps++instance Pan.ToAttributes DIONode where+ toAttributes ln = [ ("rank", toAtom $ rank ln),+ ("dio_interval", toAtom $ dioInterval ln)+ ]++-- | Classification of RPL neighbors.+data NeighborType = PreferredParent+ -- ^ The neighbor is the preferred parent.+ | ParentCandidate+ -- ^ The neighbor is not the preferred parent, but+ -- is in the parent set.+ | OtherNeighbor+ -- ^ The neighbor is not in the parent set.+ deriving (Show,Eq,Ord,Enum,Bounded)++neighborTypeToText :: NeighborType -> Text+neighborTypeToText nt = case nt of+ PreferredParent -> "preferred_parent"+ ParentCandidate -> "parent_candidate"+ OtherNeighbor -> "other_neighbor"++neighborTypeFromText :: Text -> Maybe NeighborType+neighborTypeFromText t = case t of+ "preferred_parent" -> return PreferredParent+ "parent_candidate" -> return ParentCandidate+ "other_neighbor" -> return OtherNeighbor+ _ -> Nothing++-- | Unsafely convert walk's type signature+adaptWalk :: (Element e1, Element e2) => Walk SideEffect e1 e1 -> Walk SideEffect e2 e2+adaptWalk = bimap undefined undefined++instance LinkAttributes NeighborType where+ writeLinkAttributes nt = writePropertyKeyValues [("neighbor_type", neighborTypeToText nt)]+ parseLinkAttributes ps = fromT =<< parseOneValue "neighbor_type" ps+ where+ fromT t = maybe (fail ("Unknown neighbor type: " <> unpack t)) return $ neighborTypeFromText t++-- | Link attributes about DIO.+--+-- Basically this represents information of a neighbor learned from+-- the DIOs it has sent.+data DIOLink =+ DIOLink+ { neighborType :: NeighborType,+ -- ^ Type of the neighbor at the other end of this link.+ neighborRank :: Rank,+ -- ^ Observed rank of the neighbor.+ metric :: Maybe Rank+ -- ^ Link metric of this link, calculated as step of Rank. Because+ -- Rank computation is up to the Objective Function, this field is+ -- optional.+ }+ deriving (Show,Eq,Ord)++instance LinkAttributes DIOLink where+ writeLinkAttributes ll = do+ nt_steps <- writeLinkAttributes $ neighborType ll+ other <- writePropertyKeyValues pairs+ return (adaptWalk nt_steps <> other)+ where+ pairs = [ ("neighbor_rank", toJSON $ neighborRank ll),+ ("metric", toJSON $ metric ll)+ ]+ parseLinkAttributes ps =+ DIOLink+ <$> parseLinkAttributes ps+ <*> parseOneValue "neighbor_rank" ps+ <*> parseOneValue "metric" ps++-- | 'LinkState' that should be set for given 'DIOLink'.+dioLinkState :: DIOLink -> LinkState+dioLinkState l =+ case neighborType l of+ PreferredParent -> LinkToTarget+ _ -> LinkUnused++toAttributesPrefix :: Atom -> DIOLink -> [Pan.Attribute]+toAttributesPrefix prefix ll = + [ (prefix <> "neighbor_type", toAtom $ neighborTypeToText $ neighborType ll),+ (prefix <> "neighbor_rank", toAtom $ neighborRank ll)+ ]+ +++ ( case metric ll of+ Nothing -> []+ Just metric_val -> [(prefix <> "metric", toAtom metric_val)]+ )++instance Pan.ToAttributes DIOLink where+ toAttributes = toAttributesPrefix ""++-- | Link attributes merging two 'DIOLink's from the two end nodes+-- of the link.+data MergedDIOLink =+ MergedDIOLink+ { fromSource :: DIOLink,+ fromDest :: Maybe DIOLink+ }+ deriving (Show,Eq,Ord)++-- | 'UnifyStdConfig' for RPL DIO data+dioUnifierConf :: UnifyStdConfig FindingID DIONode DIOLink MergedDIOLink ()+dioUnifierConf = Unify.UnifyStdConfig+ { Unify.makeLinkSubId = const (),+ Unify.mergeSamples = merger,+ Unify.negatesLinkSample = \_ _ -> False+ }+ where+ merger llinks rlinks =+ case (latestLinkSample llinks, latestLinkSample rlinks) of+ (Nothing, Nothing) -> Nothing+ (Just ll, Nothing) -> Just $ doMerge ll Nothing+ (Nothing, Just rl) -> Just $ doMerge rl Nothing+ (Just ll, Just rl) -> Just $ doMerge ll $ Just rl+ doMerge main_link msub_link =+ case msub_link of+ Nothing -> main_link+ { lsLinkAttributes = MergedDIOLink main_ll Nothing }+ Just sub_link ->+ if neighborType main_ll <= neighborType sub_ll+ then main_link { lsLinkAttributes = MergedDIOLink main_ll $ Just sub_ll }+ else sub_link { lsLinkAttributes = MergedDIOLink sub_ll $ Just main_ll }+ where+ sub_ll = lsLinkAttributes sub_link+ where+ main_ll = lsLinkAttributes main_link++instance Pan.ToAttributes MergedDIOLink where+ toAttributes ml =+ toAttributesPrefix "source_" (fromSource ml)+ +++ ( case fromDest ml of+ Nothing -> []+ Just dl -> toAttributesPrefix "dest_" dl+ )
+ src/NetSpider/RPL/FindingID.hs view
@@ -0,0 +1,144 @@+{-# LANGUAGE OverloadedStrings, DeriveGeneric #-}+-- |+-- Module: NetSpider.RPL.FindingID+-- Description: NetSpider Node ID type for RPL network graph+-- Maintainer: Toshio Ito <debug.ito@gmail.com>+--+-- +module NetSpider.RPL.FindingID+ ( -- * FindingID+ FindingID(..),+ idToText,+ idFromText,+ -- * FindingType+ FindingType(..),+ typeToText,+ typeFromText,+ -- * IPv6ID+ IPv6ID(..),+ ipv6ToText,+ ipv6FromText,+ ipv6Only+ ) where++import Control.Applicative ((<$>), (<*>))+import Control.Monad.Fail (MonadFail)+import Data.Monoid ((<>))+import Data.Aeson (FromJSON(..), ToJSON(..))+import qualified Data.Aeson as Aeson+import Data.Greskell (FromGraphSON(..))+import Data.Hashable (Hashable(..))+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)+import Net.IPv6 (IPv6(..))+import qualified Net.IPv6 as IPv6+import NetSpider.Pangraph.Atom (ToAtom(..))++-- | Type of local finding.+data FindingType = FindingDIO+ -- ^ Local finding about DIO (Upward) routes.+ | FindingDAO+ -- ^ Local finding about DAO (Downward) routes.+ deriving (Show,Eq,Ord,Enum,Bounded,Generic)++instance Hashable FindingType++typeToText :: FindingType -> Text+typeToText ft =+ case ft of+ FindingDIO -> "dio"+ FindingDAO -> "dao"++typeFromText :: Text -> Maybe FindingType+typeFromText t =+ case t of+ "dio" -> Just FindingDIO+ "dao" -> Just FindingDAO+ _ -> Nothing++-- | The node ID.+--+-- Basically a node is identified by its IPv6 address in RPL+-- network. 'FindingID' is distinguished by 'FindingType' as well,+-- because in RPL there can be difference between topology formed by+-- DIOs and DAOs.+data FindingID =+ FindingID+ { findingType :: FindingType,+ -- ^ Finding type+ nodeAddress :: IPv6+ -- ^ IPv6 address of the subject node.+ }+ deriving (Show,Eq,Ord,Generic)++idToText :: FindingID -> Text+idToText fid = ft_str <> "://[" <> addr_str <> "]"+ where+ ft_str = typeToText $ findingType fid+ addr_str = ipv6ToText $ IPv6ID $ nodeAddress fid++idFromText :: Text -> Maybe FindingID+idFromText t = FindingID <$> m_ft <*> m_addr+ where+ (ft_str, rest) = T.breakOn "://[" t+ (addr_str, _) = T.breakOn "]" $ T.drop 4 rest+ m_ft = typeFromText ft_str+ m_addr = fmap unIPv6ID $ ipv6FromText addr_str++instance ToJSON FindingID where+ toJSON = Aeson.String . idToText++parseFromText :: MonadFail m => Text -> m FindingID+parseFromText t = + case idFromText t of+ Nothing -> fail ("Invalid FindingID: " <> T.unpack t)+ Just fid -> return fid++instance FromJSON FindingID where+ parseJSON v = parseFromText =<< parseJSON v++instance FromGraphSON FindingID where+ parseGraphSON gv = parseFromText =<< parseGraphSON gv++instance Hashable FindingID where+ hashWithSalt s fid = s `hashWithSalt` ft `hashWithSalt` addr_id+ where+ ft = findingType fid+ addr_id = IPv6ID $ nodeAddress fid++instance ToAtom FindingID where+ toAtom = toAtom . idToText++-- | 'IPv6' address with additional type-class instances.+newtype IPv6ID = IPv6ID { unIPv6ID :: IPv6 }+ deriving (Show,Eq,Ord,Generic)++instance Hashable IPv6ID where+ hashWithSalt s (IPv6ID a) =+ s `hashWithSalt` (ipv6A a) `hashWithSalt` (ipv6B a)++ipv6ToText :: IPv6ID -> Text+ipv6ToText (IPv6ID a) = IPv6.encode a++ipv6FromText :: Text -> Maybe IPv6ID+ipv6FromText = fmap IPv6ID . IPv6.decode++parseIPv6IDFromText :: MonadFail m => Text -> m IPv6ID+parseIPv6IDFromText t =+ case ipv6FromText t of+ Nothing -> fail ("Invalid IPv6 address: " <> T.unpack t)+ Just a -> return a++instance FromJSON IPv6ID where+ parseJSON v = parseIPv6IDFromText =<< parseJSON v++instance FromGraphSON IPv6ID where+ parseGraphSON gv = parseIPv6IDFromText =<< parseGraphSON gv++instance ToAtom IPv6ID where+ toAtom = toAtom . ipv6ToText++-- | Extract 'IPv6ID' from 'FindingID'.+ipv6Only :: FindingID -> IPv6ID+ipv6Only = IPv6ID . nodeAddress
+ src/NetSpider/RPL/IPv6.hs view
@@ -0,0 +1,33 @@+-- |+-- Module: NetSpider.RPL.IPv6+-- Description: +-- Maintainer: Toshio Ito <debug.ito@gmail.com>+--+-- __This module is for internal use.__+module NetSpider.RPL.IPv6+ ( Prefix,+ InterfaceID,+ isLinkLocal,+ getPrefix,+ setPrefix+ ) where++import Data.Bits ((.&.), shift)+import Data.Word (Word64)+import Net.IPv6 (IPv6(..), toWord16s)++type Prefix = Word64++type InterfaceID = Word64++isLinkLocal :: IPv6 -> Bool+isLinkLocal addr = (== 0xfe80) $ (.&. bit_mask) $ top_word+ where+ (top_word, _, _, _, _, _, _, _) = toWord16s addr+ bit_mask = (2 ^ (10 :: Int) - 1) `shift` 6++getPrefix :: IPv6 -> Prefix+getPrefix = ipv6A++setPrefix :: Prefix -> IPv6 -> IPv6+setPrefix p orig = orig { ipv6A = p }
+ test/NetSpider/RPL/ContikiNGSpec.hs view
@@ -0,0 +1,259 @@+{-# LANGUAGE OverloadedStrings #-}+module NetSpider.RPL.ContikiNGSpec (main,spec) where++import Data.Maybe (fromJust)+import Data.Text (Text)+import NetSpider.Found (FoundNode(..), FoundLink(..), LinkState(..))+import NetSpider.Timestamp (Timestamp, fromEpochMillisecond)+import Test.Hspec++import NetSpider.RPL.ContikiNG (parseFile, pCoojaLogHead', pSyslogHead)+import NetSpider.RPL.FindingID (idFromText)+import qualified NetSpider.RPL.DIO as DIO+import qualified NetSpider.RPL.DAO as DAO++main :: IO ()+main = hspec spec++daoEntry :: Timestamp -> Text -> Maybe Word -> [(Text, Word)] -> DAO.FoundNodeDAO+daoEntry ts src mroute_num dests =+ FoundNode+ { subjectNode = fromJust $ idFromText src,+ foundAt = ts,+ nodeAttributes = DAO.DAONode mroute_num,+ neighborLinks = map toLink dests+ }+ where+ toLink (dest, lifetime) =+ FoundLink+ { targetNode = fromJust $ idFromText dest,+ linkState = LinkToTarget,+ linkAttributes = DAO.DAOLink lifetime+ }++spec :: Spec+spec = do+ describe "parseFile" $ do+ specify "cooja log" $ do+ let exp_dio =+ FoundNode { subjectNode = fromJust $ idFromText "dio://[fd00::201:1:1:1]",+ foundAt = fromEpochMillisecond 60382,+ neighborLinks = exp_dio_links,+ nodeAttributes = DIO.DIONode { DIO.rank = 128, DIO.dioInterval = 15 }+ }+ exp_dio_links =+ [ FoundLink { targetNode = fromJust $ idFromText "dio://[fd00::202:2:2:2]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 299,+ DIO.metric = Just 141+ }+ }+ ]+ exp_daos = [ daoEntry (fromEpochMillisecond 60382)+ "dao://[fd00::201:1:1:1]" (Just 2)+ [("dao://[fd00::202:2:2:2]", 1740)]+ ]+ (got_dios, got_daos) <- parseFile pCoojaLogHead' "test/data/cooja.log"+ got_dios `shouldBe` [exp_dio]+ got_daos `shouldBe` exp_daos+ specify "syslog root log" $ do+ let exp_timestamp = fromEpochMillisecond 1546968337000+ exp_dio =+ FoundNode+ { subjectNode = fromJust $ idFromText "dio://[fd00::212:1199:eebb:62c4]",+ foundAt = exp_timestamp,+ nodeAttributes = DIO.DIONode { DIO.rank = 128, DIO.dioInterval = 14 },+ neighborLinks =+ [ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::212:1199:bbcc:4fdf]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 262,+ DIO.metric = Just 128+ }+ },+ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::212:1199:bbcc:d52d]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 263,+ DIO.metric = Just 128+ }+ },+ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::212:1199:bbcc:5e88]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 256,+ DIO.metric = Just 128+ }+ },+ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::212:1199:eebb:62fe]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 256,+ DIO.metric = Just 129+ }+ }+ ]+ }+ exp_daos = [ daoEntry exp_timestamp+ "dao://[fd00::212:1199:eebb:62c4]" (Just 5)+ [ ("dao://[fd00::212:1199:bbcc:d52d]", 1080),+ ("dao://[fd00::212:1199:bbcc:4fdf]", 1260),+ ("dao://[fd00::212:1199:eebb:62fe]", 1440),+ ("dao://[fd00::212:1199:bbcc:5e88]", 1140)+ ]+ ]+ pHead = pSyslogHead 2019 Nothing+ (got_dios, got_daos) <- parseFile pHead "test/data/syslog_root.log"+ got_dios `shouldBe` [exp_dio]+ got_daos `shouldBe` exp_daos+ specify "syslog nonroot" $ do+ let exp_timestamp = fromEpochMillisecond 1547558149000+ exp_dio =+ FoundNode+ { subjectNode = fromJust $ idFromText "dio://[fd00::212:eeaa:0077:2f9c]",+ foundAt = exp_timestamp,+ nodeAttributes = DIO.DIONode { DIO.rank = 423, DIO.dioInterval = 16 },+ neighborLinks =+ [ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::212:eeaa:33cc:632a]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 256,+ DIO.metric = Just 198+ }+ },+ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::212:eeaa:33ff:a874]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 256,+ DIO.metric = Just 177+ }+ },+ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::212:eeaa:33cc:63d0]",+ linkState = LinkToTarget,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.PreferredParent,+ DIO.neighborRank = 272,+ DIO.metric = Just 151+ }+ },+ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::212:eeaa:9977:13ba]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 283,+ DIO.metric = Just 152+ }+ },+ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::212:eeaa:33cc:6350]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 400,+ DIO.metric = Just 171+ }+ },+ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::212:eeaa:e88:db36]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 432,+ DIO.metric = Just 166+ }+ },+ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::212:eeaa:9977:b4]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.OtherNeighbor,+ DIO.neighborRank = 584,+ DIO.metric = Just 65535+ }+ }+ ]+ }+ pHead = pSyslogHead 2019 Nothing+ (got_dios, got_daos) <- parseFile pHead "test/data/syslog_nonroot.log"+ got_dios `shouldBe` [exp_dio]+ got_daos `shouldBe` []+ specify "syslog sr tables" $ do+ let exp_ts_jan = fromEpochMillisecond 1548843376000+ exp_ts_feb = fromEpochMillisecond 1549373162000+ exp_dio1 =+ FoundNode+ { subjectNode = fromJust $ idFromText "dio://[fd00::222:5566:cc99:62c4]",+ foundAt = exp_ts_jan,+ nodeAttributes = DIO.DIONode { DIO.rank = 128, DIO.dioInterval = 15 },+ neighborLinks = []+ }+ exp_dio2 =+ FoundNode+ { subjectNode = fromJust $ idFromText "dio://[fd00::222:5566:cc99:62c4]",+ foundAt = exp_ts_feb,+ nodeAttributes = DIO.DIONode { DIO.rank = 128, DIO.dioInterval = 18 },+ neighborLinks = exp_dio_links2+ }+ exp_dio_links2 =+ [ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::222:5566:ddee:4fdf]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 256,+ DIO.metric = Just 128+ }+ },+ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::222:5566:ddee:d52d]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 396,+ DIO.metric = Just 128+ }+ },+ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::222:5566:cc99:62fe]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 266,+ DIO.metric = Just 137+ }+ },+ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::222:5566:ddee:401e]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 384,+ DIO.metric = Just 128+ }+ },+ FoundLink+ { targetNode = fromJust $ idFromText "dio://[fd00::222:5566:ddee:5e88]",+ linkState = LinkUnused,+ linkAttributes = DIO.DIOLink { DIO.neighborType = DIO.ParentCandidate,+ DIO.neighborRank = 406,+ DIO.metric = Just 128+ }+ }+ ]+ exp_daos = [ daoEntry exp_ts_feb+ "dao://[fd00::222:5566:cc99:62c4]" (Just 6)+ [ ("dao://[fd00::222:5566:ddee:4fdf]", 1140),+ ("dao://[fd00::222:5566:cc99:62fe]", 1380)+ ],+ daoEntry exp_ts_feb+ "dao://[fd00::222:5566:ddee:4fdf]" Nothing+ [ ("dao://[fd00::222:5566:ddee:d52d]", 1380),+ ("dao://[fd00::222:5566:ddee:401e]", 1740),+ ("dao://[fd00::222:5566:ddee:5e88]", 1740)+ ]+ ]+ pHead = pSyslogHead 2019 Nothing+ (got_dios, got_daos) <- parseFile pHead "test/data/syslog_sr_tables.log"+ got_dios `shouldBe` [exp_dio1, exp_dio2]+ got_daos `shouldMatchList` exp_daos+
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/data/cooja.log view
@@ -0,0 +1,23 @@+01:00.339 ID:2 [INFO: 6LoWPAN ] input: fragment (tag 4, payload 96, offset 232) -- 101 5+01:00.339 ID:2 [INFO: TSCH-LOG ] {asn 00.0000176a link 0 7 2 2 0 ch 15} uc-1-0 rx LL-0001->LL-0002, len 122, seq 29, edr 0, dr 0+01:00.339 ID:1 [INFO: RPL ] packet sent to 0002.0002.0002.0002, status 0, tx 1, new link metric 143+01:00.347 ID:2 [INFO: 6LoWPAN ] input: fragment (tag 4, payload 24, offset 328) -- 29 5+01:00.347 ID:2 [INFO: 6LoWPAN ] input: received IPv6 packet with len 352+01:00.347 ID:2 [INFO: TCP/IP ] input: received 352 bytes+01:00.347 ID:2 [INFO: IPv6 ] packet received from fd00::201:1:1:1 to fd00::202:2:2:2+01:00.347 ID:2 [INFO: IPv6 ] icmpv6 input length 352 type: 129 +01:00.347 ID:2 [INFO: ICMPv6 ] Received Echo Reply from fd00::201:1:1:1 to fd00::202:2:2:2+01:00.347 ID:2 [INFO: TSCH-LOG ] {asn 00.0000176b link 0 7 3 3 0 ch 15} uc-1-0 rx LL-0001->LL-0002, len 50, seq 30, edr 0, dr 0+01:00.347 ID:1 [INFO: RPL ] packet sent to 0002.0002.0002.0002, status 0, tx 1, new link metric 141+01:00.382 ID:1 [INFO: RPL ] nbr: own state, addr fd00::201:1:1:1, DAG state: reachable, MOP 1 OCP 1 rank 128 max-rank 65535, dioint 15, nbr count 1 (Periodic)+01:00.382 ID:1 [INFO: RPL ] nbr: fe80::202:2:2:2 299, 141 => 440 -- 16 baf (last tx 0 min ago)+01:00.382 ID:1 [INFO: RPL ] nbr: end of list+01:00.382 ID:1 [INFO: RPL ] links: 2 routing links in total (Periodic)+01:00.382 ID:1 [INFO: RPL ] links: fd00::201:1:1:1 (DODAG root) (lifetime: infinite)+01:00.382 ID:1 [INFO: RPL ] links: fd00::202:2:2:2 to fd00::201:1:1:1 (lifetime: 1740 seconds)+01:00.382 ID:1 [INFO: RPL ] links: end of list+01:01.785 ID:1 [INFO: TSCH-LOG ] {asn 00.000017fb link 0 7 0 0 0 ch 20} bc-0-0 rx LL-0002->LL-NULL, len 35, seq 80, edr 0+01:02.835 ID:2 [INFO: TSCH-LOG ] {asn 00.00001864 link 0 7 0 0 0 ch 15} bc-0-0 rx LL-0001->LL-NULL, len 35, seq 30, edr 0, dr 0+01:05.425 ID:1 [INFO: TSCH-LOG ] {asn 00.00001967 link 0 7 0 0 0 ch 20} bc-0-0 rx LL-0002->LL-NULL, len 35, seq 80, edr 0+01:05.581 ID:2 [INFO: RPL ] sending a multicast-DIO with rank 265 to ff02::1a+01:05.581 ID:2 [INFO: ICMPv6 ] Sending ICMPv6 packet to ff02::1a, type 155, code 1, len 72
+ test/data/syslog_nonroot.log view
@@ -0,0 +1,14 @@+Jan 15 13:15:49 hostname label[15738]: [INFO: TSCH-LOG ] {asn 00.00e136df link 0 7 0 0 ch 16} bc-1-0 tx LL-2f9c->LL-NULL, len 94, seq 95, st 0 1+Jan 15 13:15:49 hostname label[15738]: TSCH associated: 1+Jan 15 13:15:49 hostname label[15738]: [WARN: RPL ] nbr: own state, addr fd00::212:eeaa:0077:2f9c, DAG state: reachable, MOP 1 OCP 1 rank 423 max-rank 1408, dioint 16, nbr count 10 (L-reporter)+Jan 15 13:15:49 hostname label[15738]: [WARN: RPL ] nbr: fe80::212:eeaa:33cc:632a 256, 198 => 454 -- 7 af+Jan 15 13:15:49 hostname label[15738]: [WARN: RPL ] nbr: fe80::212:eeaa:33ff:a874 256, 177 => 433 -- 3 a+Jan 15 13:15:49 hostname label[15738]: [WARN: RPL ] nbr: fe80::212:eeaa:33cc:63d0 272, 151 => 423 -- 16 bafp+Jan 15 13:15:49 hostname label[15738]: [WARN: RPL ] nbr: fe80::212:eeaa:9977:13ba 283, 152 => 435 -- 4 af+Jan 15 13:15:49 hostname label[15738]: [WARN: RPL ] nbr: fe80::212:eeaa:33cc:6350 400, 171 => 571 -- 2 a+Jan 15 13:15:49 hostname label[15738]: [WARN: RPL ] nbr: fe80::212:eeaa:e88:db36 432, 166 => 598 -- 3 a+Jan 15 13:15:49 hostname label[15738]: [WARN: RPL ] nbr: fe80::212:eeaa:9977:b4 584, 65535 => 65535 -- 0+Jan 15 13:15:49 hostname label[15738]: [WARN: RPL ] nbr: end of list+Jan 15 13:15:49 hostname label[15738]: [INFO: TSCH-LOG ] {asn 00.00e136f4 link 0 7 0 0 ch 10} bc-0-0 rx LL-1391->LL-NULL, len 35, seq 19, edr -9+Jan 15 13:15:50 hostname label[15738]: [INFO: TSCH-LOG ] {asn 00.00e1370b link 2 7 2 0 ch 16} bc-1-0 rx LL-1391->LL-NULL, len 94, seq 20, edr -5+Jan 15 13:15:50 hostname label[15738]: [INFO: TSCH-LOG ] {asn 00.00e1371e link 0 7 0 0 ch 14} bc-0-0 tx LL-2f9c->LL-NULL, len 35, seq 96, st 0 1
+ test/data/syslog_root.log view
@@ -0,0 +1,13 @@+Jan 8 17:25:37 hostname syslog_label[1715]: [INFO: RPL ] nbr: own state, addr fd00::212:1199:eebb:62c4, DAG state: reachable, MOP 1 OCP 1 rank 128 max-rank 65535, dioint 14, nbr count 5 (Periodic)+Jan 8 17:25:37 hostname syslog_label[1715]: [INFO: RPL ] nbr: fe80::212:1199:bbcc:4fdf 262, 128 => 390 -- 0 a (last tx 4945 min ago)+Jan 8 17:25:37 hostname syslog_label[1715]: [INFO: RPL ] nbr: fe80::212:1199:bbcc:d52d 263, 128 => 391 -- 0 a (last tx 1870 min ago)+Jan 8 17:25:37 hostname syslog_label[1715]: [INFO: RPL ] nbr: fe80::212:1199:bbcc:5e88 256, 128 => 384 -- 0 ba (last tx 4435 min ago)+Jan 8 17:25:37 hostname syslog_label[1715]: [INFO: RPL ] nbr: fe80::212:1199:eebb:62fe 256, 129 => 385 -- 0 a (last tx 2020 min ago)+Jan 8 17:25:37 hostname syslog_label[1715]: [INFO: RPL ] nbr: end of list+Jan 8 17:25:37 hostname syslog_label[1715]: [INFO: RPL ] links: 5 routing links in total (Periodic)+Jan 8 17:25:37 hostname syslog_label[1715]: [INFO: RPL ] links: fd00::212:1199:eebb:62c4 (DODAG root) (lifetime: infinite)+Jan 8 17:25:37 hostname syslog_label[1715]: [INFO: RPL ] links: fd00::212:1199:bbcc:d52d to fd00::212:1199:eebb:62c4 (lifetime: 1080 seconds)+Jan 8 17:25:37 hostname syslog_label[1715]: [INFO: RPL ] links: fd00::212:1199:bbcc:4fdf to fd00::212:1199:eebb:62c4 (lifetime: 1260 seconds)+Jan 8 17:25:37 hostname syslog_label[1715]: [INFO: RPL ] links: fd00::212:1199:eebb:62fe to fd00::212:1199:eebb:62c4 (lifetime: 1440 seconds)+Jan 8 17:25:37 hostname syslog_label[1715]: [INFO: RPL ] links: fd00::212:1199:bbcc:5e88 to fd00::212:1199:eebb:62c4 (lifetime: 1140 seconds)+Jan 8 17:25:37 hostname syslog_label[1715]: [INFO: RPL ] links: end of list
+ test/data/syslog_sr_tables.log view
@@ -0,0 +1,20 @@+Jan 30 10:16:16 hostname tag[3005]: [INFO: RPL ] nbr: own state, addr fd00::222:5566:cc99:62c4, DAG state: reachable, MOP 1 OCP 1 rank 128 max-rank 65535, dioint 15, nbr count 0 (Periodic)+Jan 30 10:16:16 hostname tag[3005]: [INFO: RPL ] nbr: end of list+Jan 30 10:16:16 hostname tag[3005]: [INFO: RPL ] No routing links+Feb 5 13:25:58 hostname tag[20840]: [INFO: RPL ] packet sent to 0222.5566.ddee.4fdf, status 0, tx 1, new link metric 128+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] nbr: own state, addr fd00::222:5566:cc99:62c4, DAG state: reachable, MOP 1 OCP 1 rank 128 max-rank 65535, dioint 18, nbr count 5 (Periodic)+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] nbr: fe80::222:5566:ddee:4fdf 256, 128 => 384 -- 16 baf (last tx 0 min ago)+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] nbr: fe80::222:5566:ddee:d52d 396, 128 => 524 -- 1 a (last tx 7 min ago)+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] nbr: fe80::222:5566:cc99:62fe 266, 137 => 403 -- 13 af (last tx 0 min ago)+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] nbr: fe80::222:5566:ddee:401e 384, 128 => 512 -- 0 a (no tx)+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] nbr: fe80::222:5566:ddee:5e88 406, 128 => 534 -- 0 a (no tx)+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] nbr: end of list+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] links: 6 routing links in total (Periodic)+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] links: fd00::222:5566:cc99:62c4 (DODAG root) (lifetime: infinite)+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] links: fd00::222:5566:ddee:4fdf to fd00::222:5566:cc99:62c4 (lifetime: 1140 seconds)+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] links: fd00::222:5566:ddee:d52d to fd00::222:5566:ddee:4fdf (lifetime: 1380 seconds)+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] links: fd00::222:5566:cc99:62fe to fd00::222:5566:cc99:62c4 (lifetime: 1380 seconds)+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] links: fd00::222:5566:ddee:401e to fd00::222:5566:ddee:4fdf (lifetime: 1740 seconds)+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] links: fd00::222:5566:ddee:5e88 to fd00::222:5566:ddee:4fdf (lifetime: 1740 seconds)+Feb 5 13:26:02 hostname tag[20840]: [INFO: RPL ] links: end of list+Feb 5 13:26:03 hostname tag[20840]: [INFO: RPL ] sending a multicast-DIO with rank 128 to ff02::1a