packages feed

wireguard-hs-0.1.0: System/Network/Wireguard.hsc

module System.Network.Wireguard where

import           Control.Exception (throw, bracket, mask)
import           Control.Monad (unless)
import           Control.Monad.IO.Class (MonadIO(..))

import           Crypto.Error (CryptoFailable(..))
import           Crypto.PubKey.Curve25519 (generateSecretKey, toPublic, secretKey)

#ifdef WITH_AESON
import           Control.Applicative ((<|>))
import           Data.Aeson (ToJSON(..), FromJSON(..))
import qualified Data.Text.Encoding as TE
import qualified Data.ByteString.Base64 as B64
#endif
import           Data.Bits
import           Data.Coerce (coerce)
import qualified Data.ByteArray as BA
import           Data.Maybe (fromMaybe)
import           Data.String (fromString)
import qualified Data.Text as T
import           Data.Time (UTCTime)
import           Data.Proxy (Proxy(..))
import qualified Data.Vector.Storable as V
import qualified Data.Vector.Storable.Sized as VS
import           Data.WideWord.Word128 (Word128(..))
import           Data.Word

#ifdef WITH_BEAM
import           Database.Beam
import           Database.Beam.Backend.SQL
import qualified Data.ByteString as BS
#endif

import           GHC.Generics (Generic)

import           Foreign.C.Error
import           Foreign.C.String
import           Foreign.C.Types
import           Foreign.ForeignPtr (withForeignPtr)
import           Foreign.Marshal.Alloc (malloc, mallocBytes, free, alloca)
import           Foreign.Ptr
import           Foreign.Storable

import qualified Net.IP as IP
import           Net.IPv4 (IPv4(..), IPv4Range(..))
import           Net.IPv6 (IPv6(..), IPv6Range(..))
import qualified Net.IPv6 as IPv6
import           Network.Socket (SockAddr(..))
import           Network.Socket.Address (SocketAddress(..))

import           System.Clock (TimeSpec(..))
import           System.IO.Unsafe (unsafePerformIO)

#include "wireguard.h"
#include "string.h"

foreign import ccall unsafe "strncpy" c_strncpy :: Ptr CChar -> Ptr CChar -> CInt -> IO (Ptr CChar)

data IPRange
    = IPRange4 !IPv4Range
    | IPRange6 !IPv6Range
      deriving (Show, Eq, Ord)

#ifdef WITH_AESON
instance ToJSON IPRange where
    toJSON (IPRange4 x) = toJSON x
    toJSON (IPRange6 x) = toJSON x

instance FromJSON IPRange where
    parseJSON x = fmap IPRange4 (parseJSON x) <|> fmap IPRange6 (parseJSON x)
#endif

-- | Linux network interface status
data LinkStatus
  = LinkStatusUp | LinkStatusDown
    deriving (Show, Eq, Ord)

linkStatus2Text :: LinkStatus -> T.Text
linkStatus2Text LinkStatusUp = "up"
linkStatus2Text LinkStatusDown = "down"

text2LinkStatus :: T.Text -> Maybe LinkStatus
text2LinkStatus "up" = pure LinkStatusUp
text2LinkStatus "down" = pure LinkStatusDown
text2LinkStatus _ = Nothing

#ifdef WITH_AESON
instance ToJSON LinkStatus where
  toJSON = toJSON . linkStatus2Text

instance FromJSON LinkStatus where
  parseJSON p = maybe (fail "Expected 'up' or 'down' for link status")
                      pure =<<
                fmap text2LinkStatus (parseJSON p)
#endif

#ifdef WITH_BEAM
instance HasSqlValueSyntax syntax T.Text => HasSqlValueSyntax syntax LinkStatus where
    sqlValueSyntax = sqlValueSyntax . linkStatus2Text

instance (FromBackendRow be T.Text, BeamSqlBackend be) => FromBackendRow be LinkStatus where
    fromBackendRow = do
      txt <- fromBackendRow
      case text2LinkStatus txt of
        Nothing -> fail "Expected up, down for link status"
        Just ls -> pure ls
#endif

-- | Type-level representation of wireguard key
data KeyType = PublicKey | PrivateKey

class KnownKeyType (ty :: KeyType) where
  keyTypeVal :: Proxy ty -> KeyType
