From 05ae3d067da0de6d2daaaa1a7bb6a9dbba70a361 Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Fri, 28 Aug 2026 14:47:34 +0200 Subject: [PATCH 1/2] Add TCP listening support to the gRPC server Replace the rpcSocketPath field of RpcConfigF with an RpcEndpoint sum type: the server listens either on a unix domain socket (default, rpc.sock next to the node socket) or on plaintext TCP (HTTP/2 without TLS) when a listen port is configured. The TCP listen address defaults to 127.0.0.1. Trace the resolved endpoint on server start. --- ...20260828_cardano_rpc_grpc_tcp_listener.yml | 7 ++++ cardano-rpc/cardano-rpc.cabal | 1 + cardano-rpc/src/Cardano/Rpc/Server.hs | 16 +++++++-- cardano-rpc/src/Cardano/Rpc/Server/Config.hs | 36 ++++++++++++++----- .../Cardano/Rpc/Server/Internal/Tracing.hs | 9 ++++- 5 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 .changes/20260828_cardano_rpc_grpc_tcp_listener.yml 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..79a2189bd9 --- /dev/null +++ b/.changes/20260828_cardano_rpc_grpc_tcp_listener.yml @@ -0,0 +1,7 @@ +project: cardano-rpc +pr: 0 +kind: + - feature + - breaking +description: | + The cardano-rpc gRPC server can now listen on a plain TCP endpoint (HTTP/2 without TLS) instead of a unix domain socket, configured via the node configuration keys `RpcListenAddress`/`RpcListenPort` or the `--grpc-listen-address`/`--grpc-listen-port` CLI flags; the listen address defaults to `127.0.0.1`, and configuring both a socket path and a listen port is rejected at configuration parsing time. As part of this, `RpcConfigF`'s `rpcSocketPath` field was replaced by `rpcEndpoint`, a new `RpcEndpoint` sum type with `RpcEndpointUnixSocket` and `RpcEndpointTcp` constructors, and `TraceRpc` gained a new `TraceRpcServerListening` constructor. diff --git a/cardano-rpc/cardano-rpc.cabal b/cardano-rpc/cardano-rpc.cabal index 354c8864d9..fcd250ae3f 100644 --- a/cardano-rpc/cardano-rpc.cabal +++ b/cardano-rpc/cardano-rpc.cabal @@ -123,6 +123,7 @@ library 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..ec36a19458 100644 --- a/cardano-rpc/src/Cardano/Rpc/Server.hs +++ b/cardano-rpc/src/Cardano/Rpc/Server.hs @@ -46,6 +46,7 @@ import Cardano.Rpc.Server.NodeKernelAccess import RIO import Control.Tracer +import Data.Text qualified as Text import Network.GRPC.Common import Network.GRPC.Server import Network.GRPC.Server.Protobuf @@ -117,12 +118,20 @@ runRpcServer runRpcServer tracer rpcConfig networkMagic nodeKernelAccessRef = handleFatalExceptions $ do let RpcConfig { isEnabled = Identity isEnabled - , rpcSocketPath = Identity (File rpcSocketPathFp) + , rpcEndpoint = Identity rpcEndpoint , nodeSocketPath = Identity nodeSocketPath } = rpcConfig + insecureConfig :: InsecureConfig + insecureConfig = case rpcEndpoint of + RpcEndpointUnixSocket (File socketPath) -> InsecureUnix socketPath + RpcEndpointTcp host port -> + InsecureConfig + { insecureHost = Just $ Text.unpack host + , insecurePort = port + } config = ServerConfig - { serverInsecure = Just $ InsecureUnix rpcSocketPathFp + { serverInsecure = Just insecureConfig , serverSecure = Nothing } rpcEnv = @@ -133,7 +142,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..99e62b279a 100644 --- a/cardano-rpc/src/Cardano/Rpc/Server/Config.hs +++ b/cardano-rpc/src/Cardano/Rpc/Server/Config.hs @@ -9,6 +9,8 @@ module Cardano.Rpc.Server.Config ( RpcConfig , PartialRpcConfig , RpcConfigF (..) + , RpcEndpoint (..) + , defaultRpcListenAddress , makeRpcConfig , nodeSocketPathToRpcSocketPath ) @@ -19,6 +21,7 @@ import Cardano.Api import RIO import Data.Monoid +import Network.Socket (PortNumber) import System.FilePath (takeDirectory, ()) import Generic.Data (gmappend, gmempty) @@ -27,12 +30,26 @@ type PartialRpcConfig = RpcConfigF Last type RpcConfig = RpcConfigF Identity +-- | Endpoint the RPC server listens on. Exactly one listener is active at a +-- time. Future transports (for example TLS) are added as new constructors. +data RpcEndpoint + = RpcEndpointUnixSocket !SocketPath + | -- | host and port of the TCP listener, HTTP/2 without TLS. The host is + -- always concrete: config parsers apply 'defaultRpcListenAddress' when + -- only a port was provided. Port 0 makes the operating system choose. + RpcEndpointTcp !Text !PortNumber + deriving (Eq, Show) + +-- | Default host the TCP listener binds to when only a port is configured. +defaultRpcListenAddress :: Text +defaultRpcListenAddress = "127.0.0.1" + -- | RPC server configuration, which is a part of cardano-node configuration. 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. } @@ -57,7 +74,7 @@ instance Monoid (RpcConfigF Last) where -- -- 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 +84,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 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..423e41382b 100644 --- a/cardano-rpc/src/Cardano/Rpc/Server/Internal/Tracing.hs +++ b/cardano-rpc/src/Cardano/Rpc/Server/Internal/Tracing.hs @@ -4,7 +4,7 @@ -- | Provides datatypes used in tracing module Cardano.Rpc.Server.Internal.Tracing where -import Cardano.Api (SlotNo) +import Cardano.Api (File (..), SlotNo) import Cardano.Api.Consensus (TxValidationErrorInCardanoMode) import Cardano.Api.Era (Inject (..)) import Cardano.Api.Error @@ -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,8 @@ data TraceRpc | TraceRpcNodeKernelAccess TraceRpcNodeKernelAccess | TraceRpcError SomeException | TraceRpcFatalError SomeException + | -- | Emitted just before the server starts listening on the endpoint. + TraceRpcServerListening !RpcEndpoint -- | Traces used in Query service data TraceRpcQuery @@ -45,6 +48,10 @@ 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 (RpcEndpointUnixSocket (File socketPath)) -> + "RPC server starting, listening on unix socket " <> pretty socketPath + TraceRpcServerListening (RpcEndpointTcp host port) -> + "RPC server starting, listening on " <> pretty host <> ":" <> pshow port -- | Span type data TraceSpanEvent From 999ef2044b58d7962c76939203621f9809106851 Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Fri, 28 Aug 2026 15:10:03 +0200 Subject: [PATCH 2/2] Add TLS listening support to the gRPC server Add an RpcEndpointTcpTls endpoint: when TLS certificate and private key files are configured, the server listens with TLS on the configured host and port. Grapesy's default of honouring the SSLKEYLOGFILE environment variable is explicitly disabled so the node never silently logs TLS session keys. --- ...20260828_cardano_rpc_grpc_tcp_listener.yml | 4 +- cardano-rpc/cardano-rpc.cabal | 1 + cardano-rpc/src/Cardano/Rpc/Server.hs | 43 ++++++++++---- cardano-rpc/src/Cardano/Rpc/Server/Config.hs | 59 ++++++++++++++----- .../Cardano/Rpc/Server/Internal/Tracing.hs | 12 ++-- 5 files changed, 81 insertions(+), 38 deletions(-) diff --git a/.changes/20260828_cardano_rpc_grpc_tcp_listener.yml b/.changes/20260828_cardano_rpc_grpc_tcp_listener.yml index 79a2189bd9..f97d058ceb 100644 --- a/.changes/20260828_cardano_rpc_grpc_tcp_listener.yml +++ b/.changes/20260828_cardano_rpc_grpc_tcp_listener.yml @@ -1,7 +1,7 @@ project: cardano-rpc -pr: 0 +pr: 1322 kind: - feature - breaking description: | - The cardano-rpc gRPC server can now listen on a plain TCP endpoint (HTTP/2 without TLS) instead of a unix domain socket, configured via the node configuration keys `RpcListenAddress`/`RpcListenPort` or the `--grpc-listen-address`/`--grpc-listen-port` CLI flags; the listen address defaults to `127.0.0.1`, and configuring both a socket path and a listen port is rejected at configuration parsing time. As part of this, `RpcConfigF`'s `rpcSocketPath` field was replaced by `rpcEndpoint`, a new `RpcEndpoint` sum type with `RpcEndpointUnixSocket` and `RpcEndpointTcp` constructors, and `TraceRpc` gained a new `TraceRpcServerListening` constructor. + 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 fcd250ae3f..64cdf67930 100644 --- a/cardano-rpc/cardano-rpc.cabal +++ b/cardano-rpc/cardano-rpc.cabal @@ -120,6 +120,7 @@ library generic-data, grapesy, grpc-spec, + iproute, memory, mempack, microlens, diff --git a/cardano-rpc/src/Cardano/Rpc/Server.hs b/cardano-rpc/src/Cardano/Rpc/Server.hs index ec36a19458..5fb5df2155 100644 --- a/cardano-rpc/src/Cardano/Rpc/Server.hs +++ b/cardano-rpc/src/Cardano/Rpc/Server.hs @@ -46,7 +46,6 @@ import Cardano.Rpc.Server.NodeKernelAccess import RIO import Control.Tracer -import Data.Text qualified as Text import Network.GRPC.Common import Network.GRPC.Server import Network.GRPC.Server.Protobuf @@ -121,19 +120,37 @@ runRpcServer tracer rpcConfig networkMagic nodeKernelAccessRef = handleFatalExce , rpcEndpoint = Identity rpcEndpoint , nodeSocketPath = Identity nodeSocketPath } = rpcConfig - insecureConfig :: InsecureConfig - insecureConfig = case rpcEndpoint of - RpcEndpointUnixSocket (File socketPath) -> InsecureUnix socketPath - RpcEndpointTcp host port -> - InsecureConfig - { insecureHost = Just $ Text.unpack host - , insecurePort = port + 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 + } } - config = - ServerConfig - { serverInsecure = Just insecureConfig - , serverSecure = Nothing - } rpcEnv = RpcEnv { config = rpcConfig diff --git a/cardano-rpc/src/Cardano/Rpc/Server/Config.hs b/cardano-rpc/src/Cardano/Rpc/Server/Config.hs index 99e62b279a..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 #-} @@ -10,6 +11,9 @@ module Cardano.Rpc.Server.Config , PartialRpcConfig , RpcConfigF (..) , RpcEndpoint (..) + , RpcTlsFiles (..) + , TlsCertificate + , TlsPrivateKey , defaultRpcListenAddress , makeRpcConfig , nodeSocketPathToRpcSocketPath @@ -20,6 +24,7 @@ import Cardano.Api import RIO +import Data.IP (IP) import Data.Monoid import Network.Socket (PortNumber) import System.FilePath (takeDirectory, ()) @@ -30,20 +35,6 @@ type PartialRpcConfig = RpcConfigF Last type RpcConfig = RpcConfigF Identity --- | Endpoint the RPC server listens on. Exactly one listener is active at a --- time. Future transports (for example TLS) are added as new constructors. -data RpcEndpoint - = RpcEndpointUnixSocket !SocketPath - | -- | host and port of the TCP listener, HTTP/2 without TLS. The host is - -- always concrete: config parsers apply 'defaultRpcListenAddress' when - -- only a port was provided. Port 0 makes the operating system choose. - RpcEndpointTcp !Text !PortNumber - deriving (Eq, Show) - --- | Default host the TCP listener binds to when only a port is configured. -defaultRpcListenAddress :: Text -defaultRpcListenAddress = "127.0.0.1" - -- | RPC server configuration, which is a part of cardano-node configuration. data RpcConfigF m = RpcConfig { isEnabled :: !(m Bool) @@ -70,6 +61,44 @@ 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 @@ -88,7 +117,7 @@ makeRpcConfig , 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 rpcEndpoint = fromMaybe (RpcEndpointUnixSocket $ nodeSocketPathToRpcSocketPath nodeSocketPath) mRpcEndpoint when (isEnabled && isNothing mNodeSocketPath) $ diff --git a/cardano-rpc/src/Cardano/Rpc/Server/Internal/Tracing.hs b/cardano-rpc/src/Cardano/Rpc/Server/Internal/Tracing.hs index 423e41382b..94a554e370 100644 --- a/cardano-rpc/src/Cardano/Rpc/Server/Internal/Tracing.hs +++ b/cardano-rpc/src/Cardano/Rpc/Server/Internal/Tracing.hs @@ -4,7 +4,7 @@ -- | Provides datatypes used in tracing module Cardano.Rpc.Server.Internal.Tracing where -import Cardano.Api (File (..), SlotNo) +import Cardano.Api (SlotNo) import Cardano.Api.Consensus (TxValidationErrorInCardanoMode) import Cardano.Api.Era (Inject (..)) import Cardano.Api.Error @@ -12,7 +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 Cardano.Rpc.Server.Config (RpcEndpoint) import Control.Exception import Data.Word (Word64) @@ -25,8 +25,7 @@ data TraceRpc | TraceRpcNodeKernelAccess TraceRpcNodeKernelAccess | TraceRpcError SomeException | TraceRpcFatalError SomeException - | -- | Emitted just before the server starts listening on the endpoint. - TraceRpcServerListening !RpcEndpoint + | TraceRpcServerListening !RpcEndpoint -- | Traces used in Query service data TraceRpcQuery @@ -48,10 +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 (RpcEndpointUnixSocket (File socketPath)) -> - "RPC server starting, listening on unix socket " <> pretty socketPath - TraceRpcServerListening (RpcEndpointTcp host port) -> - "RPC server starting, listening on " <> pretty host <> ":" <> pshow port + TraceRpcServerListening endpoint -> "RPC server starting on " <> pretty endpoint -- | Span type data TraceSpanEvent