Skip to content

Commit fa772af

Browse files
authored
agent: support socks proxy without isolate-by-auth, with and without credentials (#1320)
* agent: support socks proxy without isolate-by-auth, with and without credentials * add unit tests * make xftp use correct SOCKS credentials * rename * support ipv6 in brackets, test parsing * constant * textToHostMode * space
1 parent bec4e5e commit fa772af

File tree

9 files changed

+229
-47
lines changed

9 files changed

+229
-47
lines changed

simplexmq.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,7 @@ test-suite simplexmq-test
610610
CoreTests.CryptoTests
611611
CoreTests.EncodingTests
612612
CoreTests.RetryIntervalTests
613+
CoreTests.SOCKSSettings
613614
CoreTests.TRcvQueuesTests
614615
CoreTests.UtilTests
615616
CoreTests.VersionRangeTests

src/Simplex/FileTransfer/Client.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import Simplex.Messaging.Client
3838
defaultNetworkConfig,
3939
proxyUsername,
4040
transportClientConfig,
41+
clientSocksCredentials,
4142
unexpectedResponse,
4243
)
4344
import qualified Simplex.Messaging.Crypto as C
@@ -100,15 +101,15 @@ defaultXFTPClientConfig =
100101

101102
getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient)
102103
getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} disconnected = runExceptT $ do
103-
let username = proxyUsername transportSession
104+
let socksCreds = clientSocksCredentials xftpNetworkConfig $ proxyUsername transportSession
104105
ProtocolServer _ host port keyHash = srv
105106
useHost <- liftEither $ chooseTransportHost xftpNetworkConfig host
106107
let tcConfig = (transportClientConfig xftpNetworkConfig useHost) {alpn = clientALPN}
107108
http2Config = xftpHTTP2Config tcConfig config
108109
clientVar <- newTVarIO Nothing
109110
let usePort = if null port then "443" else port
110111
clientDisconnected = readTVarIO clientVar >>= mapM_ disconnected
111-
http2Client <- liftError' xftpClientError $ getVerifiedHTTP2Client (Just username) useHost usePort (Just keyHash) Nothing http2Config clientDisconnected
112+
http2Client <- liftError' xftpClientError $ getVerifiedHTTP2Client socksCreds useHost usePort (Just keyHash) Nothing http2Config clientDisconnected
112113
let HTTP2Client {sessionId, sessionALPN} = http2Client
113114
v = VersionXFTP 1
114115
thServerVRange = versionToRange v