instance KnownKeyType 'PublicKey where
  keyTypeVal _ = PublicKey
instance KnownKeyType 'PrivateKey where
  keyTypeVal _ = PrivateKey

newtype WgKey (ty :: KeyType) = WgKey (VS.Vector 32 Word8)
  deriving newtype (Storable, Eq, Ord)

#ifdef WITH_BEAM
instance HasSqlEqualityCheck be BS.ByteString => HasSqlEqualityCheck be (WgKey ty)
#endif

wgEmptyKey :: WgKey a
wgEmptyKey = WgKey (VS.replicate 0)

-- | Convert any 'WgKey' into a public one. This is unsafe, and only used for testing
unsafeMkPublic :: WgKey a -> WgKey 'PublicKey
unsafeMkPublic (WgKey x) = WgKey x

instance KnownKeyType ty => Show (WgKey ty) where
  show (WgKey x) = case keyTypeVal (Proxy @ty) of
                     PrivateKey -> "<private key>"
                     PublicKey  -> "WgKey (" ++ show x ++ ")"

instance BA.ByteArrayAccess (WgKey ty) where
  length (WgKey v) = VS.length v -- Always 32, but updates if it changes?
  withByteArray (WgKey v) withPtr =
   let (fPtr, _) = V.unsafeToForeignPtr0 (VS.fromSized v)
   in withForeignPtr fPtr (withPtr . castPtr)

#ifdef WITH_AESON
instance KnownKeyType ty => ToJSON (WgKey ty) where
  toJSON (WgKey key) =
    case keyTypeVal (Proxy @ty) of
      PrivateKey -> toJSON (id @T.Text "(redacted)")
      PublicKey  -> toJSON . B64.encodeBase64 . BS.pack . VS.toList $ key

instance FromJSON (WgKey ty) where
  parseJSON x = do
    b64Encoded <- parseJSON x
    let eRes = B64.decodeBase64 (TE.encodeUtf8 b64Encoded)
    case eRes of
      Left  err -> fail ("Could not decode key: " ++ T.unpack err)
      Right key ->
        case VS.toSized (V.fromList (BS.unpack key)) of
          Nothing -> fail ("key is not the right size")
          Just keySized -> pure (WgKey keySized)
#endif

#ifdef WITH_BEAM
instance FromBackendRow be BS.ByteString => FromBackendRow be (WgKey ty) where
   fromBackendRow = do
      bs <- fromBackendRow
      case VS.toSized (V.fromList (BS.unpack bs)) of
        Nothing -> fail "key is not the right size"
        Just keySized -> pure (WgKey keySized)

instance HasSqlValueSyntax syntax BS.ByteString => HasSqlValueSyntax syntax (WgKey ty) where
   sqlValueSyntax (WgKey key) = sqlValueSyntax (BS.pack . V.toList . VS.fromSized $ key)
#endif

newtype WgDeviceFlags = WgDeviceFlags Word32
  deriving (Show, Eq, Ord)
  deriving newtype (Storable, Bits)

instance Semigroup WgDeviceFlags where
    (<>) = coerce ((.|.) :: Word32 -> Word32 -> Word32)

instance Monoid WgDeviceFlags where
    mempty = WgDeviceFlags 0

wgDeviceReplacePeers, wgDeviceHasPrivateKey, wgDeviceHasPublicKey,
  wgDeviceHasListenPort, wgDeviceHasFwMark :: WgDeviceFlags
