Skip to content

Commit cb3250e

Browse files
authored
servers: better socket leak prevention during TLS handshake, add NetworkError type to better diagnose connection errors (#1619)
* servers: better socket leak prevention during TLS handshake * log tcp connection errors * more detailed network error * log full address * rename error * add encodings for NetworkError * refactor * comment * bind * style * remove parameters of NETWORK error from encoding
1 parent 0319add commit cb3250e

File tree

12 files changed

+160
-71
lines changed

12 files changed

+160
-71
lines changed

src/Simplex/FileTransfer/Client.hs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ import Simplex.Messaging.Protocol
5959
RecipientId,
6060
SenderId,
6161
pattern NoEntity,
62+
NetworkError (..),
63+
toNetworkError,
6264
)
6365
import Simplex.Messaging.Transport (ALPN, CertChainPubKey (..), HandshakeError (..), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), defaultSupportedParams)
6466
import Simplex.Messaging.Transport.Client (TransportClientConfig (..), TransportHost)
@@ -191,7 +193,7 @@ xftpHTTP2Config transportConfig XFTPClientConfig {xftpNetworkConfig = NetworkCon
191193
xftpClientError :: HTTP2ClientError -> XFTPClientError
192194
xftpClientError = \case
193195
HCResponseTimeout -> PCEResponseTimeout
194-
HCNetworkError -> PCENetworkError
196+
HCNetworkError e -> PCENetworkError e
195197
HCIOError e -> PCEIOError e
196198

197199
sendXFTPCommand :: forall p. FilePartyI p => XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> FileCommand p -> Maybe XFTPChunkSpec -> ExceptT XFTPClientError IO (FileResponse, HTTP2Body)
@@ -261,9 +263,9 @@ downloadXFTPChunk g c@XFTPClient {config} rpKey fId chunkSpec@XFTPRcvChunkSpec {
261263
ExceptT (sequence <$> (t `timeout` (download cbState `catches` errors))) >>= maybe (throwE PCEResponseTimeout) pure
262264
where
263265
errors =
264-
[ Handler $ \(_e :: H.HTTP2Error) -> pure $ Left PCENetworkError,
265-
Handler $ \(e :: IOException) -> pure $ Left (PCEIOError e),
266-
Handler $ \(_e :: SomeException) -> pure $ Left PCENetworkError
266+
[ Handler $ \(e :: H.HTTP2Error) -> pure $ Left $ PCENetworkError $ NEConnectError $ displayException e,
267+
Handler $ \(e :: IOException) -> pure $ Left $ PCEIOError e,
268+
Handler $ \(e :: SomeException) -> pure $ Left $ PCENetworkError $ toNetworkError e
267269
]
268270
download cbState =
269271
runExceptT . withExceptT PCEResponseError $

src/Simplex/Messaging/Agent/Client.hs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ import Simplex.Messaging.Protocol
250250
EntityId (..),
251251
ServiceId,
252252
ErrorType,
253+
NetworkError (..),
253254
MsgFlags (..),
254255
MsgId,
255256
NtfServer,
@@ -1199,12 +1200,12 @@ protocolClientError protocolError_ host = \case
11991200
PCEResponseError e -> BROKER host $ RESPONSE $ B.unpack $ smpEncode e
12001201
PCEUnexpectedResponse e -> BROKER host $ UNEXPECTED $ B.unpack e
12011202
PCEResponseTimeout -> BROKER host TIMEOUT
1202-
PCENetworkError -> BROKER host NETWORK
1203+
PCENetworkError e -> BROKER host $ NETWORK e
12031204
PCEIncompatibleHost -> BROKER host HOST
12041205
PCETransportError e -> BROKER host $ TRANSPORT e
12051206
e@PCECryptoError {} -> INTERNAL $ show e
12061207
PCEServiceUnavailable {} -> BROKER host NO_SERVICE
1207-
PCEIOError {} -> BROKER host NETWORK
1208+
PCEIOError e -> BROKER host $ NETWORK $ NEConnectError $ E.displayException e
12081209

12091210
data ProtocolTestStep
12101211
= TSConnect
@@ -1478,7 +1479,7 @@ temporaryAgentError = \case
14781479
_ -> False
14791480
where
14801481
tempBrokerError = \case
1481-
NETWORK -> True
1482+
NETWORK _ -> True
14821483
TIMEOUT -> True
14831484
_ -> False
14841485

@@ -1518,7 +1519,7 @@ subscribeQueues c qs = do
15181519
subscribeQueues_ env session smp qs' = do
15191520
let (userId, srv, _) = transportSession' smp
15201521
atomically $ incSMPServerStat' c userId srv connSubAttempts $ length qs'
1521-
rs <- sendBatch (\smp' _ -> subscribeSMPQueues smp') smp NRMBackground qs'
1522+
rs <- sendBatch (\smp' _ -> subscribeSMPQueues smp') smp NRMBackground qs'
15221523
active <-
15231524
atomically $
15241525
ifM
@@ -1529,7 +1530,8 @@ subscribeQueues c qs = do
15291530
then when (hasTempErrors rs) resubscribe $> rs
15301531
else do
15311532
logWarn "subcription batch result for replaced SMP client, resubscribing"
1532-
resubscribe $> L.map (second $ \_ -> Left PCENetworkError) rs
1533+
-- TODO we probably use PCENetworkError here instead of the original error, so it becomes temporary.
1534+
resubscribe $> L.map (second $ Left . PCENetworkError . NESubscribeError . show) rs
15331535
where
15341536
tSess = transportSession' smp
15351537
sessId = sessionId $ thParams smp

src/Simplex/Messaging/Client.hs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -597,12 +597,14 @@ getProtocolClient g nm transportSession@(_, srv, _) cfg@ProtocolClientConfig {qS
597597
socksCreds = clientSocksCredentials networkConfig proxySessTs transportSession
598598
tId <-
599599
runTransportClient tcConfig socksCreds useHost port' (Just $ keyHash srv) (client t c cVar)
600-
`forkFinally` \_ -> void (atomically . tryPutTMVar cVar $ Left PCENetworkError)
600+
`forkFinally` \r ->
601+
let err = either toNetworkError (const NEFailedError) r
602+
in void $ atomically $ tryPutTMVar cVar $ Left $ PCENetworkError err
601603
c_ <- netTimeoutInt tcpConnectTimeout nm `timeout` atomically (takeTMVar cVar)
602604
case c_ of
603605
Just (Right c') -> mkWeakThreadId tId >>= \tId' -> pure $ Right c' {action = Just tId'}
604606
Just (Left e) -> pure $ Left e
605-
Nothing -> killThread tId $> Left PCENetworkError
607+
Nothing -> killThread tId $> Left (PCENetworkError NETimeoutError)
606608

607609
useTransport :: (ServiceName, ATransport 'TClient)
608610
useTransport = case port srv of
@@ -743,7 +745,7 @@ data ProtocolClientError err
743745
PCEResponseTimeout
744746
| -- | Failure to establish TCP connection.
745747
-- Forwarded to the agent client as `ERR BROKER NETWORK`.
746-
PCENetworkError
748+
PCENetworkError NetworkError
747749
| -- | No host compatible with network configuration
748750
PCEIncompatibleHost
749751
| -- | Service is unavailable for command that requires service connection
@@ -761,7 +763,7 @@ type SMPClientError = ProtocolClientError ErrorType
761763

762764
temporaryClientError :: ProtocolClientError err -> Bool
763765
temporaryClientError = \case
764-
PCENetworkError -> True
766+
PCENetworkError _ -> True
765767
PCEResponseTimeout -> True
766768
PCEIOError _ -> True
767769
_ -> False
@@ -782,7 +784,7 @@ smpProxyError = \case
782784
PCEResponseError e -> PROXY $ BROKER $ RESPONSE $ B.unpack $ strEncode e
783785
PCEUnexpectedResponse e -> PROXY $ BROKER $ UNEXPECTED $ B.unpack e
784786
PCEResponseTimeout -> PROXY $ BROKER TIMEOUT
785-
PCENetworkError -> PROXY $ BROKER NETWORK
787+
PCENetworkError e -> PROXY $ BROKER $ NETWORK e
786788
PCEIncompatibleHost -> PROXY $ BROKER HOST
787789
PCEServiceUnavailable -> PROXY $ BROKER $ NO_SERVICE -- for completeness, it cannot happen.
788790
PCETransportError t -> PROXY $ BROKER $ TRANSPORT t

src/Simplex/Messaging/Client/Agent.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ withSMP ca srv action = (getSMPServerClient' ca srv >>= action) `catchE` logSMPE
391391
where
392392
logSMPError :: SMPClientError -> ExceptT SMPClientError IO a
393393
logSMPError e = do
394-
logInfo $ "SMP error (" <> safeDecodeUtf8 (strEncode $ host srv) <> "): " <> tshow e
394+
logInfo $ "SMP error (" <> safeDecodeUtf8 (strEncode srv) <> "): " <> tshow e
395395
throwE e
396396

397397
subscribeQueuesNtfs :: SMPClientAgent 'NotifierService -> SMPServer -> NonEmpty (NotifierId, NtfPrivateAuthKey) -> IO ()

src/Simplex/Messaging/Notifications/Server.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ ntfSubscriber NtfSubscriber {smpAgent = ca@SMPClientAgent {msgQ, agentQ}} =
613613
PCEIncompatibleHost -> Just $ NSErr "IncompatibleHost"
614614
PCEServiceUnavailable -> Just NSService -- this error should not happen on individual subscriptions
615615
PCEResponseTimeout -> Nothing
616-
PCENetworkError -> Nothing
616+
PCENetworkError _ -> Nothing
617617
PCEIOError _ -> Nothing
618618
where
619619
-- Note on moving to PostgreSQL: the idea of logging errors without e is removed here

src/Simplex/Messaging/Protocol.hs

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ module Simplex.Messaging.Protocol
8181
CommandError (..),
8282
ProxyError (..),
8383
BrokerErrorType (..),
84+
NetworkError (..),
8485
BlockingInfo (..),
8586
BlockingReason (..),
8687
RawTransmission,
@@ -168,6 +169,7 @@ module Simplex.Messaging.Protocol
168169
noMsgFlags,
169170
messageId,
170171
messageTs,
172+
toNetworkError,
171173

172174
-- * Parse and serialize
173175
ProtocolMsgTag (..),
@@ -212,7 +214,7 @@ module Simplex.Messaging.Protocol
212214
where
213215

214216
import Control.Applicative (optional, (<|>))
215-
import Control.Exception (Exception)
217+
import Control.Exception (Exception, SomeException, displayException, fromException)
216218
import Control.Monad.Except
217219
import Data.Aeson (FromJSON (..), ToJSON (..))
218220
import qualified Data.Aeson.TH as J
@@ -241,6 +243,7 @@ import GHC.TypeLits (ErrorMessage (..), TypeError, type (+))
241243
import qualified GHC.TypeLits as TE
242244
import qualified GHC.TypeLits as Type
243245
import Network.Socket (ServiceName)
246+
import qualified Network.TLS as TLS
244247
import Simplex.Messaging.Agent.Store.DB (Binary (..), FromField (..), ToField (..))
245248
import qualified Simplex.Messaging.Crypto as C
246249
import Simplex.Messaging.Encoding
@@ -1555,7 +1558,7 @@ data BrokerErrorType
15551558
| -- | unexpected response
15561559
UNEXPECTED {respErr :: String}
15571560
| -- | network error
1558-
NETWORK
1561+
NETWORK {networkError :: NetworkError}
15591562
| -- | no compatible server host (e.g. onion when public is required, or vice versa)
15601563
HOST
15611564
| -- | service unavailable client-side - used in agent errors
@@ -1566,6 +1569,24 @@ data BrokerErrorType
15661569
TIMEOUT
15671570
deriving (Eq, Read, Show, Exception)
15681571

1572+
data NetworkError
1573+
= NEConnectError {connectError :: String}
1574+
| NETLSError {tlsError :: String}
1575+
| NEUnknownCAError
1576+
| NEFailedError
1577+
| NETimeoutError
1578+
| NESubscribeError {subscribeError :: String}
1579+
deriving (Eq, Read, Show)
1580+
1581+
toNetworkError :: SomeException -> NetworkError
1582+
toNetworkError e = maybe (NEConnectError err) fromTLSError (fromException e)
1583+
where
1584+
err = displayException e
1585+
fromTLSError :: TLS.TLSException -> NetworkError
1586+
fromTLSError = \case
1587+
TLS.HandshakeFailed (TLS.Error_Protocol _ TLS.UnknownCa) -> NEUnknownCAError
1588+
_ -> NETLSError err
1589+
15691590
data BlockingInfo = BlockingInfo
15701591
{ reason :: BlockingReason
15711592
}
@@ -2001,7 +2022,7 @@ instance Encoding BrokerErrorType where
20012022
RESPONSE e -> "RESPONSE " <> smpEncode e
20022023
UNEXPECTED e -> "UNEXPECTED " <> smpEncode e
20032024
TRANSPORT e -> "TRANSPORT " <> smpEncode e
2004-
NETWORK -> "NETWORK"
2025+
NETWORK e -> "NETWORK" -- TODO once all upgrade: "NETWORK " <> smpEncode e
20052026
TIMEOUT -> "TIMEOUT"
20062027
HOST -> "HOST"
20072028
NO_SERVICE -> "NO_SERVICE"
@@ -2010,7 +2031,7 @@ instance Encoding BrokerErrorType where
20102031
"RESPONSE" -> RESPONSE <$> _smpP
20112032
"UNEXPECTED" -> UNEXPECTED <$> _smpP
20122033
"TRANSPORT" -> TRANSPORT <$> _smpP
2013-
"NETWORK" -> pure NETWORK
2034+
"NETWORK" -> NETWORK <$> (_smpP <|> pure NEFailedError)
20142035
"TIMEOUT" -> pure TIMEOUT
20152036
"HOST" -> pure HOST
20162037
"NO_SERVICE" -> pure NO_SERVICE
@@ -2021,7 +2042,7 @@ instance StrEncoding BrokerErrorType where
20212042
RESPONSE e -> "RESPONSE " <> encodeUtf8 (T.pack e)
20222043
UNEXPECTED e -> "UNEXPECTED " <> encodeUtf8 (T.pack e)
20232044
TRANSPORT e -> "TRANSPORT " <> smpEncode e
2024-
NETWORK -> "NETWORK"
2045+
NETWORK e -> "NETWORK" -- TODO once all upgrade: "NETWORK " <> strEncode e
20252046
TIMEOUT -> "TIMEOUT"
20262047
HOST -> "HOST"
20272048
NO_SERVICE -> "NO_SERVICE"
@@ -2030,13 +2051,50 @@ instance StrEncoding BrokerErrorType where
20302051
"RESPONSE" -> RESPONSE <$> _textP
20312052
"UNEXPECTED" -> UNEXPECTED <$> _textP
20322053
"TRANSPORT" -> TRANSPORT <$> _smpP
2033-
"NETWORK" -> pure NETWORK
2054+
"NETWORK" -> NETWORK <$> (_strP <|> pure NEFailedError)
20342055
"TIMEOUT" -> pure TIMEOUT
20352056
"HOST" -> pure HOST
20362057
"NO_SERVICE" -> pure NO_SERVICE
20372058
_ -> fail "bad BrokerErrorType"
2038-
where
2039-
_textP = A.space *> (T.unpack . safeDecodeUtf8 <$> A.takeByteString)
2059+
2060+
instance Encoding NetworkError where
2061+
smpEncode = \case
2062+
NEConnectError e -> "CONNECT " <> smpEncode e
2063+
NETLSError e -> "TLS " <> smpEncode e
2064+
NEUnknownCAError -> "UNKNOWNCA"
2065+
NEFailedError -> "FAILED"
2066+
NETimeoutError -> "TIMEOUT"
2067+
NESubscribeError e -> "SUBSCRIBE " <> smpEncode e
2068+
smpP =
2069+
A.takeTill (== ' ') >>= \case
2070+
"CONNECT" -> NEConnectError <$> _smpP
2071+
"TLS" -> NETLSError <$> _smpP
2072+
"UNKNOWNCA" -> pure NEUnknownCAError
2073+
"FAILED" -> pure NEFailedError
2074+
"TIMEOUT" -> pure NETimeoutError
2075+
"SUBSCRIBE" -> NESubscribeError <$> _smpP
2076+
_ -> fail "bad NetworkError"
2077+
2078+
instance StrEncoding NetworkError where
2079+
strEncode = \case
2080+
NEConnectError e -> "CONNECT " <> encodeUtf8 (T.pack e)
2081+
NETLSError e -> "TLS " <> encodeUtf8 (T.pack e)
2082+
NEUnknownCAError -> "UNKNOWNCA"
2083+
NEFailedError -> "FAILED"
2084+
NETimeoutError -> "TIMEOUT"
2085+
NESubscribeError e -> "SUBSCRIBE " <> encodeUtf8 (T.pack e)
2086+
strP =
2087+
A.takeTill (== ' ') >>= \case
2088+
"CONNECT" -> NEConnectError <$> _textP
2089+
"TLS" -> NETLSError <$> _textP
2090+
"UNKNOWNCA" -> pure NEUnknownCAError
2091+
"FAILED" -> pure NEFailedError
2092+
"TIMEOUT" -> pure NETimeoutError
2093+
"SUBSCRIBE" -> NESubscribeError <$> _textP
2094+
_ -> fail "bad NetworkError"
2095+
2096+
_textP :: Parser String
2097+
_textP = A.space *> (T.unpack . safeDecodeUtf8 <$> A.takeByteString)
20402098

20412099
-- | Send signed SMP transmission to TCP transport.
20422100
tPut :: Transport c => THandle v c p -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()]
@@ -2200,6 +2258,8 @@ $(J.deriveJSON defaultJSON ''MsgFlags)
22002258

22012259
$(J.deriveJSON (sumTypeJSON id) ''CommandError)
22022260

2261+
$(J.deriveJSON (sumTypeJSON $ dropPrefix "NE") ''NetworkError)
2262+
22032263
$(J.deriveJSON (sumTypeJSON id) ''BrokerErrorType)
22042264

22052265
$(J.deriveJSON defaultJSON ''BlockingInfo)

src/Simplex/Messaging/Server/Information.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
1414
import Data.Int (Int64)
1515
import Data.Maybe (isJust)
1616
import Data.Text (Text)
17-
import Simplex.Messaging.Agent.Protocol (ConnectionLink, ConnectionMode (..), ConnectionRequestUri)
17+
import Simplex.Messaging.Agent.Protocol (ConnectionLink, ConnectionMode (..))
1818
import Simplex.Messaging.Encoding.String
1919
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON)
2020

src/Simplex/Messaging/Transport/Client.hs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ where
3030

3131
import Control.Applicative (optional, (<|>))
3232
import Control.Logger.Simple (logError)
33+
import Control.Monad
3334
import Data.Aeson (FromJSON (..), ToJSON (..))
3435
import qualified Data.Attoparsec.ByteString.Char8 as A
3536
import Data.ByteString.Char8 (ByteString)
3637
import qualified Data.ByteString.Char8 as B
3738
import Data.Char (isAsciiLower, isDigit, isHexDigit)
3839
import Data.Default (def)
40+
import Data.Functor (($>))
3941
import Data.IORef
4042
import Data.IP
4143
import Data.List.NonEmpty (NonEmpty (..))
@@ -58,7 +60,7 @@ import Simplex.Messaging.Parsers (parseAll, parseString)
5860
import Simplex.Messaging.Transport
5961
import Simplex.Messaging.Transport.KeepAlive
6062
import Simplex.Messaging.Transport.Shared
61-
import Simplex.Messaging.Util (bshow, catchAll, tshow, (<$?>))
63+
import Simplex.Messaging.Util (bshow, catchAll, catchAll_, tshow, (<$?>))
6264
import System.IO.Error
6365
import Text.Read (readMaybe)
6466
import UnliftIO.Exception (IOException)
@@ -156,6 +158,11 @@ clientTransportConfig TransportClientConfig {logTLSErrors} =
156158
runTransportClient :: Transport c => TransportClientConfig -> Maybe SocksCredentials -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c 'TClient -> IO a) -> IO a
157159
runTransportClient = runTLSTransportClient defaultSupportedParams Nothing
158160

161+
data ConnectionHandle c
162+
= CHSocket Socket
163+
| CHContext T.Context
164+
| CHTransport (c 'TClient)
165+
159166
runTLSTransportClient :: Transport c => T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> Maybe SocksCredentials -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c 'TClient -> IO a) -> IO a
160167
runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive, clientCredentials, clientALPN, useSNI} socksCreds host port keyHash client = do
161168
serverCert <- newEmptyTMVarIO
@@ -165,17 +172,22 @@ runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy,
165172
connectTCP = case socksProxy of
166173
Just proxy -> connectSocksClient proxy socksCreds (hostAddr host)
167174
_ -> connectTCPClient hostName
168-
c <- do
169-
sock <- connectTCP port
170-
mapM_ (setSocketKeepAlive sock) tcpKeepAlive `catchAll` \e -> logError ("Error setting TCP keep-alive" <> tshow e)
175+
h <- newIORef Nothing
176+
let set hc = (>>= \c -> writeIORef h (Just $ hc c) $> c)
177+
E.bracket (set CHSocket $ connectTCP port) (\_ -> closeConn h) $ \sock -> do
178+
mapM_ (setSocketKeepAlive sock) tcpKeepAlive `catchAll` \e -> logError ("Error setting TCP keep-alive " <> tshow e)
171179
let tCfg = clientTransportConfig cfg
172180
-- No TLS timeout to avoid failing connections via SOCKS
173-
tls <- connectTLS (Just hostName) tCfg clientParams sock
174-
chain <- takePeerCertChain serverCert `E.onException` closeTLS tls
181+
tls <- set CHContext $ connectTLS (Just hostName) tCfg clientParams sock
182+
chain <- takePeerCertChain serverCert
175183
sent <- readIORef clientCredsSent
176-
getTransportConnection tCfg sent chain tls
177-
client c `E.finally` closeConnection c
184+
client =<< set CHTransport (getTransportConnection tCfg sent chain tls)
178185
where
186+
closeConn = readIORef >=> mapM_ (\c -> E.uninterruptibleMask_ $ closeConn_ c `catchAll_` pure ())
187+
closeConn_ = \case
188+
CHSocket sock -> close sock
189+
CHContext tls -> closeTLS tls
190+
CHTransport c -> closeConnection c
179191
hostAddr = \case
180192
THIPv4 addr -> SocksAddrIPV4 $ tupleToHostAddress addr
181193
THIPv6 addr -> SocksAddrIPV6 addr
@@ -199,10 +211,11 @@ connectTCPClient host port = withSocketsDo $ resolve >>= tryOpen err
199211
E.try (open addr) >>= either (`tryOpen` as) pure
200212

201213
open :: AddrInfo -> IO Socket
202-
open addr = do
203-
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
204-
connect sock $ addrAddress addr
205-
pure sock
214+
open addr =
215+
E.bracketOnError
216+
(socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))
217+
close
218+
(\sock -> connect sock (addrAddress addr) $> sock)
206219

207220
defaultSMPPort :: PortNumber
208221
defaultSMPPort = 5223

0 commit comments

Comments
 (0)