src/Simplex/Messaging/Client.hs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,12 @@ module Simplex.Messaging.Client
8080
defaultSMPClientConfig,
8181
defaultNetworkConfig,
8282
transportClientConfig,
83+
clientSocksCredentials,
8384
chooseTransportHost,
8485
proxyUsername,
8586
temporaryClientError,
8687
smpProxyError,
88+
textToHostMode,
8789
ServerTransmissionBatch,
8890
ServerTransmission (..),
8991
ClientCommand,
@@ -122,10 +124,13 @@ import Data.List (find)
122124
import Data.List.NonEmpty (NonEmpty (..))
123125
import qualified Data.List.NonEmpty as L
124126
import Data.Maybe (catMaybes, fromMaybe)
127+
import Data.Text (Text)
128+
import qualified Data.Text as T
125129
import Data.Time.Clock (UTCTime (..), diffUTCTime, getCurrentTime)
126130
import qualified Data.X509 as X
127131
import qualified Data.X509.Validation as XV
128132
import Network.Socket (ServiceName)
133+
import Network.Socks5 (SocksCredentials (..))
129134
import Numeric.Natural
130135
import qualified Simplex.Messaging.Crypto as C
131136
import Simplex.Messaging.Encoding
@@ -136,7 +141,7 @@ import Simplex.Messaging.Server.QueueStore.QueueInfo
136141
import Simplex.Messaging.TMap (TMap)
137142
import qualified Simplex.Messaging.TMap as TM
138143
import Simplex.Messaging.Transport
139-
import Simplex.Messaging.Transport.Client (SocksProxy, TransportClientConfig (..), TransportHost (..), defaultTcpConnectTimeout, runTransportClient)
144+
import Simplex.Messaging.Transport.Client (SocksAuth (..), SocksProxyWithAuth (..), TransportClientConfig (..), TransportHost (..), defaultTcpConnectTimeout, runTransportClient)
140145
import Simplex.Messaging.Transport.KeepAlive
141146
import Simplex.Messaging.Transport.WebSockets (WS)
142147
import Simplex.Messaging.Util (bshow, diffToMicroseconds, ifM, liftEitherWith, raceAny_, threadDelay', tshow, whenM)
@@ -236,6 +241,12 @@ data HostMode
236241
HMPublic
237242
deriving (Eq, Show)
238243

244+
textToHostMode :: Text -> Either String HostMode
245+
textToHostMode = \case
246+
"public" -> Right HMPublic
247+
"onion" -> Right HMOnionViaSocks
248+
s -> Left $ T.unpack $ "Invalid host_mode: " <> s
249+
239250
data SocksMode
240251
= -- | always use SOCKS proxy when enabled
241252
SMAlways
@@ -257,7 +268,7 @@ instance StrEncoding SocksMode where
257268
-- | network configuration for the client
258269
data NetworkConfig = NetworkConfig
259270
{ -- | use SOCKS5 proxy
260-
socksProxy :: Maybe SocksProxy,
271+
socksProxy :: Maybe SocksProxyWithAuth,
261272
-- | when to use SOCKS proxy
262273
socksMode :: SocksMode,
263274
-- | determines critera which host is chosen from the list
@@ -355,12 +366,22 @@ transportClientConfig :: NetworkConfig -> TransportHost -> TransportClientConfig
355366
transportClientConfig NetworkConfig {socksProxy, socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} host =
356367
TransportClientConfig {socksProxy = useSocksProxy socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing}
357368
where
358-
useSocksProxy SMAlways = socksProxy
369+
socksProxy' = (\(SocksProxyWithAuth _ proxy) -> proxy) <$> socksProxy
370+
useSocksProxy SMAlways = socksProxy'
359371
useSocksProxy SMOnion = case host of
360-
THOnionHost _ -> socksProxy
372+
THOnionHost _ -> socksProxy'
361373
_ -> Nothing
362374
{-# INLINE transportClientConfig #-}
363375

376+
clientSocksCredentials :: NetworkConfig -> ByteString -> Maybe SocksCredentials
377+
clientSocksCredentials NetworkConfig {socksProxy} sessionUsername = case socksProxy of
378+
Just (SocksProxyWithAuth auth _) -> case auth of
379+
SocksAuthUsername {username, password} -> Just $ SocksCredentials username password
380+
SocksAuthNull -> Nothing
381+
SocksIsolateByAuth -> Just $ SocksCredentials sessionUsername ""
382+
Nothing -> Nothing
383+
{-# INLINE clientSocksCredentials #-}
384+
364385
-- | protocol client configuration.
365386
data ProtocolClientConfig v = ProtocolClientConfig
366387
{ -- | size of TBQueue to use for server commands and responses
@@ -489,9 +510,9 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
489510
runClient (port', ATransport t) useHost c = do
490511
cVar <- newEmptyTMVarIO
491512
let tcConfig = (transportClientConfig networkConfig useHost) {alpn = clientALPN}
492-
username = proxyUsername transportSession
513+
socksCreds = clientSocksCredentials networkConfig $ proxyUsername transportSession
493514
tId <-
494-
runTransportClient tcConfig (Just username) useHost port' (Just $ keyHash srv) (client t c cVar)
515+
runTransportClient tcConfig socksCreds useHost port' (Just $ keyHash srv) (client t c cVar)
495516
`forkFinally` \_ -> void (atomically . tryPutTMVar cVar $ Left PCENetworkError)
496517
c_ <- tcpConnectTimeout `timeout` atomically (takeTMVar cVar)
497518
case c_ of

src/Simplex/Messaging/Notifications/Server/Main.hs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import qualified Data.Text as T
1616
import qualified Data.Text.IO as T
1717
import Network.Socket (HostName)
1818
import Options.Applicative
19-
import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig)
19+
import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig, textToHostMode)
2020
import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig)
2121
import qualified Simplex.Messaging.Crypto as C
2222
import Simplex.Messaging.Notifications.Server (runNtfServer)
@@ -26,7 +26,6 @@ import Simplex.Messaging.Notifications.Transport (supportedNTFHandshakes, suppor
2626
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), pattern NtfServer)
2727
import Simplex.Messaging.Server.CLI
2828
import Simplex.Messaging.Server.Expiration
29-
import Simplex.Messaging.Server.Main (textToHostMode)
3029
import Simplex.Messaging.Transport (simplexMQVersion)
3130
import Simplex.Messaging.Transport.Client (TransportHost (..))
3231
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig)
@@ -139,7 +138,7 @@ ntfServerCLI cfgPath logPath =
139138
defaultNetworkConfig
140139
{ socksProxy = either error id <$!> strDecodeIni "SUBSCRIBER" "socks_proxy" ini,
141140
socksMode = maybe SMOnion (either error id) $! strDecodeIni "SUBSCRIBER" "socks_mode" ini,
142-
hostMode = either (const HMPublic) textToHostMode $ lookupValue "SUBSCRIBER" "host_mode" ini,
141+
hostMode = either (const HMPublic) (either error id . textToHostMode) $ lookupValue "SUBSCRIBER" "host_mode" ini,
143142
requiredHostMode = fromMaybe False $ iniOnOff "SUBSCRIBER" "required_host_mode" ini,
144143
smpPingInterval = 60_000_000 -- 1 minutes
145144
}

src/Simplex/Messaging/Server/Main.hs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import qualified Data.Text.IO as T
2929
import Network.Socket (HostName)
3030
import Options.Applicative
3131
import Simplex.Messaging.Agent.Protocol (connReqUriP')
32-
import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig)
32+
import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig, textToHostMode)
3333
import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig)
3434
import qualified Simplex.Messaging.Crypto as C
3535
import Simplex.Messaging.Encoding.String
@@ -307,7 +307,7 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
307307
defaultNetworkConfig
308308
{ socksProxy = either error id <$!> strDecodeIni "PROXY" "socks_proxy" ini,
309309
socksMode = maybe SMOnion (either error id) $! strDecodeIni "PROXY" "socks_mode" ini,
310-
hostMode = either (const HMPublic) textToHostMode $ lookupValue "PROXY" "host_mode" ini,
310+
hostMode = either (const HMPublic) (either error id . textToHostMode) $ lookupValue "PROXY" "host_mode" ini,
311311
requiredHostMode = fromMaybe False $ iniOnOff "PROXY" "required_host_mode" ini
312312
}
313313
},
@@ -341,12 +341,6 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
341341
where
342342
isOnion = \case THOnionHost _ -> True; _ -> False
343343