wgDeviceReplacePeers = WgDeviceFlags (#const WGDEVICE_REPLACE_PEERS)
wgDeviceHasPrivateKey = WgDeviceFlags (#const WGDEVICE_HAS_PRIVATE_KEY)
wgDeviceHasPublicKey = WgDeviceFlags (#const WGDEVICE_HAS_PUBLIC_KEY)
wgDeviceHasListenPort = WgDeviceFlags (#const WGDEVICE_HAS_LISTEN_PORT)
wgDeviceHasFwMark = WgDeviceFlags (#const WGDEVICE_HAS_FWMARK)

data WgDevice
  = WgDevice
  { wgDeviceName    :: T.Text
  , wgDeviceIndex   :: Word32
  , wgDeviceFlags   :: WgDeviceFlags
  , wgDevicePubKey  :: WgKey 'PublicKey
  , wgDevicePrivKey :: WgKey 'PrivateKey
  , wgDeviceFwMark  :: Word32
  , wgDeviceListenPort :: Word16
  , wgDevicePeers   :: [ WgPeer ]
  } deriving Show

foreign import ccall unsafe "ntohl" c_ntohl :: Word32 -> Word32
foreign import ccall unsafe "htonl" c_htonl :: Word32 -> Word32

instance Storable WgDevice where
  sizeOf _ = (#size wg_device)
  alignment _ = alignment (undefined :: Word32)

  peek p = do
    nm <- fromString <$> peekCString ((#ptr wg_device, name) p)
    ix <- (#peek wg_device, ifindex) p
    flags <- (#peek wg_device, flags) p
    pubkey <- (#peek wg_device, public_key) p
    privkey <- (#peek wg_device, private_key) p
    fwmark <- (#peek wg_device, fwmark) p
    listenport <- (#peek wg_device, listen_port) p
    peerPtr <- (#peek wg_device, first_peer) p
    peers <- readWgPeers peerPtr
    pure WgDevice { wgDeviceName = nm
                  , wgDeviceIndex = ix
                  , wgDeviceFlags = flags
                  , wgDevicePubKey = pubkey
                  , wgDevicePrivKey = privkey
                  , wgDeviceFwMark = fwmark
                  , wgDeviceListenPort = listenport
                  , wgDevicePeers = peers }

  poke p d = do
    let ifName = wgDeviceName d
        ifNameStr = T.unpack ifName

    _ <- withCString ifNameStr $ \ifnamePtr ->
         c_strncpy ((#ptr wg_device, name) p) ifnamePtr (#const IFNAMSIZ)

    (#poke wg_device, ifindex) p (wgDeviceIndex d)
    (#poke wg_device, flags)   p (wgDeviceFlags d)
    (#poke wg_device, public_key) p (wgDevicePubKey d)
    (#poke wg_device, private_key) p (wgDevicePrivKey d)
    (#poke wg_device, fwmark) p (wgDeviceFwMark d)
    (#poke wg_device, listen_port) p (wgDeviceListenPort d)

    (firstPeer, lastPeer) <- writeWgPeers (wgDevicePeers d)
    (#poke wg_device, first_peer) p firstPeer
    (#poke wg_device, last_peer)  p lastPeer

newtype WgPeerFlags = WgPeerFlags Word32
  deriving (Show, Eq, Ord)
  deriving newtype (Storable, Bits)
#ifdef WITH_AESON
  deriving newtype (ToJSON, FromJSON)
#endif

instance Semigroup WgPeerFlags where
    (<>) = coerce ((.|.) :: Word32 -> Word32 -> Word32)

instance Monoid WgPeerFlags where
    mempty = WgPeerFlags 0

wgPeerRemoveMe, wgPeerReplaceAllowedIps, wgPeerHasPublicKey,
  wgPeerHasPresharedKey, wgPeerHasPersistentKeepaliveInterval :: WgPeerFlags
wgPeerRemoveMe = WgPeerFlags (#const WGPEER_REMOVE_ME)
wgPeerReplaceAllowedIps = WgPeerFlags (#const WGPEER_REPLACE_ALLOWEDIPS)
wgPeerHasPublicKey = WgPeerFlags (#const WGPEER_HAS_PUBLIC_KEY)
wgPeerHasPresharedKey = WgPeerFlags (#const WGPEER_HAS_PRESHARED_KEY)
wgPeerHasPersistentKeepaliveInterval = WgPeerFlags (#const WGPEER_HAS_PERSISTENT_KEEPALIVE_INTERVAL)

newtype WgSockAddr = WgSockAddr { unWgSockAddr :: SockAddr }
    deriving Show

#ifdef WITH_AESON
instance ToJSON WgSockAddr where
    toJSON (WgSockAddr a) = toJSON (show a)

instance FromJSON WgSockAddr where
    parseJSON = error "FromJSON{WgSockAddr}"
#endif

data WgPeer
  = WgPeer
  { wgPeerFlags :: WgPeerFlags
  , wgPeerKey   :: WgKey 'PublicKey
  , wgPeerPsk   :: WgKey 'PublicKey

  , wgPeerEndpoint  :: WgSockAddr

  , wgPeerLastHandshakeTime :: TimeSpec
  , wgPeerRxBytes, wgPeerTxBytes :: Word64
  , wgPeerPersistentKeepaliveInterval :: Word16

  , wgPeerAllowedIPs :: [ IPRange ]
  } deriving (Show, Generic)

-- | Template for adding a new peer
wgNewPeer :: WgPeer
wgNewPeer = WgPeer
          { wgPeerFlags = mempty
          , wgPeerKey   = wgEmptyKey
          , wgPeerPsk   = wgEmptyKey
          , wgPeerEndpoint = WgSockAddr (SockAddrInet 0 0)
          , wgPeerLastHandshakeTime = 0
          , wgPeerRxBytes = 0
          , wgPeerTxBytes = 0
          , wgPeerPersistentKeepaliveInterval = 0
          , wgPeerAllowedIPs = []
          }

readWgPeers :: Ptr WgPeer -> IO [ WgPeer ]
readWgPeers = go pure
  where
    go a peerPtr
      | peerPtr == nullPtr = a []
      | otherwise = do
         peer <- peek peerPtr
         nextPeer <- (#peek wg_peer, next_peer) peerPtr
         go (a . (peer:)) nextPeer

writeWgPeers :: [WgPeer] -> IO (Ptr WgPeer, Ptr WgPeer)
writeWgPeers = go Nothing Nothing
  where
    go !firstPeer !lastPeer peers =
      case peers of
        [] -> pure ( fromMaybe nullPtr firstPeer
                   , fromMaybe nullPtr lastPeer)

        (x:xs) -> do

          peerPtr <- malloc
          poke peerPtr x -- Store current peer
          (#poke wg_peer, next_peer) peerPtr nullPtr

          -- Update previous next pointer
          case lastPeer of
            Nothing -> pure ()
            Just lastPeerPtr ->
              (#poke wg_peer, next_peer) lastPeerPtr peerPtr

          go (Just (fromMaybe peerPtr firstPeer)) (Just peerPtr) xs

readAllowedIps :: Ptr IPRange -> IO [ IPRange ]
readAllowedIps = go pure
  where
    go a ipPtr
      | ipPtr == nullPtr = a []
      | otherwise = do
          family :: Word16 <- (#peek wg_allowedip, family) ipPtr
          addr <-
            case family of
              (#const AF_INET) -> do
                 IPv4 ip4Network <- (#peek wg_allowedip, ip4) ipPtr
                 pure (IP.fromIPv4 $ IPv4 (c_ntohl ip4Network))
              (#const AF_INET6) -> do
                let bytes = (#ptr wg_allowedip, ip6) ipPtr
                [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15] <-
                  mapM (peekElemOff bytes) [0..15]
                pure (IP.fromIPv6 (IPv6.fromOctets a0 a1 a2 a3
                                                   a4 a5 a6 a7
                                                   a8 a9 a10 a11
                                                   a12 a13 a14 a15))
              _ -> fail "Bad IP in WgPeer ip range"
          cidr :: Word8 <- (#peek wg_allowedip, cidr) ipPtr

          nextPtr <- (#peek wg_allowedip, next_allowedip) ipPtr

          let ipr = IP.case_ (\addr -> IPRange4 $ IPv4Range addr cidr)
                             (\addr -> IPRange6 $ IPv6Range addr cidr)
                             addr

          go (a . (ipr :)) nextPtr

bytes64 :: Word64 -> ( Word8, Word8, Word8, Word8
                     , Word8, Word8, Word8, Word8 )
bytes64 a = ( f 56, f 48, f 40, f 32
            , f 24, f 16, f 8, f 0 )
  where
    f l = fromIntegral ((a `shiftR` l) .&. 0xFF)

writeAllowedIps :: [ IPRange ] -> IO (Ptr IPRange, Ptr IPRange)
writeAllowedIps = go Nothing Nothing
  where
    continue !firstIp !lastIp !ipPtr xs = do
      -- Now set the next allowed ip to null
      (#poke wg_allowedip, next_allowedip) ipPtr nullPtr

      case lastIp of
        Nothing -> pure ()
        Just lastIpPtr ->
          (#poke wg_allowedip, next_allowedip) lastIpPtr ipPtr

      go (Just (fromMaybe ipPtr firstIp)) (Just ipPtr) xs

    go !firstIp !lastIp peers =
      case peers of
        [] -> pure ( fromMaybe nullPtr firstIp
                   , fromMaybe nullPtr lastIp )

        (IPRange4 (IPv4Range (IPv4 ip4Network) cidr):xs) -> do
          ipPtr <- mallocBytes (#size wg_allowedip)
          (#poke wg_allowedip, family) ipPtr ((#const AF_INET) :: Word16)

          -- Write ip4 and convert to network byte order
          (#poke wg_allowedip, ip4) ipPtr ip4Network
          networkHost <- (#peek wg_allowedip, ip4) ipPtr
          (#poke wg_allowedip, ip4) ipPtr (c_htonl networkHost)

          (#poke wg_allowedip, cidr) ipPtr (cidr :: Word8)

          continue firstIp lastIp ipPtr xs

        (IPRange6 (IPv6Range (IPv6 (Word128 ipa ipb)) cidr):xs) -> do
          ipPtr <- mallocBytes (#size wg_allowedip)
          (#poke wg_allowedip, family) ipPtr ((#const AF_INET6) :: Word16)

          let (a0, a1, a2, a3, a4, a5, a6, a7) = bytes64 ipa
              (a8, a9, a10, a11, a12, a13, a14, a15) = bytes64 ipb
              bytes = (#ptr wg_allowedip, ip6) ipPtr
          mapM_ (uncurry (pokeElemOff bytes)) (zip [0..] [ a0, a1, a2, a3, a4, a5, a6, a7, a8
                                                         , a9, a10, a11, a12, a13, a14, a15 ])

          (#poke wg_allowedip, cidr) ipPtr (cidr :: Word8)
          continue firstIp lastIp ipPtr xs

instance Storable WgPeer where
  sizeOf _ = (#size wg_peer)
  alignment _ = alignment (undefined :: Word32)

  peek p = do
    flags <- (#peek wg_peer, flags) p
    key   <- (#peek wg_peer, public_key) p
    psk   <- (#peek wg_peer, preshared_key) p
    ep    <- peekSocketAddress ((#ptr wg_peer, endpoint) p)
    lasthstm <- (#peek wg_peer, last_handshake_time) p
    rx    <- (#peek wg_peer, rx_bytes) p
    tx    <- (#peek wg_peer, tx_bytes) p
    pkai  <- (#peek wg_peer, persistent_keepalive_interval) p
    ipsPtr <- (#peek wg_peer, first_allowedip) p
    ips   <- readAllowedIps ipsPtr
    pure WgPeer { wgPeerFlags = flags
                , wgPeerKey   = key
                , wgPeerPsk   = psk
                , wgPeerEndpoint = WgSockAddr ep
                , wgPeerLastHandshakeTime = lasthstm
                , wgPeerRxBytes = rx
                , wgPeerTxBytes = tx
                , wgPeerPersistentKeepaliveInterval = pkai
                , wgPeerAllowedIPs = ips }

  poke p peer = do
    (#poke wg_peer, flags) p (wgPeerFlags peer)
    (#poke wg_peer, public_key) p (wgPeerKey peer)
    (#poke wg_peer, preshared_key) p (wgPeerPsk peer)
    pokeSocketAddress ((#ptr wg_peer, endpoint) p) (unWgSockAddr (wgPeerEndpoint peer))

    -- The following three fields are not strictly necesary...
    (#poke wg_peer, last_handshake_time) p (wgPeerLastHandshakeTime peer)
    (#poke wg_peer, rx_bytes) p (wgPeerRxBytes peer)
    (#poke wg_peer, tx_bytes) p (wgPeerTxBytes peer)

    (#poke wg_peer, persistent_keepalive_interval) p (wgPeerPersistentKeepaliveInterval peer)

    (firstAllowedIp, lastAllowedIp) <- writeAllowedIps (wgPeerAllowedIPs peer)
    (#poke wg_peer, first_allowedip) p firstAllowedIp
    (#poke wg_peer, last_allowedip) p lastAllowedIp

readStringList :: ([String] -> [String]) -> CString -> IO [String]
readStringList a ptr = do
  s <- peekCString ptr
  if null s then pure (a [])
     else do
       readStringList (a . (s:)) (ptr `plusPtr` (length s + 1))

foreign import ccall unsafe "wg_list_device_names" c_wg_list_device_names :: IO CString

wgListDeviceNames :: MonadIO m => m [T.Text]
wgListDeviceNames = liftIO $ do
  devs <- c_wg_list_device_names
  res <- fmap (map fromString) (readStringList id devs)
  free devs

  errno <- getErrno
  if errno == eOK
     then pure res
     else throwErrno "wgListDeviceNames"

foreign import ccall unsafe "wg_get_device" c_wg_get_device :: Ptr (Ptr WgDevice) -> CString -> IO CInt
foreign import ccall unsafe "wg_free_device" c_wg_free_device :: Ptr WgDevice -> IO ()

wgGetDevice :: MonadIO m => T.Text -> m WgDevice
wgGetDevice iface =
  liftIO $
  alloca $ \wgPtrPtr ->
  withCString (T.unpack iface) $ \ifaceStr -> do
    _ <- c_wg_get_device wgPtrPtr ifaceStr
    errno <- getErrno
    if errno == eOK
       then do
         wgPtr <- peek wgPtrPtr
         wg <- peek wgPtr
         c_wg_free_device wgPtr
         pure wg
       else throwErrno "wgGetDevice"

foreign import ccall unsafe "wg_add_device" c_wg_add_device :: Ptr CChar -> IO CInt

wgAddDevice :: MonadIO m => T.Text -> m ()
wgAddDevice iface =
  liftIO $
  withCString (T.unpack iface) $ \ifaceStr -> do
    _ <- c_wg_add_device ifaceStr
    errno <- getErrno
    unless (errno == eOK) (throwErrno "wgAddDevice")

foreign import ccall unsafe "wg_del_device" c_wg_del_device :: Ptr CChar -> IO CInt

wgDelDevice :: MonadIO m => T.Text -> m ()
wgDelDevice iface =
  liftIO $
  withCString (T.unpack iface) $ \ifaceStr -> do
    _ <- c_wg_del_device ifaceStr
    errno <- getErrno
    unless (errno == eOK) (throwErrno "wgDelDevice")

foreign import ccall unsafe "wg_set_device" c_wg_set_device :: Ptr WgDevice -> IO CInt

wgSetDevice :: MonadIO m => WgDevice -> m ()
wgSetDevice dev =
  liftIO $
  bracket (mask $ \_ -> do -- This should not cause any exception...
             wgPtr <- malloc
             poke wgPtr dev
             pure wgPtr)

          c_wg_free_device $ \devPtr -> do
    _ <- c_wg_set_device devPtr
    errno <- getErrno
    unless (errno == eOK) (throwErrno "wgSetDevice");

-- Link status

foreign import ccall unsafe "wg_get_link_status" c_wg_get_link_status :: Ptr CChar -> Ptr CInt -> IO CInt

data LinkFlags
  = LinkFlags
  { lfStatus :: LinkStatus
  } deriving Show

wgGetLinkStatus :: MonadIO m => T.Text -> m LinkFlags
wgGetLinkStatus ifaceNm =
  liftIO $
  withCString (T.unpack ifaceNm) $ \ifaceStr ->
  alloca $ \flagsV -> do
    res <- c_wg_get_link_status ifaceStr flagsV
    if res == 0
      then do
        flags <- peek flagsV
        pure LinkFlags
         { lfStatus = if (flags .&. (#const IFF_UP)) > 0
                      then LinkStatusUp
                      else LinkStatusDown
         }
      else throwErrno "wgGetLinkStatus"

-- | Generate a wireguard private key
wgGeneratePrivateKey :: MonadIO m => m (WgKey 'PrivateKey)
wgGeneratePrivateKey = liftIO $ do
  key <- generateSecretKey
  BA.withByteArray key peek

-- | Derive public key from private one
wgGetPublicKey :: WgKey 'PrivateKey -> WgKey 'PublicKey
wgGetPublicKey key =
  case secretKey key of
    CryptoFailed err -> throw err
    CryptoPassed privKey ->
      unsafePerformIO $ BA.withByteArray (toPublic privKey) peek