mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-16 09:51:59 +03:00
fcef6e5cb2
## Description This PR is in reference to #2449 (support IP blacklisting for multitenant) *RFC Update: Add support for IPv6 blocking* ### Solution and Design Using [http-client-restricted](https://hackage.haskell.org/package/http-client-restricted) package, we're creating the HTTP manager with restricting capabilities. The IPs can be supplied from the CLI arguments as `--ipv4BlocklistCidrs cidr1, cidr2...` or `--disableDefaultIPv4Blocklist` for a default IP list. The new manager will block all requests to the provided CIDRs. We are extracting the error message string to show the end-user that given IP is blocked from being set as a webhook. There are 2 ways to extract the error message "connection to IP address is blocked". Given below are the responses from event trigger to a blocked IP for these implementations: - 6d74fde316f61e246c861befcca5059d33972fa7 - We return the error message string as a HTTPErr(HOther) from `Hasura/Eventing/HTTP.hs`. ``` { "data": { "message": "blocked connection to private IP address " }, "version": "2", "type": "client_error" } ``` - 88e17456345cbb449a5ecd4877c84c9f319dbc25 - We case match on HTTPExceptionContent for InternaException in `Hasura/HTTP.hs` and extract the error message string from it. (this is implemented as it handles all the cases where pro engine makes webhook requests) ``` { "data": { "message": { "type": "http_exception", "message": "blocked connection to private IP address ", "request": { "secure": false, "path": "/webhook", "responseTimeout": "ResponseTimeoutMicro 60000000", "queryString": "", "method": "POST", "requestHeaders": { "Content-Type": "application/json", "X-B3-ParentSpanId": "5ae6573edb2a6b36", "X-B3-TraceId": "29ea7bd6de6ebb8f", "X-B3-SpanId": "303137d9f1d4f341", "User-Agent": "hasura-graphql-engine/cerebushttp-ip-blacklist-a793a0e41-dirty" }, "host": "139.59.90.109", "port": 8000 } } }, "version": "2", "type": "client_error" } ``` ### Steps to test and verify The restricted IPs can be used as webhooks in event triggers, and hasura will return an error message in reponse. ### Limitations, known bugs & workarounds - The `http-client-restricted` has a needlessly complex interface, and puts effort into implementing proxy support which we don't want, so we've inlined a stripped down version. - Performance constraint: As the blocking is checked for each request, if a long list of blocked CIDRs is supplied, iterating through all of them is not what we would prefer. Using trie is suggested to overcome this. (Added to RFC) - Calls to Lux endpoints are inconsistent: We use either the http manager from the ProServeCtx which is unrestricted, or the http manager from the ServeCtx which is restricted (the latter through the instances for MonadMetadataApiAuthorization and UserAuthentication). (The failure scenario here would be: cloud sets PRO_ENDPOINT to something that resolves to an internal address, and then restricted requests to those endpoints fail, causing auth to fail on user requests. This is about HTTP requests to lux auth endpoints.) ## Changelog - ✅ `CHANGELOG.md` is updated with user-facing content relevant to this PR. ## Affected components - ✅ Server - ✅ Tests PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3186 Co-authored-by: Robert <132113+robx@users.noreply.github.com> GitOrigin-RevId: 5bd2de2d028bc416b02c99e996c7bebce56fb1e7
175 lines
6.2 KiB
Haskell
175 lines
6.2 KiB
Haskell
-- | Restricted `ManagerSettings` for <https://haskell-lang.org/library/http-client>
|
|
-- -
|
|
-- - Portions from http-client-tls Copyright (c) 2013 Michael Snoyman
|
|
-- - Portions from http-client-restricted Copyright 2018 Joey Hess <id@joeyh.name>
|
|
-- -
|
|
-- - License: MIT
|
|
module Network.HTTP.Client.Restricted
|
|
( Decision (..),
|
|
Restriction,
|
|
mkRestrictedManagerSettings,
|
|
ConnectionRestricted (..),
|
|
)
|
|
where
|
|
|
|
import Control.Exception
|
|
import Data.Default
|
|
import Data.Maybe
|
|
import Data.Typeable
|
|
import Network.BSD (getProtocolNumber)
|
|
import Network.Connection qualified as NC
|
|
import Network.HTTP.Client qualified as HTTP
|
|
import Network.HTTP.Client.Internal qualified as HTTP
|
|
import Network.HTTP.Client.TLS qualified as HTTP
|
|
import Network.Socket
|
|
import Prelude
|
|
|
|
data Decision = Allow | Deny
|
|
|
|
type Restriction = AddrInfo -> Decision
|
|
|
|
-- | Blocked requests raise this exception, wrapped as 'InternalException'.
|
|
data ConnectionRestricted = ConnectionRestricted
|
|
{ crHostName :: String,
|
|
crAddress :: AddrInfo
|
|
}
|
|
deriving (Show, Typeable)
|
|
|
|
instance Exception ConnectionRestricted
|
|
|
|
-- | Adjusts a ManagerSettings to enforce a Restriction. The restriction
|
|
-- will be checked each time a Request is made, and for each redirect
|
|
-- followed.
|
|
--
|
|
-- This overrides the `managerRawConnection`
|
|
-- and `managerTlsConnection` with its own implementations that check
|
|
-- the Restriction. They should otherwise behave the same as the
|
|
-- ones provided by http-client-tls.
|
|
--
|
|
-- This function is not exported, because using it with a ManagerSettings
|
|
-- produced by something other than http-client-tls would result in
|
|
-- surprising behavior, since its connection methods would not be used.
|
|
restrictManagerSettings ::
|
|
Maybe NC.ConnectionContext ->
|
|
Maybe NC.TLSSettings ->
|
|
Restriction ->
|
|
HTTP.ManagerSettings ->
|
|
HTTP.ManagerSettings
|
|
restrictManagerSettings mcontext mtls cfg base =
|
|
base
|
|
{ HTTP.managerRawConnection = restrictedRawConnection cfg,
|
|
HTTP.managerTlsConnection = restrictedTlsConnection mcontext mtls cfg,
|
|
HTTP.managerWrapException = wrapOurExceptions base
|
|
}
|
|
|
|
-- | Makes a TLS-capable ManagerSettings with a Restriction applied to it.
|
|
--
|
|
-- The Restriction will be checked each time a Request is made, and for
|
|
-- each redirect followed.
|
|
--
|
|
-- Aside from checking the Restriction, it should behave the same as
|
|
-- `Network.HTTP.Client.TLS.mkManagerSettingsContext`
|
|
-- from http-client-tls.
|
|
--
|
|
-- > main = do
|
|
-- > manager <- newManager $ mkRestrictedManagerSettings myRestriction Nothing Nothing
|
|
-- > request <- parseRequest "http://httpbin.org/get"
|
|
-- > response <- httpLbs request manager
|
|
-- > print $ responseBody response
|
|
--
|
|
-- See `mkManagerSettingsContext` for why
|
|
-- it can be useful to provide a `NC.ConnectionContext`.
|
|
--
|
|
-- Note that SOCKS is not supported.
|
|
mkRestrictedManagerSettings ::
|
|
Restriction ->
|
|
Maybe NC.ConnectionContext ->
|
|
Maybe NC.TLSSettings ->
|
|
HTTP.ManagerSettings
|
|
mkRestrictedManagerSettings cfg mcontext mtls =
|
|
restrictManagerSettings mcontext mtls cfg $
|
|
HTTP.mkManagerSettingsContext mcontext (fromMaybe def mtls) Nothing
|
|
|
|
wrapOurExceptions :: HTTP.ManagerSettings -> HTTP.Request -> IO a -> IO a
|
|
wrapOurExceptions base req a =
|
|
let wrapper se
|
|
| Just (_ :: ConnectionRestricted) <- fromException se =
|
|
toException $
|
|
HTTP.HttpExceptionRequest req $
|
|
HTTP.InternalException se
|
|
| otherwise = se
|
|
in HTTP.managerWrapException base req (handle (throwIO . wrapper) a)
|
|
|
|
restrictedRawConnection :: Restriction -> IO (Maybe HostAddress -> String -> Int -> IO HTTP.Connection)
|
|
restrictedRawConnection cfg = getConnection cfg Nothing Nothing
|
|
|
|
restrictedTlsConnection :: Maybe NC.ConnectionContext -> Maybe NC.TLSSettings -> Restriction -> IO (Maybe HostAddress -> String -> Int -> IO HTTP.Connection)
|
|
restrictedTlsConnection mcontext mtls cfg =
|
|
getConnection cfg (Just (fromMaybe def mtls)) mcontext
|
|
|
|
-- Based on Network.HTTP.Client.TLS.getTlsConnection.
|
|
--
|
|
-- Checks the Restriction
|
|
--
|
|
-- Does not support SOCKS.
|
|
getConnection ::
|
|
Restriction ->
|
|
Maybe NC.TLSSettings ->
|
|
Maybe NC.ConnectionContext ->
|
|
IO (Maybe HostAddress -> String -> Int -> IO HTTP.Connection)
|
|
getConnection restriction tls mcontext = do
|
|
context <- maybe NC.initConnectionContext return mcontext
|
|
return $ \_hostAddress hostName port ->
|
|
bracketOnError
|
|
(go context hostName port)
|
|
NC.connectionClose
|
|
convertConnection
|
|
where
|
|
go context hostName port = do
|
|
let connparams =
|
|
NC.ConnectionParams
|
|
{ NC.connectionHostname = host,
|
|
NC.connectionPort = fromIntegral port,
|
|
NC.connectionUseSecure = tls,
|
|
NC.connectionUseSocks = Nothing -- unsupprted
|
|
}
|
|
proto <- getProtocolNumber "tcp"
|
|
let serv = show port
|
|
let hints =
|
|
defaultHints
|
|
{ addrFlags = [AI_ADDRCONFIG],
|
|
addrProtocol = proto,
|
|
addrSocketType = Stream
|
|
}
|
|
addrs <- getAddrInfo (Just hints) (Just host) (Just serv)
|
|
bracketOnError
|
|
(firstSuccessful $ map tryToConnect addrs)
|
|
close
|
|
(\sock -> NC.connectFromSocket context sock connparams)
|
|
where
|
|
host = HTTP.strippedHostName hostName
|
|
tryToConnect addr = case restriction addr of
|
|
Allow ->
|
|
bracketOnError
|
|
(socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))
|
|
close
|
|
(\sock -> connect sock (addrAddress addr) >> return sock)
|
|
Deny -> throwIO $ ConnectionRestricted host addr
|
|
firstSuccessful [] = throwIO $ NC.HostNotResolved host
|
|
firstSuccessful (a : as) =
|
|
a `catch` \(e :: IOException) ->
|
|
case as of
|
|
[] -> throwIO e
|
|
_ -> firstSuccessful as
|
|
|
|
-- Copied from Network.HTTP.Client.TLS, unfortunately not exported.
|
|
convertConnection :: NC.Connection -> IO HTTP.Connection
|
|
convertConnection conn =
|
|
HTTP.makeConnection
|
|
(NC.connectionGetChunk conn)
|
|
(NC.connectionPut conn)
|
|
-- Closing an SSL connection gracefully involves writing/reading
|
|
-- on the socket. But when this is called the socket might be
|
|
-- already closed, and we get a @ResourceVanished@.
|
|
(NC.connectionClose conn `Control.Exception.catch` \(_ :: IOException) -> return ())
|