344-
textToHostMode :: Text -> HostMode
345-
textToHostMode = \case
346-
"public" -> HMPublic
347-
"onion" -> HMOnionViaSocks
348-
s -> error . T.unpack $ "Invalid host_mode: " <> s
349-
350344
data EmbeddedWebParams = EmbeddedWebParams
351345
{ webStaticPath :: FilePath,
352346
webHttpPort :: Maybe Int,

src/Simplex/Messaging/Transport/Client.hs

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,21 @@ module Simplex.Messaging.Transport.Client
1313
defaultSMPPort,
1414
defaultTcpConnectTimeout,
1515
defaultTransportClientConfig,
16+
defaultSocksProxyWithAuth,
1617
defaultSocksProxy,
18+
defaultSocksHost,
1719
TransportClientConfig (..),
18-
SocksProxy,
20+
SocksProxy (..),
21+
SocksProxyWithAuth (..),
22+
SocksAuth (..),
1923
TransportHost (..),
2024
TransportHosts (..),
2125
TransportHosts_ (..),
2226
validateCertificateChain,
2327
)
2428
where
2529

26-
import Control.Applicative (optional)
30+
import Control.Applicative (optional, (<|>))
2731
import Control.Logger.Simple (logError)
2832
import Control.Monad (when)
2933
import Data.Aeson (FromJSON (..), ToJSON (..))
@@ -79,14 +83,17 @@ instance StrEncoding TransportHost where
7983
strP =
8084
A.choice
8185
[ THIPv4 <$> ((,,,) <$> ipNum <*> ipNum <*> ipNum <*> A.decimal),
82-
maybe (Left "bad IPv6") (Right . THIPv6 . fromIPv6w) . readMaybe . B.unpack <$?> A.takeWhile1 (\c -> isHexDigit c || c == ':'),
86+
maybe (Left "bad IPv6") (Right . THIPv6 . fromIPv6w) . readMaybe . B.unpack <$?> ipv6StrP,
8387
THOnionHost <$> ((<>) <$> A.takeWhile (\c -> isAsciiLower c || isDigit c) <*> A.string ".onion"),
8488
THDomainName . B.unpack <$> (notOnion <$?> A.takeWhile1 (A.notInClass ":#,;/ \n\r\t"))
8589
]
8690
where
8791
ipNum = validIP <$?> (A.decimal <* A.char '.')
8892
validIP :: Int -> Either String Word8
8993
validIP n = if 0 <= n && n <= 255 then Right $ fromIntegral n else Left "invalid IP address"
94+
ipv6StrP =
95+
A.char '[' *> A.takeWhile1 (/= ']') <* A.char ']'
96+
<|> A.takeWhile1 (\c -> isHexDigit c || c == ':')
9097
notOnion s = if ".onion" `B.isSuffixOf` s then Left "invalid onion host" else Right s
9198

