packages feed

haskoin-node-2.4.0: src/Haskoin/Node/Address.hs

-- |
-- Module      : Haskoin.Node.Address
-- Copyright   : (c) Haskoin Developers
-- License     : MIT
-- Maintainer  : jprupp@protonmail.ch
-- Stability   : experimental
-- Portability : POSIX
--
-- Peer addresses: parsing them, deciding which relayed addresses are worth
-- connecting to, and opening TCP connections.
--
-- "Haskoin.Node.PeerMgr" re-exports 'toHostService', 'toSockAddr' and
-- 'isRoutable', and "Haskoin.Node" re-exports 'withConnection', where they
-- used to be defined. They live here because none of them depends on the
-- peer manager or the node: they are about addresses and sockets only.
module Haskoin.Node.Address
  ( toHostService,
    toSockAddr,
    isRoutable,
    withConnection,
  )
where

import Control.Applicative ((<|>))
import Control.Arrow (second)
import Control.Exception (IOException, catch)
import Data.Bits ((.&.))
import Data.Conduit.Network
  ( ClientSettings,
    appSink,
    appSource,
    clientSettings,
    runTCPClient,
  )
import Data.List (elemIndex)
import Data.String.Conversions (cs)
import Data.Word (Word8)
import Haskoin (Network (..))
import Haskoin.Node.Peer (Conduits (..), WithConnection)
import Network.Socket
  ( AddrInfo (..),
    NameInfoFlag (..),
    SockAddr (..),
    getAddrInfo,
    getNameInfo,
    hostAddress6ToTuple,
    hostAddressToTuple,
  )
import Text.Read (readMaybe)

-- | Split a peer address into host and port. Accepts @host@, @host:port@,
-- @[ipv6]@ and @[ipv6]:port@. Either part is 'Nothing' if missing.
toHostService :: String -> (Maybe String, Maybe String)
toHostService str =
  let host = case m6 of
        Just (x, _) -> Just x
        Nothing -> case takeWhile (/= ':') str of
          [] -> Nothing
          xs -> Just xs
      srv = case m6 of
        Just (_, y) -> s y
        Nothing -> s str
      s xs =
        case dropWhile (/= ':') xs of
          [] -> Nothing
          _ : ys -> Just ys
      m6 = case str of
        (x : xs)
          | x == '[' -> do
              i <- elemIndex ']' xs
              return $ second (drop 1) $ splitAt i xs
          | x == ':' -> do
              return (str, "")
        _ -> Nothing
   in (host, srv)

-- | Resolve a peer address given as in 'toHostService' into socket addresses,
-- using the default port of the network if none is given. Returns an empty list
-- if it cannot be resolved.
--
-- Only resolver failures, which are 'IOException's, are caught. Catching
-- every exception would also swallow the asynchronous exception that cancels
-- the thread doing the lookup, and the node could then never shut down.
toSockAddr :: Network -> String -> IO [SockAddr]
toSockAddr net str =
  resolve `catch` \(_ :: IOException) -> return []
  where
    resolve = map (.addrAddress) <$> getAddrInfo Nothing host srv
    (host, srv) =
      second (<|> Just (show net.defaultPort)) (toHostService str)

-- | Whether an address that a peer relayed is worth connecting to: a public
-- unicast address with a port. Loopback, private, link-local, shared,
-- documentation, multicast and reserved ranges are refused, so that a peer
-- cannot make the node connect to services on its own host or network.
-- Static peers in the configuration are not checked.
isRoutable :: SockAddr -> Bool
isRoutable (SockAddrInet port h) =
  port /= 0 && routable4 (hostAddressToTuple h)
isRoutable (SockAddrInet6 port _ h _) =
  port /= 0 && case hostAddress6ToTuple h of
    (0, 0, 0, 0, 0, 0xffff, hi, lo) ->
      routable4
        ( fromIntegral (hi `div` 256),
          fromIntegral (hi `mod` 256),
          fromIntegral (lo `div` 256),
          fromIntegral (lo `mod` 256)
        )
    (a, b, _, _, _, _, _, _)
      | a == 0 -> False -- unspecified, loopback, IPv4-compatible
      | a .&. 0xfe00 == 0xfc00 -> False -- unique local
      | a .&. 0xffc0 == 0xfe80 -> False -- link-local
      | a .&. 0xffc0 == 0xfec0 -> False -- site-local
      | a .&. 0xff00 == 0xff00 -> False -- multicast
      | a == 0x2001 && b == 0x0db8 -> False -- documentation
      | otherwise -> True
isRoutable _ = False

-- | Whether an IPv4 address is public unicast.
routable4 :: (Word8, Word8, Word8, Word8) -> Bool
routable4 (a, b, c, _)
  | a == 0 = False -- this network
  | a == 10 = False -- private
  | a == 100 && b .&. 0xc0 == 64 = False -- shared address space
  | a == 127 = False -- loopback
  | a == 169 && b == 254 = False -- link-local
  | a == 172 && b .&. 0xf0 == 16 = False -- private
  | a == 192 && b == 0 && c == 0 = False -- protocol assignments
  | a == 192 && b == 0 && c == 2 = False -- documentation
  | a == 192 && b == 168 = False -- private
  | a == 198 && b .&. 0xfe == 18 = False -- benchmarking
  | a == 198 && b == 51 && c == 100 = False -- documentation
  | a == 203 && b == 0 && c == 113 = False -- documentation
  | a >= 224 = False -- multicast and reserved
  | otherwise = True

-- | Open a TCP connection to a peer address and pass its conduits to a
-- function. The connection is closed when the function returns. Calls 'error'
-- if the address cannot be converted into a host and port.
withConnection :: SockAddr -> WithConnection
withConnection sa f =
  fromSockAddr sa >>= \case
    Nothing -> error $ "Invalid address " <> show sa
    Just settings ->
      runTCPClient settings $ \ad ->
        f Conduits {inboundConduit = appSource ad, outboundConduit = appSink ad}

-- | Turn a socket address into client settings for a TCP connection, or
-- 'Nothing' if it cannot be converted.
fromSockAddr :: SockAddr -> IO (Maybe ClientSettings)
fromSockAddr sa =
  resolve `catch` \(_ :: IOException) -> return Nothing
  where
    flags = [NI_NUMERICHOST, NI_NUMERICSERV]
    resolve = do
      (host, port) <- (getNameInfo flags True True sa)
      return $ clientSettings <$> (readMaybe =<< port) <*> (cs <$> host)