From 7ed446c694b6175588bd9ceccf08cc9a17d85cbe Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Fri, 28 Aug 2026 14:47:42 +0200 Subject: [PATCH 1/2] Add gRPC TCP endpoint configuration Parse the new RpcEndpoint sum type from node configuration (RpcListenAddress/RpcListenPort) and CLI (--grpc-listen-address/ --grpc-listen-port). A configured listen port makes the gRPC server listen on plaintext TCP instead of the unix socket; configuring both a socket path and a listen port is rejected at configuration parse time. The listen address defaults to 127.0.0.1. --- .../src/Cardano/Node/Configuration/POM.hs | 23 +++++++--- cardano-node/src/Cardano/Node/Parsers.hs | 46 +++++++++++++++---- .../src/Cardano/Node/Tracing/Tracers/Rpc.hs | 15 ++++++ cardano-node/src/Cardano/Node/Types.hs | 15 +++--- cardano-node/test/Test/Cardano/Node/POM.hs | 10 ++-- 5 files changed, 84 insertions(+), 25 deletions(-) diff --git a/cardano-node/src/Cardano/Node/Configuration/POM.hs b/cardano-node/src/Cardano/Node/Configuration/POM.hs index 3f60f118247..0c2583a8230 100644 --- a/cardano-node/src/Cardano/Node/Configuration/POM.hs +++ b/cardano-node/src/Cardano/Node/Configuration/POM.hs @@ -39,7 +39,7 @@ import Cardano.Node.Handlers.Shutdown import Cardano.Node.Protocol.Types (Protocol (..)) import Cardano.Node.Types import Cardano.Rpc.Server.Config (PartialRpcConfig, RpcConfig, RpcConfigF (..), - makeRpcConfig) + RpcEndpoint (..), defaultRpcListenAddress, makeRpcConfig) import Ouroboros.Consensus.Ledger.SupportsMempool import Ouroboros.Consensus.Mempool (MempoolCapacityBytesOverride (..)) import Ouroboros.Consensus.Node (NodeDatabasePaths (..)) @@ -68,6 +68,7 @@ import Data.Monoid (Last (..)) import Data.Text (Text) import qualified Data.Text as Text import Data.Time.Clock (DiffTime, secondsToDiffTime) +import Data.Word (Word16) import Data.Yaml (decodeFileThrow) import GHC.Generics (Generic) import Options.Applicative @@ -412,11 +413,21 @@ instance FromJSON PartialNodeConfiguration where <$> v .:? "ResponderCoreAffinityPolicy" <*> v .:? "ForkPolicy" -- deprecated - pncRpcConfig <- - RpcConfig - <$> (Last <$> v .:? "EnableRpc") - <*> (Last <$> v .:? "RpcSocketPath") - <*> pure mempty + pncRpcConfig <- do + enableRpc <- Last <$> v .:? "EnableRpc" + mSocketPath <- v .:? "RpcSocketPath" + mListenAddress <- v .:? "RpcListenAddress" + mListenPort :: Maybe Word16 <- v .:? "RpcListenPort" + rpcEndpoint <- case (mSocketPath, mListenAddress, mListenPort) of + (Just _, Just _, _) -> fail "RpcSocketPath and RpcListenAddress are mutually exclusive" + (Just _, _, Just _) -> fail "RpcSocketPath and RpcListenPort are mutually exclusive" + (Nothing, Just _, Nothing) -> fail "RpcListenAddress requires RpcListenPort to be set" + (Just socketPath, Nothing, Nothing) -> pure . Just $ RpcEndpointUnixSocket socketPath + (Nothing, _, Just listenPort) -> + pure . Just $ + RpcEndpointTcp (fromMaybe defaultRpcListenAddress mListenAddress) (fromIntegral listenPort) + (Nothing, Nothing, Nothing) -> pure Nothing + pure (mempty :: PartialRpcConfig){isEnabled = enableRpc, rpcEndpoint = Last rpcEndpoint} txSubmissionLogicVersion <- Last <$> v .:? "TxSubmissionLogicVersion" let parseInitDelay = diff --git a/cardano-node/src/Cardano/Node/Parsers.hs b/cardano-node/src/Cardano/Node/Parsers.hs index 0b42c77dc62..bbdce5fc385 100644 --- a/cardano-node/src/Cardano/Node/Parsers.hs +++ b/cardano-node/src/Cardano/Node/Parsers.hs @@ -23,7 +23,8 @@ import Cardano.Node.Configuration.Socket import Cardano.Node.Handlers.Shutdown import Cardano.Node.Types import Cardano.Prelude (ConvertText (..)) -import Cardano.Rpc.Server.Config (PartialRpcConfig, RpcConfigF (..)) +import Cardano.Rpc.Server.Config (PartialRpcConfig, RpcConfigF (..), RpcEndpoint (..), + defaultRpcListenAddress) import Ouroboros.Consensus.Ledger.SupportsMempool import Ouroboros.Consensus.Node @@ -438,8 +439,8 @@ parseStartAsNonProducingNode = parseRpcConfig :: Parser PartialRpcConfig parseRpcConfig = do isEnabled <- lastOption parseRpcToggle - socketPath <- lastOption parseRpcSocketPath - pure $ RpcConfig isEnabled socketPath mempty + rpcEndpoint <- lastOption $ parseRpcUnixSocketEndpoint <|> parseRpcTcpEndpoint + pure (mempty :: PartialRpcConfig){isEnabled, rpcEndpoint} where parseRpcToggle :: Parser Bool parseRpcToggle = @@ -447,11 +448,40 @@ parseRpcConfig = do [ long "grpc-enable" , help "[EXPERIMENTAL] Enable node gRPC endpoint." ] - parseRpcSocketPath :: Parser SocketPath - parseRpcSocketPath = - parseSocketPath - "grpc-socket-path" - "[EXPERIMENTAL] gRPC socket path. Defaults to rpc.sock in the same directory as node socket." + + parseRpcUnixSocketEndpoint :: Parser RpcEndpoint + parseRpcUnixSocketEndpoint = + RpcEndpointUnixSocket <$> + parseSocketPath + "grpc-socket-path" + "[EXPERIMENTAL] gRPC unix socket path. Defaults to rpc.sock in the same directory as the node socket. Mutually exclusive with --grpc-listen-port." + + parseRpcTcpEndpoint :: Parser RpcEndpoint + parseRpcTcpEndpoint = + RpcEndpointTcp . fromMaybe defaultRpcListenAddress + <$> Opt.optional parseRpcListenAddress + <*> Opt.option readPortNumber (mconcat + [ long "grpc-listen-port" + , metavar "PORT" + , help "[EXPERIMENTAL] TCP port the gRPC server listens on. When set, the gRPC server listens on plaintext TCP (HTTP/2 without TLS) instead of a unix socket. Mutually exclusive with --grpc-socket-path." + ]) + + parseRpcListenAddress :: Parser Text + parseRpcListenAddress = + strOption $ mconcat + [ long "grpc-listen-address" + , metavar "HOST" + , help "[EXPERIMENTAL] Host address the gRPC server binds to. Requires --grpc-listen-port. Defaults to 127.0.0.1." + ] + +-- | Read a TCP port number, rejecting values outside the 0 - 65535 range. +readPortNumber :: Opt.ReadM PortNumber +readPortNumber = Opt.eitherReader $ \raw -> + case readMaybe raw :: Maybe Integer of + Just port + | 0 <= port && port <= 65535 -> Right $ fromIntegral port + | otherwise -> Left $ "Port number out of range (0 - 65535): " <> show port + Nothing -> Left $ "Not a valid port number: " <> raw -- | Produce just the brief help header for a given CLI option parser, -- without the options. diff --git a/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs b/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs index d89854d8003..b25f2a8c5dc 100644 --- a/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs +++ b/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs @@ -9,13 +9,16 @@ module Cardano.Node.Tracing.Tracers.Rpc () where +import Cardano.Api (File (..)) import Cardano.Api.Pretty import Cardano.Logging hiding (nsInner) import Cardano.Rpc.Server (TraceRpc (..), TraceRpcNodeKernelAccess (..), TraceRpcQuery (..), TraceRpcSubmit (..), TraceRpcSync (..), TraceSpanEvent (..)) +import Cardano.Rpc.Server.Config (RpcEndpoint (..)) import Data.Aeson (Object, Value (..), (.=)) +import qualified Data.Text as Text instance LogFormatting TraceRpc where forMachine _dtal tr = @@ -64,6 +67,10 @@ instance LogFormatting TraceRpc where ["kind" .= String "NodeKernelAccess"] <> case nodeKernelAccessTrace of TraceRpcUnsupportedBlockType blockType -> ["blockType" .= String blockType] + TraceRpcServerListening endpoint -> + [ "kind" .= String "ServerListening" + , "endpoint" .= endpointToText endpoint + ] forHuman = docToText . pretty @@ -114,6 +121,7 @@ instance MetaTrace TraceRpc where "NodeKernelAccess" : case nodeKernelAccessTrace of TraceRpcUnsupportedBlockType _ -> ["UnsupportedBlockType"] + TraceRpcServerListening _ -> ["ServerListening"] severityFor (Namespace _ nsInner) _ = case nsInner of ["FatalError"] -> Just Error -- RPC server startup errors @@ -134,6 +142,7 @@ instance MetaTrace TraceRpc where ["SyncService", "ReadTip", "Span"] -> Just Debug ["SyncService", "FollowTip", "Span"] -> Just Debug ["NodeKernelAccess", "UnsupportedBlockType"] -> Just Warning + ["ServerListening"] -> Just Notice -- one-off startup event, must be visible with default config _ -> Nothing documentFor (Namespace _ nsInner) = case nsInner of @@ -157,6 +166,7 @@ instance MetaTrace TraceRpc where ["SyncService", "ReadTip", "Span"] -> Just "Span for the ReadTip SyncService method." ["SyncService", "FollowTip", "Span"] -> Just "Span for the FollowTip SyncService method." ["NodeKernelAccess", "UnsupportedBlockType"] -> Just "The block type is not supported by the RPC server." + ["ServerListening"] -> Just "RPC server is starting to listen on the configured endpoint." _ -> Nothing metricsDocFor (Namespace _ nsInner) = case nsInner of @@ -200,6 +210,7 @@ instance MetaTrace TraceRpc where , ["SyncService", "FollowTip", "Span"] , ["QueryService", "ReadGenesis", "Span"] , ["NodeKernelAccess", "UnsupportedBlockType"] + , ["ServerListening"] ] -- helper functions @@ -209,3 +220,7 @@ spanToObject = mconcat . \case SpanBegin spanId -> ["span" .= String "begin", "spanId" .= spanId] SpanEnd spanId -> ["span" .= String "end", "spanId" .= spanId] + +endpointToText :: RpcEndpoint -> Text +endpointToText (RpcEndpointUnixSocket (File socketPath)) = Text.pack socketPath +endpointToText (RpcEndpointTcp host port) = host <> ":" <> Text.pack (show port) diff --git a/cardano-node/src/Cardano/Node/Types.hs b/cardano-node/src/Cardano/Node/Types.hs index 9f9cad5cd57..593707cd8ca 100644 --- a/cardano-node/src/Cardano/Node/Types.hs +++ b/cardano-node/src/Cardano/Node/Types.hs @@ -49,7 +49,7 @@ import Cardano.Network.ConsensusMode (ConsensusMode (..)) import Cardano.Network.NodeToNode (DiffusionMode (..)) import Cardano.Node.Configuration.Socket (SocketConfig (..)) import Cardano.Node.Orphans () -import Cardano.Rpc.Server.Config (RpcConfigF (..)) +import Cardano.Rpc.Server.Config (RpcConfigF (..), RpcEndpoint (..)) import Control.Exception import Data.Aeson @@ -509,11 +509,14 @@ instance AdjustFilePaths (File a b) where adjustFilePaths f (File p) = File $ f p instance Functor f => AdjustFilePaths (RpcConfigF f) where - adjustFilePaths f (RpcConfig isEnabled rpcSocketPath nodeSocketPath) = - RpcConfig - isEnabled - (adjustFilePaths f <$> rpcSocketPath) - (adjustFilePaths f <$> nodeSocketPath) + adjustFilePaths f rpcConfig@RpcConfig{rpcEndpoint, nodeSocketPath} = + rpcConfig + { rpcEndpoint = adjustEndpoint <$> rpcEndpoint + , nodeSocketPath = adjustFilePaths f <$> nodeSocketPath + } + where + adjustEndpoint (RpcEndpointUnixSocket socketPath) = RpcEndpointUnixSocket $ adjustFilePaths f socketPath + adjustEndpoint endpoint@RpcEndpointTcp{} = endpoint data VRFPrivateKeyFilePermissionError = OtherPermissionsExist FilePath diff --git a/cardano-node/test/Test/Cardano/Node/POM.hs b/cardano-node/test/Test/Cardano/Node/POM.hs index c3dd9914416..b6abb4e455b 100644 --- a/cardano-node/test/Test/Cardano/Node/POM.hs +++ b/cardano-node/test/Test/Cardano/Node/POM.hs @@ -21,7 +21,7 @@ import Cardano.Node.Configuration.POM import Cardano.Node.Configuration.Socket import Cardano.Node.Handlers.Shutdown import Cardano.Node.Types -import Cardano.Rpc.Server.Config (RpcConfigF (..), makeRpcConfig) +import Cardano.Rpc.Server.Config (PartialRpcConfig, RpcConfigF (..), makeRpcConfig) import Ouroboros.Consensus.Node (NodeDatabasePaths (..)) import Ouroboros.Consensus.Node.Genesis (disableGenesisConfig) import Ouroboros.Consensus.Storage.LedgerDB.Args @@ -436,7 +436,7 @@ prop_rpcReload_cliEnabledYamlSilent :: Property prop_rpcReload_cliEnabledYamlSilent = H.propertyOnce $ do let cliConfig = testPartialCliConfig - { pncRpcConfig = RpcConfig (Last (Just True)) mempty mempty + { pncRpcConfig = (mempty :: PartialRpcConfig){isEnabled = Last (Just True)} , pncSocketConfig = testSocketConfigWithPath } merged = defaultPartialNodeConfiguration <> testPartialYamlConfig <> cliConfig @@ -447,7 +447,7 @@ prop_rpcReload_cliEnabledYamlSilent = prop_rpcReload_cliSilentYamlEnabled :: Property prop_rpcReload_cliSilentYamlEnabled = H.propertyOnce $ do - let yamlConfig = testPartialYamlConfig{pncRpcConfig = RpcConfig (Last (Just True)) mempty mempty} + let yamlConfig = testPartialYamlConfig{pncRpcConfig = (mempty :: PartialRpcConfig){isEnabled = Last (Just True)}} cliConfig = testPartialCliConfig{pncSocketConfig = testSocketConfigWithPath} merged = defaultPartialNodeConfiguration <> yamlConfig <> cliConfig NodeConfiguration{ncRpcConfig = RpcConfig{isEnabled}} <- evalEither $ makeNodeConfiguration merged @@ -465,9 +465,9 @@ prop_rpcReload_bothSilent = prop_rpcReload_cliOverridesYaml :: Property prop_rpcReload_cliOverridesYaml = H.propertyOnce $ do - let yamlConfig = testPartialYamlConfig{pncRpcConfig = RpcConfig (Last (Just False)) mempty mempty} + let yamlConfig = testPartialYamlConfig{pncRpcConfig = (mempty :: PartialRpcConfig){isEnabled = Last (Just False)}} cliConfig = testPartialCliConfig - { pncRpcConfig = RpcConfig (Last (Just True)) mempty mempty + { pncRpcConfig = (mempty :: PartialRpcConfig){isEnabled = Last (Just True)} , pncSocketConfig = testSocketConfigWithPath } merged = defaultPartialNodeConfiguration <> yamlConfig <> cliConfig From 299f4a9eb1a499a7d99b1b8ec90c2d83d381fce4 Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Fri, 28 Aug 2026 15:10:06 +0200 Subject: [PATCH 2/2] Add gRPC TLS endpoint configuration Add RpcTlsCertificateFile/RpcTlsPrivateKeyFile/RpcTlsChainCertificateFiles configuration keys and --grpc-tls-certificate/--grpc-tls-private-key/ --grpc-tls-chain-certificate CLI flags. Certificate and private key must be configured together and require a TCP listen port; violations are rejected at configuration parse time. --- .../src/Cardano/Node/Configuration/POM.hs | 59 +++++++--- cardano-node/src/Cardano/Node/Parsers.hs | 111 ++++++++++++++---- .../src/Cardano/Node/Tracing/Tracers/Rpc.hs | 10 +- cardano-node/src/Cardano/Node/Types.hs | 13 +- 4 files changed, 143 insertions(+), 50 deletions(-) diff --git a/cardano-node/src/Cardano/Node/Configuration/POM.hs b/cardano-node/src/Cardano/Node/Configuration/POM.hs index 0c2583a8230..3b6de2bea52 100644 --- a/cardano-node/src/Cardano/Node/Configuration/POM.hs +++ b/cardano-node/src/Cardano/Node/Configuration/POM.hs @@ -34,12 +34,13 @@ import Cardano.Network.ConsensusMode (ConsensusMode (..), defaultConse import qualified Cardano.Network.Diffusion.Configuration as Cardano import Cardano.Network.PeerSelection (NumberOfBigLedgerPeers (..)) import Cardano.Node.Configuration.LedgerDB +import Cardano.Node.Configuration.NodeAddress (NodeHostIPAddress (..)) import Cardano.Node.Configuration.Socket (SocketConfig (..)) import Cardano.Node.Handlers.Shutdown import Cardano.Node.Protocol.Types (Protocol (..)) import Cardano.Node.Types import Cardano.Rpc.Server.Config (PartialRpcConfig, RpcConfig, RpcConfigF (..), - RpcEndpoint (..), defaultRpcListenAddress, makeRpcConfig) + RpcEndpoint (..), RpcTlsFiles (..), defaultRpcListenAddress, makeRpcConfig) import Ouroboros.Consensus.Ledger.SupportsMempool import Ouroboros.Consensus.Mempool (MempoolCapacityBytesOverride (..)) import Ouroboros.Consensus.Node (NodeDatabasePaths (..)) @@ -413,21 +414,7 @@ instance FromJSON PartialNodeConfiguration where <$> v .:? "ResponderCoreAffinityPolicy" <*> v .:? "ForkPolicy" -- deprecated - pncRpcConfig <- do - enableRpc <- Last <$> v .:? "EnableRpc" - mSocketPath <- v .:? "RpcSocketPath" - mListenAddress <- v .:? "RpcListenAddress" - mListenPort :: Maybe Word16 <- v .:? "RpcListenPort" - rpcEndpoint <- case (mSocketPath, mListenAddress, mListenPort) of - (Just _, Just _, _) -> fail "RpcSocketPath and RpcListenAddress are mutually exclusive" - (Just _, _, Just _) -> fail "RpcSocketPath and RpcListenPort are mutually exclusive" - (Nothing, Just _, Nothing) -> fail "RpcListenAddress requires RpcListenPort to be set" - (Just socketPath, Nothing, Nothing) -> pure . Just $ RpcEndpointUnixSocket socketPath - (Nothing, _, Just listenPort) -> - pure . Just $ - RpcEndpointTcp (fromMaybe defaultRpcListenAddress mListenAddress) (fromIntegral listenPort) - (Nothing, Nothing, Nothing) -> pure Nothing - pure (mempty :: PartialRpcConfig){isEnabled = enableRpc, rpcEndpoint = Last rpcEndpoint} + pncRpcConfig <- parsePartialRpcConfig v txSubmissionLogicVersion <- Last <$> v .:? "TxSubmissionLogicVersion" let parseInitDelay = @@ -733,6 +720,46 @@ instance FromJSON PartialNodeConfiguration where , npcCheckpointsFileHash } + parsePartialRpcConfig v = do + enableRpc <- Last <$> v .:? "EnableRpc" + mSocketPath <- v .:? "RpcSocketPath" + mListenAddress <- fmap unNodeHostIPAddress <$> v .:? "RpcListenAddress" + mListenPort :: Maybe Word16 <- v .:? "RpcListenPort" + mTlsFiles <- parseRpcTlsFiles v + rpcEndpoint <- case (mSocketPath, mListenAddress, mListenPort, mTlsFiles) of + (Just _, Just _, _, _) -> fail "RpcSocketPath and RpcListenAddress are mutually exclusive" + (Just _, _, Just _, _) -> fail "RpcSocketPath and RpcListenPort are mutually exclusive" + (Just _, _, _, Just _) -> fail "RpcSocketPath and TLS configuration are mutually exclusive" + (Just socketPath, Nothing, Nothing, Nothing) -> pure . Just $ RpcEndpointUnixSocket socketPath + (Nothing, Just _, Nothing, _) -> fail "RpcListenAddress requires RpcListenPort to be set" + (Nothing, _, Nothing, Just _) -> fail "TLS configuration requires RpcListenPort to be set" + (Nothing, _, Just listenPort, Just tlsFiles) -> + pure . Just $ + RpcEndpointHttps (fromMaybe defaultRpcListenAddress mListenAddress) (fromIntegral listenPort) tlsFiles + (Nothing, _, Just listenPort, Nothing) -> + pure . Just $ + RpcEndpointHttp (fromMaybe defaultRpcListenAddress mListenAddress) (fromIntegral listenPort) + (Nothing, Nothing, Nothing, Nothing) -> pure Nothing + pure (mempty :: PartialRpcConfig){isEnabled = enableRpc, rpcEndpoint = Last rpcEndpoint} + + parseRpcTlsFiles v = do + mCertificateFile <- v .:? "RpcTlsCertificateFile" + mPrivateKeyFile <- v .:? "RpcTlsPrivateKeyFile" + mChainCertificateFiles <- v .:? "RpcTlsChainCertificateFiles" + case (mCertificateFile, mPrivateKeyFile) of + (Just certificateFile, Just privateKeyFile) -> + pure . Just $ + RpcTlsFiles + { certificateFile + , privateKeyFile + , chainCertificateFiles = fromMaybe [] mChainCertificateFiles + } + (Nothing, Nothing) + | Just _ <- mChainCertificateFiles -> + fail "RpcTlsChainCertificateFiles requires RpcTlsCertificateFile and RpcTlsPrivateKeyFile to be set" + | otherwise -> pure Nothing + _ -> fail "RpcTlsCertificateFile and RpcTlsPrivateKeyFile must be set together" + -- | Default configuration is mainnet defaultPartialNodeConfiguration :: PartialNodeConfiguration defaultPartialNodeConfiguration = diff --git a/cardano-node/src/Cardano/Node/Parsers.hs b/cardano-node/src/Cardano/Node/Parsers.hs index bbdce5fc385..9a4aebfdd07 100644 --- a/cardano-node/src/Cardano/Node/Parsers.hs +++ b/cardano-node/src/Cardano/Node/Parsers.hs @@ -1,4 +1,5 @@ {-# LANGUAGE ApplicativeDo #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TupleSections #-} @@ -13,9 +14,11 @@ module Cardano.Node.Parsers , parseHostPort ) where +import Cardano.Api (FileDirection (In)) + import Cardano.Logging.Types import qualified Cardano.Logging.Types as Net -import Cardano.Node.Configuration.NodeAddress (File (..), +import Cardano.Node.Configuration.NodeAddress (File (..), NodeHostIPAddress (..), NodeHostIPv4Address (NodeHostIPv4Address), NodeHostIPv6Address (NodeHostIPv6Address), PortNumber, SocketPath) import Cardano.Node.Configuration.POM (PartialNodeConfiguration (..), lastOption) @@ -24,17 +27,18 @@ import Cardano.Node.Handlers.Shutdown import Cardano.Node.Types import Cardano.Prelude (ConvertText (..)) import Cardano.Rpc.Server.Config (PartialRpcConfig, RpcConfigF (..), RpcEndpoint (..), - defaultRpcListenAddress) + RpcTlsFiles (..), TlsCertificate, TlsPrivateKey, defaultRpcListenAddress) import Ouroboros.Consensus.Ledger.SupportsMempool import Ouroboros.Consensus.Node import Data.Char (isDigit) import Data.Foldable +import Data.IP (IP) import Data.Maybe (fromMaybe) import Data.Monoid (Last (..)) import Data.Text (Text) import qualified Data.Text as Text -import Data.Word (Word16, Word32) +import Data.Word (Word32) import Options.Applicative hiding (str, switch) import qualified Options.Applicative as Opt import qualified Options.Applicative.Help as OptI @@ -172,10 +176,10 @@ parseHostPort str = if | null hostRev -> Left "parseHostPort: Empty host." | null portRev -> Left "parseHostPort: Empty port." - | all isDigit portRev - , Just port <- readMaybe @Word16 (reverse portRev) -> if - | 0 <= port, port <= 65535 -> Right (Net.RemoteSocket (Text.pack (reverse hostRev)) port) - | otherwise -> Left ("parseHostPort: Numeric port '" ++ show port ++ "' out of range: 0 - 65535)") + | all isDigit portRev -> + case parsePortNumber (reverse portRev) of + Right port -> Right (Net.RemoteSocket (Text.pack (reverse hostRev)) (fromIntegral port)) + Left err -> Left ("parseHostPort: " ++ err) | otherwise -> Left "parseHostPort: Non-numeric port." | otherwise = Left "parseHostPort: No colon found." @@ -241,9 +245,18 @@ parseNodeHostIPv6Address str = (Right . NodeHostIPv6Address) (readMaybe str) +-- | Parse either an IPv4 or an IPv6 address, unlike 'parseNodeHostIPv4Address' +-- and 'parseNodeHostIPv6Address' which each accept only one address family. +parseNodeHostIPAddress :: String -> Either String NodeHostIPAddress +parseNodeHostIPAddress str = + maybe + (Left $ "Failed to parse IP address: " ++ str) + (Right . NodeHostIPAddress) + (readMaybe str) + parsePort :: Parser PortNumber parsePort = - Opt.option ((fromIntegral :: Int -> PortNumber) <$> auto) ( + Opt.option readPortNumber ( long "port" <> metavar "PORT" <> help "The port number" @@ -439,7 +452,7 @@ parseStartAsNonProducingNode = parseRpcConfig :: Parser PartialRpcConfig parseRpcConfig = do isEnabled <- lastOption parseRpcToggle - rpcEndpoint <- lastOption $ parseRpcUnixSocketEndpoint <|> parseRpcTcpEndpoint + rpcEndpoint <- lastOption $ parseRpcUnixSocketEndpoint <|> parseRpcHttpEndpoint pure (mempty :: PartialRpcConfig){isEnabled, rpcEndpoint} where parseRpcToggle :: Parser Bool @@ -456,32 +469,82 @@ parseRpcConfig = do "grpc-socket-path" "[EXPERIMENTAL] gRPC unix socket path. Defaults to rpc.sock in the same directory as the node socket. Mutually exclusive with --grpc-listen-port." - parseRpcTcpEndpoint :: Parser RpcEndpoint - parseRpcTcpEndpoint = - RpcEndpointTcp . fromMaybe defaultRpcListenAddress + parseRpcHttpEndpoint :: Parser RpcEndpoint + parseRpcHttpEndpoint = + mkHttpEndpoint <$> Opt.optional parseRpcListenAddress <*> Opt.option readPortNumber (mconcat [ long "grpc-listen-port" , metavar "PORT" - , help "[EXPERIMENTAL] TCP port the gRPC server listens on. When set, the gRPC server listens on plaintext TCP (HTTP/2 without TLS) instead of a unix socket. Mutually exclusive with --grpc-socket-path." + , help "[EXPERIMENTAL] TCP port the gRPC server listens on. When set, the gRPC server listens over HTTP/2 without TLS, or HTTP/2 over TLS if --grpc-tls-certificate is given, instead of a unix socket. Mutually exclusive with --grpc-socket-path." ]) + <*> Opt.optional parseRpcTlsFiles - parseRpcListenAddress :: Parser Text + mkHttpEndpoint :: Maybe IP -> PortNumber -> Maybe RpcTlsFiles -> RpcEndpoint + mkHttpEndpoint mAddress port = + maybe (RpcEndpointHttp address port) (RpcEndpointHttps address port) + where + address = fromMaybe defaultRpcListenAddress mAddress + + parseRpcListenAddress :: Parser IP parseRpcListenAddress = - strOption $ mconcat + unNodeHostIPAddress <$> Opt.option (eitherReader parseNodeHostIPAddress) (mconcat [ long "grpc-listen-address" - , metavar "HOST" - , help "[EXPERIMENTAL] Host address the gRPC server binds to. Requires --grpc-listen-port. Defaults to 127.0.0.1." + , metavar "IP-ADDRESS" + , help "[EXPERIMENTAL] IP address the gRPC server binds to. Requires --grpc-listen-port. Defaults to 127.0.0.1." + ]) + + parseRpcTlsFiles :: Parser RpcTlsFiles + parseRpcTlsFiles = + RpcTlsFiles + <$> parseRpcTlsCertificateFile + <*> parseRpcTlsPrivateKeyFile + <*> Opt.many parseRpcTlsChainCertificateFile + + parseRpcTlsCertificateFile :: Parser (File TlsCertificate 'In) + parseRpcTlsCertificateFile = + strOption $ mconcat + [ long "grpc-tls-certificate" + , metavar "FILEPATH" + , help "[EXPERIMENTAL] Path to the TLS certificate file. Enables TLS; requires --grpc-tls-private-key and --grpc-listen-port." + , completer (bashCompleter "file") ] --- | Read a TCP port number, rejecting values outside the 0 - 65535 range. + parseRpcTlsPrivateKeyFile :: Parser (File TlsPrivateKey 'In) + parseRpcTlsPrivateKeyFile = + strOption $ mconcat + [ long "grpc-tls-private-key" + , metavar "FILEPATH" + , help "[EXPERIMENTAL] Path to the TLS private key file. Requires --grpc-tls-certificate and --grpc-listen-port." + , completer (bashCompleter "file") + ] + + parseRpcTlsChainCertificateFile :: Parser (File TlsCertificate 'In) + parseRpcTlsChainCertificateFile = + strOption $ mconcat + [ long "grpc-tls-chain-certificate" + , metavar "FILEPATH" + , help "[EXPERIMENTAL] Path to an additional certificate to include in the TLS chain. May be given multiple times. Requires --grpc-tls-certificate and --grpc-tls-private-key." + , completer (bashCompleter "file") + ] + +-- | Parse a port number, rejecting values outside the 0 - 65535 range. +-- Decimal digits only - hex/octal notation, a sign, and surrounding +-- whitespace (all otherwise accepted by a plain 'Integer' read) are rejected. +-- Shared by every port-taking option in this module ('parsePort', +-- '--grpc-listen-port') and by 'parseHostPort'. +parsePortNumber :: String -> Either String PortNumber +parsePortNumber raw + | not (all isDigit raw) = Left $ "Not a valid port number: " <> raw + | otherwise = + case readMaybe raw :: Maybe Integer of + Just port + | 0 <= port && port <= 65535 -> Right $ fromIntegral port + | otherwise -> Left $ "Port number out of range (0 - 65535): " <> show port + Nothing -> Left $ "Not a valid port number: " <> raw + readPortNumber :: Opt.ReadM PortNumber -readPortNumber = Opt.eitherReader $ \raw -> - case readMaybe raw :: Maybe Integer of - Just port - | 0 <= port && port <= 65535 -> Right $ fromIntegral port - | otherwise -> Left $ "Port number out of range (0 - 65535): " <> show port - Nothing -> Left $ "Not a valid port number: " <> raw +readPortNumber = Opt.eitherReader parsePortNumber -- | Produce just the brief help header for a given CLI option parser, -- without the options. diff --git a/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs b/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs index b25f2a8c5dc..24fcb97171c 100644 --- a/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs +++ b/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs @@ -9,16 +9,14 @@ module Cardano.Node.Tracing.Tracers.Rpc () where -import Cardano.Api (File (..)) import Cardano.Api.Pretty import Cardano.Logging hiding (nsInner) import Cardano.Rpc.Server (TraceRpc (..), TraceRpcNodeKernelAccess (..), TraceRpcQuery (..), TraceRpcSubmit (..), TraceRpcSync (..), TraceSpanEvent (..)) -import Cardano.Rpc.Server.Config (RpcEndpoint (..)) +import Cardano.Rpc.Server.Config () import Data.Aeson (Object, Value (..), (.=)) -import qualified Data.Text as Text instance LogFormatting TraceRpc where forMachine _dtal tr = @@ -69,7 +67,7 @@ instance LogFormatting TraceRpc where TraceRpcUnsupportedBlockType blockType -> ["blockType" .= String blockType] TraceRpcServerListening endpoint -> [ "kind" .= String "ServerListening" - , "endpoint" .= endpointToText endpoint + , "endpoint" .= docToText (pretty endpoint) ] forHuman = docToText . pretty @@ -220,7 +218,3 @@ spanToObject = mconcat . \case SpanBegin spanId -> ["span" .= String "begin", "spanId" .= spanId] SpanEnd spanId -> ["span" .= String "end", "spanId" .= spanId] - -endpointToText :: RpcEndpoint -> Text -endpointToText (RpcEndpointUnixSocket (File socketPath)) = Text.pack socketPath -endpointToText (RpcEndpointTcp host port) = host <> ":" <> Text.pack (show port) diff --git a/cardano-node/src/Cardano/Node/Types.hs b/cardano-node/src/Cardano/Node/Types.hs index 593707cd8ca..4d758fd4625 100644 --- a/cardano-node/src/Cardano/Node/Types.hs +++ b/cardano-node/src/Cardano/Node/Types.hs @@ -49,7 +49,7 @@ import Cardano.Network.ConsensusMode (ConsensusMode (..)) import Cardano.Network.NodeToNode (DiffusionMode (..)) import Cardano.Node.Configuration.Socket (SocketConfig (..)) import Cardano.Node.Orphans () -import Cardano.Rpc.Server.Config (RpcConfigF (..), RpcEndpoint (..)) +import Cardano.Rpc.Server.Config (RpcConfigF (..), RpcEndpoint (..), RpcTlsFiles (..)) import Control.Exception import Data.Aeson @@ -516,7 +516,16 @@ instance Functor f => AdjustFilePaths (RpcConfigF f) where } where adjustEndpoint (RpcEndpointUnixSocket socketPath) = RpcEndpointUnixSocket $ adjustFilePaths f socketPath - adjustEndpoint endpoint@RpcEndpointTcp{} = endpoint + adjustEndpoint endpoint@RpcEndpointHttp{} = endpoint + adjustEndpoint (RpcEndpointHttps host port tlsFiles) = + RpcEndpointHttps host port (adjustTlsFiles tlsFiles) + + adjustTlsFiles RpcTlsFiles{certificateFile, privateKeyFile, chainCertificateFiles} = + RpcTlsFiles + { certificateFile = adjustFilePaths f certificateFile + , privateKeyFile = adjustFilePaths f privateKeyFile + , chainCertificateFiles = adjustFilePaths f <$> chainCertificateFiles + } data VRFPrivateKeyFilePermissionError = OtherPermissionsExist FilePath