9299
instance ToJSON TransportHost where
@@ -134,16 +141,16 @@ clientTransportConfig TransportClientConfig {logTLSErrors} =
134141
TransportConfig {logTLSErrors, transportTimeout = Nothing}
135142

136143
-- | Connect to passed TCP host:port and pass handle to the client.
137-
runTransportClient :: Transport c => TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
144+
runTransportClient :: Transport c => TransportClientConfig -> Maybe SocksCredentials -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
138145
runTransportClient = runTLSTransportClient supportedParameters Nothing
139146

140-
runTLSTransportClient :: Transport c => T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
141-
runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive, clientCredentials, alpn} proxyUsername host port keyHash client = do
147+
runTLSTransportClient :: Transport c => T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> Maybe SocksCredentials -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
148+
runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive, clientCredentials, alpn} socksCreds host port keyHash client = do
142149
serverCert <- newEmptyTMVarIO
143150
let hostName = B.unpack $ strEncode host
144151
clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials alpn serverCert
145152
connectTCP = case socksProxy of
146-
Just proxy -> connectSocksClient proxy proxyUsername (hostAddr host)
153+
Just proxy -> connectSocksClient proxy socksCreds (hostAddr host)
147154
_ -> connectTCPClient hostName
148155
c <- do
149156
sock <- connectTCP port
@@ -191,40 +198,70 @@ connectTCPClient host port = withSocketsDo $ resolve >>= tryOpen err
191198
defaultSMPPort :: PortNumber
192199
defaultSMPPort = 5223
193200

194-
connectSocksClient :: SocksProxy -> Maybe ByteString -> SocksHostAddress -> ServiceName -> IO Socket
195-
connectSocksClient (SocksProxy addr) proxyUsername hostAddr _port = do
201+
connectSocksClient :: SocksProxy -> Maybe SocksCredentials -> SocksHostAddress -> ServiceName -> IO Socket
202+
connectSocksClient (SocksProxy addr) socksCreds hostAddr _port = do
196203
let port = if null _port then defaultSMPPort else fromMaybe defaultSMPPort $ readMaybe _port
197-
fst <$> case proxyUsername of
198-
Just username -> socksConnectAuth (defaultSocksConf addr) (SocksAddress hostAddr port) (SocksCredentials username "")
204+
fst <$> case socksCreds of
205+
Just creds -> socksConnectAuth (defaultSocksConf addr) (SocksAddress hostAddr port) creds
199206
_ -> socksConnect (defaultSocksConf addr) (SocksAddress hostAddr port)
200207

