haskoin-node-2.5.0: test/unit/Haskoin/Node/AddressSpec.hs
module Haskoin.Node.AddressSpec (spec) where
import Control.Exception (IOException, bracket, try)
import Control.Monad (forM_)
import Data.Time.Clock (diffUTCTime, getCurrentTime)
import Data.Word (Word16, Word8)
import Haskoin (Network (..), btcRegTest)
import Haskoin.Node (toHostService, toSockAddr)
import Haskoin.Node.Address (NetGroup (..), isRoutable, netGroup, withConnection)
import Network.Socket
( Family (AF_INET),
SockAddr (..),
SocketType (Stream),
bind,
close,
connect,
defaultProtocol,
listen,
socket,
socketPort,
tupleToHostAddress,
tupleToHostAddress6,
)
import Test.Hspec
import Test.Hspec.QuickCheck (prop)
v4 :: (Word8, Word8, Word8, Word8) -> SockAddr
v4 = SockAddrInet 8333 . tupleToHostAddress
v6 :: (Word16, Word16, Word16, Word16, Word16, Word16, Word16, Word16) -> SockAddr
v6 t = SockAddrInet6 8333 0 (tupleToHostAddress6 t) 0
spec :: Spec
spec = do
describe "routable addresses" $ do
it "refuses the reserved IPv4 ranges" $
forM_
[ (198, 18, 0, 1), -- benchmarking, 198.18.0.0/15
(198, 19, 255, 1),
(192, 0, 2, 1), -- documentation
(198, 51, 100, 1),
(203, 0, 113, 1),
(0, 1, 2, 3), -- this network
(192, 0, 0, 1) -- protocol assignments
]
$ \a -> isRoutable (v4 a) `shouldBe` False
it "accepts the addresses just outside the benchmarking range" $ do
isRoutable (v4 (198, 17, 255, 1)) `shouldBe` True
isRoutable (v4 (198, 20, 0, 1)) `shouldBe` True
it "refuses the reserved IPv6 ranges" $
forM_
[ (0xff02, 0, 0, 0, 0, 0, 0, 1), -- multicast
(0xfec0, 0, 0, 0, 0, 0, 0, 1), -- site-local
(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1) -- documentation
]
$ \a -> isRoutable (v6 a) `shouldBe` False
it "refuses the ORCHID ranges, as Bitcoin Core does" $
-- RFC 4843: 2001:10::/28; RFC 7343: 2001:20::/28
forM_ [0x0010, 0x001f, 0x0020, 0x002f] $ \b ->
isRoutable (v6 (0x2001, b, 0, 0, 0, 0, 0, 1)) `shouldBe` False
it "accepts the addresses just outside the ORCHID ranges" $
forM_ [0x000f, 0x0030] $ \b ->
isRoutable (v6 (0x2001, b, 0, 0, 0, 0, 0, 1)) `shouldBe` True
describe "connections" $ do
it "gives up opening a connection that gets no answer after five seconds" $ do
-- Bitcoin Core: DEFAULT_CONNECT_TIMEOUT = 5000 ms. A listening socket
-- whose queue of one connection is full drops new requests on Linux,
-- so opening a connection to it hangs.
let loopback = tupleToHostAddress (127, 0, 0, 1)
listener = do
s <- socket AF_INET Stream defaultProtocol
bind s (SockAddrInet 0 loopback)
listen s 0
return s
bracket listener close $ \srv -> do
port <- socketPort srv
let sa = SockAddrInet port loopback
bracket (socket AF_INET Stream defaultProtocol) close $ \filler -> do
connect filler sa
start <- getCurrentTime
r <- try (withConnection sa (\_ -> return ()))
end <- getCurrentTime
either (\(_ :: IOException) -> True) (const False) r `shouldBe` True
end `diffUTCTime` start `shouldSatisfy` (\d -> d >= 4.5 && d < 8)
describe "network groups" $ do
-- 45.1.2.3 is 0x2d010203.
it "groups IPv4 addresses by their first two bytes, whatever the port" $ do
netGroup (SockAddrInet 1000 (tupleToHostAddress (45, 1, 2, 3))) `shouldBe` IPv4Group 45 1
netGroup (v4 (45, 1, 200, 9)) `shouldBe` IPv4Group 45 1
netGroup (v4 (45, 2, 0, 1)) `shouldBe` IPv4Group 45 2
it "groups IPv6 addresses that carry an IPv4 address by that address" $ do
netGroup (v6 (0, 0, 0, 0, 0, 0xffff, 0x2d01, 0x0203)) `shouldBe` IPv4Group 45 1
netGroup (v6 (0x0064, 0xff9b, 0, 0, 0, 0, 0x2d01, 0x0203)) `shouldBe` IPv4Group 45 1
netGroup (v6 (0x2002, 0x2d01, 0x0203, 0, 0, 0, 0, 1)) `shouldBe` IPv4Group 45 1
-- Teredo inverts the bits: 0xffffffff - 0x2d010203 = 0xd2fefdfc
netGroup (v6 (0x2001, 0, 0, 0, 0, 0, 0xd2fe, 0xfdfc)) `shouldBe` IPv4Group 45 1
it "groups other IPv6 addresses by their first four bytes" $ do
netGroup (v6 (0x2a01, 0x04f8, 0x0001, 0, 0, 0, 0, 1))
`shouldBe` IPv6Group [0x2a, 0x01, 0x04, 0xf8]
netGroup (v6 (0x2a01, 0x04f8, 0xffff, 0, 0, 0, 0, 1))
`shouldBe` IPv6Group [0x2a, 0x01, 0x04, 0xf8]
it "groups Hurricane Electric addresses by 36 bits" $
-- the fifth byte 0x12 with its low four bits set: 0x1f
netGroup (v6 (0x2001, 0x0470, 0x1234, 0, 0, 0, 0, 1))
`shouldBe` IPv6Group [0x20, 0x01, 0x04, 0x70, 0x1f]
it "puts loopback and unroutable addresses in groups of their own" $ do
netGroup (v4 (127, 0, 0, 1)) `shouldBe` LocalGroup
netGroup (v6 (0, 0, 0, 0, 0, 0, 0, 1)) `shouldBe` LocalGroup
netGroup (v4 (10, 0, 0, 1)) `shouldBe` UnroutableGroup
let net = btcRegTest
describe "addresses" $ do
it "parses host and service"
$ forM_
[ ("localhost", (Just "localhost", Nothing)),
("::1", (Just "::1", Nothing)),
("localhost:8080", (Just "localhost", Just "8080")),
("example.com", (Just "example.com", Nothing)),
("api.example.com:443", (Just "api.example.com", Just "443")),
("api.example.com:http", (Just "api.example.com", Just "http")),
("[::1]", (Just "::1", Nothing)),
("[::1]:8080", (Just "::1", Just "8080")),
("[2002::dead:beef]:ssh", (Just "2002::dead:beef", Just "ssh"))
]
$ \(s, r) -> toHostService s `shouldBe` r
it "resolves numeric addresses with the default port" $ do
as <- toSockAddr net "127.0.0.1"
map show as `shouldContain` ["127.0.0.1:" <> show net.defaultPort]
it "returns no addresses for unresolvable hosts" $
toSockAddr net "host.invalid" `shouldReturn` []
it "keeps an explicit port" $ do
as <- toSockAddr net "127.0.0.1:1234"
map show as `shouldContain` ["127.0.0.1:1234"]
-- IPv4 only: resolving an IPv6 address needs IPv6 on the host, which
-- containers and CI runners often lack. The IPv6 forms are still parsed
-- by the toHostService tests above, which need no resolver.
prop "resolves any numeric IPv4 address to itself" $ \(e, w) -> do
let a = SockAddrInet (toEnum (e `mod` 65536)) w
s <- toSockAddr net (show a)
s `shouldStartWith` [a]