diff --git a/.changes/20260828_cardano_rpc_grpc_tcp_listener.yml b/.changes/20260828_cardano_rpc_grpc_tcp_listener.yml new file mode 100644 index 0000000000..f97d058ceb --- /dev/null +++ b/.changes/20260828_cardano_rpc_grpc_tcp_listener.yml @@ -0,0 +1,7 @@ +project: cardano-rpc +pr: 1322 +kind: + - feature + - breaking +description: | + The cardano-rpc gRPC server can now listen on HTTP/2 (h2c) or HTTP/2 over TLS on a configured IP address and port instead of only a unix domain socket, configured via new cardano-node options such as `--grpc-listen-port` and `--grpc-tls-certificate`. `RpcConfigF`'s `rpcSocketPath` field was replaced by the new `RpcEndpoint` sum type. diff --git a/cardano-rpc/cardano-rpc.cabal b/cardano-rpc/cardano-rpc.cabal index 354c8864d9..64cdf67930 100644 --- a/cardano-rpc/cardano-rpc.cabal +++ b/cardano-rpc/cardano-rpc.cabal @@ -120,9 +120,11 @@ library generic-data, grapesy, grpc-spec, + iproute, memory, mempack, microlens, + network, proto-lens >=0.7.1.7, proto-lens-protobuf-types, random, diff --git a/cardano-rpc/src/Cardano/Rpc/Server.hs b/cardano-rpc/src/Cardano/Rpc/Server.hs index 5d14019541..5fb5df2155 100644 --- a/cardano-rpc/src/Cardano/Rpc/Server.hs +++ b/cardano-rpc/src/Cardano/Rpc/Server.hs @@ -117,14 +117,40 @@ runRpcServer runRpcServer tracer rpcConfig networkMagic nodeKernelAccessRef = handleFatalExceptions $ do let RpcConfig { isEnabled = Identity isEnabled - , rpcSocketPath = Identity (File rpcSocketPathFp) + , rpcEndpoint = Identity rpcEndpoint , nodeSocketPath = Identity nodeSocketPath } = rpcConfig - config = - ServerConfig - { serverInsecure = Just $ InsecureUnix rpcSocketPathFp - , serverSecure = Nothing - } + config :: ServerConfig + config = case rpcEndpoint of + RpcEndpointUnixSocket (File socketPath) -> + ServerConfig + { serverInsecure = Just $ InsecureUnix socketPath + , serverSecure = Nothing + } + RpcEndpointHttp host port -> + ServerConfig + { serverInsecure = + Just + InsecureConfig + { insecureHost = Just $ show host + , insecurePort = port + } + , serverSecure = Nothing + } + RpcEndpointHttps host port (RpcTlsFiles certificateFile privateKeyFile chainCertificateFiles) -> + ServerConfig + { serverInsecure = Nothing + , serverSecure = + Just + SecureConfig + { secureHost = show host + , securePort = port + , securePubCert = unFile certificateFile + , secureChainCerts = unFile <$> chainCertificateFiles + , securePrivKey = unFile privateKeyFile + , secureSslKeyLog = def + } + } rpcEnv = RpcEnv { config = rpcConfig @@ -133,7 +159,8 @@ runRpcServer tracer rpcConfig networkMagic nodeKernelAccessRef = handleFatalExce , rpcNodeKernelAccess = nodeKernelAccessRef } - when isEnabled $ + when isEnabled $ do + traceWith tracer $ TraceRpcServerListening rpcEndpoint runRIO rpcEnv $ withRunInIO $ \runInIO -> runServerWithHandlers serverParams config . fmap (hoistSomeRpcHandler runInIO) $ diff --git a/cardano-rpc/src/Cardano/Rpc/Server/Config.hs b/cardano-rpc/src/Cardano/Rpc/Server/Config.hs index add0cf8a72..620c9cdbcc 100644 --- a/cardano-rpc/src/Cardano/Rpc/Server/Config.hs +++ b/cardano-rpc/src/Cardano/Rpc/Server/Config.hs @@ -2,6 +2,7 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE NoFieldSelectors #-} @@ -9,6 +10,11 @@ module Cardano.Rpc.Server.Config ( RpcConfig , PartialRpcConfig , RpcConfigF (..) + , RpcEndpoint (..) + , RpcTlsFiles (..) + , TlsCertificate + , TlsPrivateKey + , defaultRpcListenAddress , makeRpcConfig , nodeSocketPathToRpcSocketPath ) @@ -18,7 +24,9 @@ import Cardano.Api import RIO +import Data.IP (IP) import Data.Monoid +import Network.Socket (PortNumber) import System.FilePath (takeDirectory, ()) import Generic.Data (gmappend, gmempty) @@ -31,8 +39,8 @@ type RpcConfig = RpcConfigF Identity data RpcConfigF m = RpcConfig { isEnabled :: !(m Bool) -- ^ whether the RPC server is enabled - , rpcSocketPath :: !(m SocketPath) - -- ^ path to the socket file where the RPC server listens + , rpcEndpoint :: !(m RpcEndpoint) + -- ^ endpoint where the RPC server listens , nodeSocketPath :: !(m SocketPath) -- ^ cardano-node socket path. Only valid if RPC endpoint is enabled. } @@ -53,11 +61,49 @@ instance Semigroup (RpcConfigF Last) where instance Monoid (RpcConfigF Last) where mempty = gmempty +-- | Endpoint the RPC server listens on. Exactly one listener is active at a +-- time. +data RpcEndpoint + = RpcEndpointUnixSocket !SocketPath + | -- | IP address and port of the HTTP/2 without TLS (h2c) listener. + RpcEndpointHttp !IP !PortNumber + | -- | IP address, port and TLS credential files of the HTTP/2 over TLS + -- listener. + RpcEndpointHttps !IP !PortNumber !RpcTlsFiles + deriving (Eq, Show) + +instance Pretty RpcEndpoint where + pretty = \case + RpcEndpointUnixSocket (File socketPath) -> pretty socketPath + RpcEndpointHttp host port -> pshow host <> ":" <> pshow port + RpcEndpointHttps host port _ -> pshow host <> ":" <> pshow port <> " (TLS)" + +-- | TLS credential files for the RPC server, PEM format. +data RpcTlsFiles = RpcTlsFiles + { certificateFile :: !(File TlsCertificate In) + -- ^ server X.509 certificate + , privateKeyFile :: !(File TlsPrivateKey In) + -- ^ private key matching the certificate + , chainCertificateFiles :: ![File TlsCertificate In] + -- ^ intermediate chain certificates, if any + } + deriving (Eq, Show) + +-- | Empty content tag for 'File' identifying a TLS certificate file. +data TlsCertificate + +-- | Empty content tag for 'File' identifying a TLS private key file. +data TlsPrivateKey + +-- | Default IP address the HTTP/2 listener binds to when only a port is configured. +defaultRpcListenAddress :: IP +defaultRpcListenAddress = "127.0.0.1" + -- | Build RPC Config -- -- Uses the following defaults if the values are not provided -- * RPC is disabled --- * @rpc.sock@ is placed in the same path as the node socket +-- * the endpoint is a unix socket, @rpc.sock@, placed in the same path as the node socket -- -- Validates if the node socket is enabled if RPC is enabled. makeRpcConfig @@ -67,21 +113,22 @@ makeRpcConfig makeRpcConfig RpcConfig { isEnabled = Last mIsEnabled - , rpcSocketPath = Last mRpcSocketPath + , rpcEndpoint = Last mRpcEndpoint , nodeSocketPath = Last mNodeSocketPath } = do let isEnabled = fromMaybe False mIsEnabled - -- default to a some non-existing path. Does not matter if the gRPC endpoint is disabled + -- Default to a non-existing path. Irrelevant when the RPC server is disabled; when enabled, the validation below requires an explicit node socket path. nodeSocketPath = fromMaybe "./node.socket" mNodeSocketPath - rpcSocketPath = fromMaybe (nodeSocketPathToRpcSocketPath nodeSocketPath) mRpcSocketPath + rpcEndpoint = fromMaybe (RpcEndpointUnixSocket $ nodeSocketPathToRpcSocketPath nodeSocketPath) mRpcEndpoint when (isEnabled && isNothing mNodeSocketPath) $ throwError "Configuration error: gRPC endpoint was enabled but node socket file was not specified. Cannot run gRPC server without node socket." - pure $ + pure RpcConfig - (pure isEnabled) - (pure rpcSocketPath) - (pure nodeSocketPath) + { isEnabled = pure isEnabled + , rpcEndpoint = pure rpcEndpoint + , nodeSocketPath = pure nodeSocketPath + } -- | Convert node socket path to a default rpc socket path. -- By default it's @rpc.sock@ in the same directory as node socket path. diff --git a/cardano-rpc/src/Cardano/Rpc/Server/Internal/Tracing.hs b/cardano-rpc/src/Cardano/Rpc/Server/Internal/Tracing.hs index 1aae342493..94a554e370 100644 --- a/cardano-rpc/src/Cardano/Rpc/Server/Internal/Tracing.hs +++ b/cardano-rpc/src/Cardano/Rpc/Server/Internal/Tracing.hs @@ -12,6 +12,7 @@ import Cardano.Api.Pretty import Cardano.Api.Serialise.Cbor (DecoderError) import Cardano.Api.Serialise.Raw (SerialiseAsRawBytesError) import Cardano.Api.Serialise.SerialiseUsing +import Cardano.Rpc.Server.Config (RpcEndpoint) import Control.Exception import Data.Word (Word64) @@ -24,6 +25,7 @@ data TraceRpc | TraceRpcNodeKernelAccess TraceRpcNodeKernelAccess | TraceRpcError SomeException | TraceRpcFatalError SomeException + | TraceRpcServerListening !RpcEndpoint -- | Traces used in Query service data TraceRpcQuery @@ -45,6 +47,7 @@ instance Pretty TraceRpc where TraceRpcNodeKernelAccess t -> pretty t TraceRpcError e -> "Exception when processing RPC request:\n" <> prettyException e TraceRpcFatalError e -> "RPC server fatal error: " <> prettyException e + TraceRpcServerListening endpoint -> "RPC server starting on " <> pretty endpoint -- | Span type data TraceSpanEvent