201-
defaultSocksHost :: HostAddress
202-
defaultSocksHost = tupleToHostAddress (127, 0, 0, 1)
208+
defaultSocksHost :: (Word8, Word8, Word8, Word8)
209+
defaultSocksHost = (127, 0, 0, 1)
210+
211+
defaultSocksProxyWithAuth :: SocksProxyWithAuth
212+
defaultSocksProxyWithAuth = SocksProxyWithAuth SocksIsolateByAuth defaultSocksProxy
203213

204214
defaultSocksProxy :: SocksProxy
205-
defaultSocksProxy = SocksProxy $ SockAddrInet 9050 defaultSocksHost
215+
defaultSocksProxy = SocksProxy $ SockAddrInet 9050 $ tupleToHostAddress defaultSocksHost
206216

207217
newtype SocksProxy = SocksProxy SockAddr
208218
deriving (Eq)
209219

220+
data SocksProxyWithAuth = SocksProxyWithAuth SocksAuth SocksProxy
221+
deriving (Eq, Show)
222+
223+
data SocksAuth
224+
= SocksAuthUsername {username :: ByteString, password :: ByteString}
225+
| SocksAuthNull
226+
| SocksIsolateByAuth -- this is default
227+
deriving (Eq, Show)
228+
210229
instance Show SocksProxy where show (SocksProxy addr) = show addr
211230

212231
instance StrEncoding SocksProxy where
213232
strEncode = B.pack . show
214233
strP = do
215-
host <- maybe defaultSocksHost tupleToHostAddress <$> optional ipv4P
234+
host <- fromMaybe (THIPv4 defaultSocksHost) <$> optional strP
216235
port <- fromMaybe 9050 <$> optional (A.char ':' *> (fromInteger <$> A.decimal))
217-
pure . SocksProxy $ SockAddrInet port host
236+
SocksProxy <$> socksAddr port host
218237
where
219-
ipv4P = (,,,) <$> ipNum <*> ipNum <*> ipNum <*> A.decimal
220-
ipNum = A.decimal <* A.char '.'
238+
socksAddr port = \case
239+
THIPv4 addr -> pure $ SockAddrInet port $ tupleToHostAddress addr
240+
THIPv6 addr -> pure $ SockAddrInet6 port 0 addr 0
241+
_ -> fail "SOCKS5 host should be IPv4 or IPv6 address"
221242

222-
instance ToJSON SocksProxy where
243+
instance StrEncoding SocksProxyWithAuth where
244+
strEncode (SocksProxyWithAuth auth proxy) = strEncode auth <> strEncode proxy
245+
strP = SocksProxyWithAuth <$> strP <*> strP
246+
247+
instance ToJSON SocksProxyWithAuth where
223248
toJSON = strToJSON
224249
toEncoding = strToJEncoding
225250

226-
instance FromJSON SocksProxy where
227-
parseJSON = strParseJSON "SocksProxy"
251+
instance FromJSON SocksProxyWithAuth where
252+
parseJSON = strParseJSON "SocksProxyWithAuth"
253+
254+
instance StrEncoding SocksAuth where
255+
strEncode = \case
256+
SocksAuthUsername {username, password} -> username <> ":" <> password <> "@"
257+
SocksAuthNull -> "@"
258+
SocksIsolateByAuth -> ""
259+
strP = usernameP <|> (SocksAuthNull <$ A.char '@') <|> pure SocksIsolateByAuth
260+
where
261+
usernameP = do
262+
username <- A.takeTill (== ':') <* A.char ':'
263+
password <- A.takeTill (== '@') <* A.char '@'
264+
pure SocksAuthUsername {username, password}
228265

229266
mkTLSClientParams :: T.Supported -> Maybe XS.CertificateStore -> HostName -> ServiceName -> Maybe C.KeyHash -> Maybe (X.CertificateChain, T.PrivKey) -> Maybe [ALPN] -> TMVar X.CertificateChain -> T.ClientParams
230267
mkTLSClientParams supported caStore_ host port cafp_ clientCreds_ alpn_ serverCerts =

0 commit comments

Comments
 (0)