socks5 0.6.0.1 → 0.8.0.0
raw patch · 8 files changed
+297/−272 lines, 8 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Network.SOCKS5.Types: AddressDomain :: Text -> Address
- Network.SOCKS5.Types: AddressIPv4 :: IPv4 -> Address
- Network.SOCKS5.Types: AddressIPv6 :: IPv6 -> Address
- Network.SOCKS5.Types: GSSAPI :: Method
- Network.SOCKS5.Types: IANAAssigned :: Word8 -> Method
- Network.SOCKS5.Types: NoAcceptableMethods :: Method
- Network.SOCKS5.Types: NoAuth :: Method
- Network.SOCKS5.Types: PrivateMethod :: Word8 -> Method
- Network.SOCKS5.Types: UnknownMethod :: Word8 -> Method
- Network.SOCKS5.Types: UserPass :: Method
- Network.SOCKS5.Types: data Address
- Network.SOCKS5.Types: data Method
- Network.SOCKS5.Types: fromSockAddr_ :: SockAddr -> (Address, PortNumber)
- Network.SOCKS5.Types: instance Data.Binary.Class.Binary Network.SOCKS5.Types.Address
- Network.SOCKS5.Types: instance Data.Binary.Class.Binary Network.SOCKS5.Types.Method
- Network.SOCKS5.Types: instance GHC.Classes.Eq Network.SOCKS5.Types.Address
- Network.SOCKS5.Types: instance GHC.Classes.Eq Network.SOCKS5.Types.Method
- Network.SOCKS5.Types: instance GHC.Show.Show Network.SOCKS5.Types.Address
- Network.SOCKS5.Types: instance GHC.Show.Show Network.SOCKS5.Types.Method
- Network.SOCKS5.Types: toSockAddr_ :: Address -> PortNumber -> SockAddr
+ Network.SOCKS5.Client: execBind :: Connection c => HostName -> PortNumber -> ClientConfig -> c -> (HostName -> PortNumber -> IO ()) -> IO (HostName, PortNumber)
+ Network.SOCKS5.Client: execConnect :: Connection c => HostName -> PortNumber -> ClientConfig -> c -> IO ()
+ Network.SOCKS5.Client: execUDPAssociate :: Connection c => ClientConfig -> c -> IO (HostName, PortNumber)
+ Network.SOCKS5.Internal: AddressDomain :: Text -> Address
+ Network.SOCKS5.Internal: AddressIPv4 :: IPv4 -> Address
+ Network.SOCKS5.Internal: AddressIPv6 :: IPv6 -> Address
+ Network.SOCKS5.Internal: GSSAPI :: Method
+ Network.SOCKS5.Internal: IANAAssigned :: Word8 -> Method
+ Network.SOCKS5.Internal: NoAcceptableMethods :: Method
+ Network.SOCKS5.Internal: NoAuth :: Method
+ Network.SOCKS5.Internal: PrivateMethod :: Word8 -> Method
+ Network.SOCKS5.Internal: UnknownMethod :: Word8 -> Method
+ Network.SOCKS5.Internal: UserPass :: Method
+ Network.SOCKS5.Internal: data Address
+ Network.SOCKS5.Internal: data Method
+ Network.SOCKS5.Internal: fromSockAddr_ :: SockAddr -> (Address, PortNumber)
+ Network.SOCKS5.Internal: instance Data.Binary.Class.Binary Network.SOCKS5.Internal.Address
+ Network.SOCKS5.Internal: instance Data.Binary.Class.Binary Network.SOCKS5.Internal.Method
+ Network.SOCKS5.Internal: instance GHC.Classes.Eq Network.SOCKS5.Internal.Address
+ Network.SOCKS5.Internal: instance GHC.Classes.Eq Network.SOCKS5.Internal.Method
+ Network.SOCKS5.Internal: instance GHC.Show.Show Network.SOCKS5.Internal.Address
+ Network.SOCKS5.Internal: instance GHC.Show.Show Network.SOCKS5.Internal.Method
+ Network.SOCKS5.Internal: toSockAddr_ :: Address -> PortNumber -> SockAddr
- Network.SOCKS5.Client: runTCPBind :: Address -> ServiceName -> ClientConfig -> (Address -> PortNumber -> IO ()) -> (Address -> PortNumber -> Socket -> IO a) -> IO a
+ Network.SOCKS5.Client: runTCPBind :: HostName -> PortNumber -> ClientConfig -> (HostName -> PortNumber -> IO ()) -> (HostName -> PortNumber -> Socket -> IO a) -> IO a
- Network.SOCKS5.Client: runTCPBindTLS :: Address -> ServiceName -> ClientConfig -> ClientParams -> (Address -> PortNumber -> IO ()) -> (Address -> PortNumber -> Context -> IO a) -> IO a
+ Network.SOCKS5.Client: runTCPBindTLS :: HostName -> PortNumber -> ClientConfig -> ClientParams -> (HostName -> PortNumber -> IO ()) -> (HostName -> PortNumber -> Context -> IO a) -> IO a
- Network.SOCKS5.Client: runTCPConnect :: Address -> ServiceName -> ClientConfig -> (Socket -> IO a) -> IO a
+ Network.SOCKS5.Client: runTCPConnect :: HostName -> PortNumber -> ClientConfig -> (Socket -> IO a) -> IO a
- Network.SOCKS5.Client: runTCPConnectTLS :: Address -> ServiceName -> ClientConfig -> ClientParams -> (Context -> IO a) -> IO a
+ Network.SOCKS5.Client: runTCPConnectTLS :: HostName -> PortNumber -> ClientConfig -> ClientParams -> (Context -> IO a) -> IO a
Files
- CHANGELOG.md +16/−12
- Network/SOCKS5/Client.hs +143/−107
- Network/SOCKS5/Internal.hs +113/−3
- Network/SOCKS5/Server.hs +13/−10
- Network/SOCKS5/Types.hs +0/−123
- example/Main.hs +1/−4
- socks5.cabal +1/−2
- tests/Spec.hs +10/−11
CHANGELOG.md view
@@ -1,29 +1,33 @@ # Revision history for socks5 -## 0.1.0.0 -- 2025-06-30+## 0.8.0.0 -- 2025-07-09 -- First version. Released on an unsuspecting world.+- Adjusted the argument of client API. Low-level API for created connections added. -## 0.2.0.0 -- 2025-07-06+## 0.6.0.1 -- 2025-07-08 -- TLS version API and server executable's CLI args added.+- Changed base requirement for example and tests -## 0.3.0.0 -- 2025-07-07+## 0.6.0.0 -- 2025-07-08 -- Adjusted the order of argument for client side API.+- Removed a record from client config. Rename test directory to tests. Added tests for TLS and UserPass authentication. +## 0.5.0.0 -- 2025-07-07++- Moved some types to separate file. Haddock documentation added. Default server port changed to 1080.+ ## 0.4.0.0 -- 2025-07-07 - UserPass authentication added, adjusted server API and CLI. -## 0.5.0.0 -- 2025-07-07+## 0.3.0.0 -- 2025-07-07 -- Moved some types to separate file. Haddock documentation added. Default server port changed to 1080.+- Adjusted the order of argument for client side API. -## 0.6.0.0 -- 2025-07-08+## 0.2.0.0 -- 2025-07-06 -- Removed a record from client config. Rename test directory to tests. Added tests for TLS and UserPass authentication.+- TLS version API and server executable's CLI args added. -## 0.6.0.1 -- 2025-07-08+## 0.1.0.0 -- 2025-06-30 -- Changed base requirement for example and tests+- First version. Released on an unsuspecting world.
Network/SOCKS5/Client.hs view
@@ -1,14 +1,24 @@ {-# LANGUAGE OverloadedStrings #-} --- | Functions to start connection using SOCKS5 proxy.+-- |+-- This module provides a client for the SOCKS5 proxy protocol+-- It offers two tiers of functions: ----- Here is an minimal example of how to use the library to connect to a remote server through a SOCKS5 proxy:+-- * High-level 'run...' functions: These are "all-in-one" helpers that+-- manage the entire connection, from connecting to the proxy to running+-- your client code. --+-- * Low-level 'exec...' functions: These functions execute a single SOCKS5+-- command on an already established connection. They provide more control+-- for users who need to manage the connection to the proxy manually.+--+-- = Example Usage+-- Here is a minimal example of how to use the library to connect to a remote server through a SOCKS5 proxy:+-- -- > {-# LANGUAGE OverloadedStrings #-} -- > -- > import qualified Data.ByteString.Char8 as C8 -- > import Network.SOCKS5.Client--- > import Network.SOCKS5.Types -- > import Network.Socket.ByteString (recv, sendAll) -- > -- > main :: IO ()@@ -19,10 +29,8 @@ -- > proxyPort = "11451", -- > userPass = Nothing -- > }--- > let destAddr = AddressDomain "example.com"--- > let destPort = "80" -- >--- > runTCPConnect destAddr destPort proxyConfig $ \sock -> do+-- > runTCPConnect "example.com" 80 proxyConfig $ \sock -> do -- > putStrLn "Connected to example.com through SOCKS5 proxy!" -- > sendAll sock "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n" -- > response <- recv sock 4096@@ -30,15 +38,20 @@ module Network.SOCKS5.Client ( ClientConfig (..), - -- * SOCKS5+ -- * High-level functions runTCPConnect, runTCPBind, runUDPAssociate, - -- * SOCKS5 with TLS+ -- * High-level functions with TLS runTCPConnectTLS, runTCPBindTLS, runUDPAssociateTLS,++ -- * Low-level functions+ execConnect,+ execBind,+ execUDPAssociate, ) where @@ -46,199 +59,222 @@ import Control.Monad.State import Data.Binary (Binary, decode) import Data.ByteString qualified as B+import Data.IP import Data.Text.Lazy qualified as LT import Network.Run.TCP import Network.Run.UDP import Network.SOCKS5.Internal-import Network.SOCKS5.Types import Network.Socket import Network.Socket.ByteString qualified as SB import Network.TLS import Text.Read (readMaybe) +-- | Configuration for connecting to the SOCKS5 proxy server. data ClientConfig = ClientConfig- { -- | Hostname or IP address of the SOCKS5 proxy server+ { -- | Hostname or IP address of the SOCKS5 proxy server. proxyHost :: HostName,- -- | Port number of the SOCKS5 proxy server+ -- | Port number of the SOCKS5 proxy server. proxyPort :: ServiceName,- -- | Username and password for UserPass authentication, `Nothing` will then use `NoAuth`+ -- | Credentials for authentication. 'Nothing' defaults to the 'NoAuth' method.+ -- Otherwise 'UserPass' will be used with the provided value. userPass :: Maybe (LT.Text, LT.Text) } type StateIO a = StateT B.ByteString IO a --- | Run a TCP connection to target address and port through proxy.+-- | Establishes a TCP connection to a target through a SOCKS5 proxy using the CONNECT command. runTCPConnect ::- -- | Destination address- Address ->- -- | Destination port- ServiceName ->- -- | Proxy configuration+ -- | Target hostname or IP address.+ HostName ->+ -- | Target port.+ PortNumber ->+ -- | SOCKS5 proxy configuration. ClientConfig ->- -- | Client function to run with the connected socket+ -- | The action to run with the connected 'Socket'. (Socket -> IO a) -> IO a-runTCPConnect destAddr destPort config client = do+runTCPConnect destHost destPort config client = do runTCPClient (proxyHost config) (proxyPort config) $ \sock -> do- startConnect destAddr destPort config sock client+ execConnect destHost destPort config sock+ client sock --- | Run a TCP connection to target address and port through proxy with TLS.+-- | Establishes a TLS-secured TCP connection to a target through a SOCKS5 proxy using the CONNECT command. runTCPConnectTLS ::- -- | Destination address- Address ->- -- | Destination port- ServiceName ->- -- | Proxy configuration+ HostName ->+ PortNumber -> ClientConfig ->- -- | TLS parameters for the connection+ -- | TLS parameters for the connection. ClientParams ->- -- | Client function to run with the connected TLS context+ -- | The action to run with the TLS 'Context'. (Context -> IO a) -> IO a-runTCPConnectTLS destAddr destPort config params client = do+runTCPConnectTLS destHost destPort config params client = do runTCPClient (proxyHost config) (proxyPort config) $ \sock -> do ctx <- contextNew sock params handshake ctx- res <- startConnect destAddr destPort config ctx client+ execConnect destHost destPort config ctx+ res <- client ctx bye ctx return res -startConnect ::+-- | Executes the CONNECT command on an established connection to the proxy.+-- This function only performs the SOCKS5 negotiation.+execConnect :: (Connection c) =>- Address ->- ServiceName ->+ -- | Target hostname or IP address.+ HostName ->+ -- | Target port.+ PortNumber ->+ -- | SOCKS5 proxy configuration. ClientConfig ->+ -- | An existing connection to the proxy (e.g., a 'Socket' or 'Context'). c ->- (c -> IO a) ->- IO a-startConnect destAddr destPort config conn client =+ IO ()+execConnect destHost destPort config conn = flip evalStateT B.empty $ do performAuth config conn- portNum <- resolvePort destPort- encodeAndSend conn $ Request Connect destAddr portNum+ let destAddr = hostToAddress destHost+ encodeAndSend conn $ Request Connect destAddr destPort reply <- recv' conn case reply of- Reply Succeeded _ _ -> liftIO $ client conn+ Reply Succeeded _ _ -> return () _ -> liftIO $ throwIO $ HandshakeFail reply --- | run a TCP connection to target by making proxy listen on a port and bind to it.+-- | Asks the SOCKS5 proxy to BIND to a port and waits for an incoming connection.+-- This is for protocols that require the client to accept a connection, like FTP. runTCPBind ::- -- | Destination address- Address ->- -- | Destination port- ServiceName ->- -- | Proxy configuration+ -- | The hostname or IP address the proxy should connect to (often ignored).+ HostName ->+ -- | The port the proxy should connect to (often ignored).+ PortNumber -> ClientConfig ->- -- | Function to call when the bind is successful, receives the proxy's listening address and port- (Address -> PortNumber -> IO ()) ->- -- | Client function to run with the bound socket, receives the connected peer's address and port- (Address -> PortNumber -> Socket -> IO a) ->+ -- | An action called after the proxy successfully binds, providing the listening address and port.+ (HostName -> PortNumber -> IO ()) ->+ -- | The action to run with the 'Socket' once a peer connects, providing the peer's address and port.+ (HostName -> PortNumber -> Socket -> IO a) -> IO a-runTCPBind destAddr destPort config notify client = do+runTCPBind destHost destPort config notify client = do runTCPClient (proxyHost config) (proxyPort config) $ \sock -> do- startBind destAddr destPort config sock notify client+ (hostAddr, hostPort) <- execBind destHost destPort config sock notify+ client hostAddr hostPort sock --- | run a TCP connection to target by making proxy listen on a port and bind to it with TLS.+-- | Asks the SOCKS5 proxy to BIND over a TLS-secured connection. runTCPBindTLS ::- -- | Destination address- Address ->- -- | Destination port- ServiceName ->- -- | Proxy configuration+ HostName ->+ PortNumber -> ClientConfig ->- -- | TLS parameters for the connection ClientParams ->- -- | Function to call when the bind is successful, receives the proxy's listening address and port- (Address -> PortNumber -> IO ()) ->- -- | Client function to run with the bound TLS context, receives the connected peer's address and port- (Address -> PortNumber -> Context -> IO a) ->+ -- | An action called after the proxy successfully binds, providing the listening address and port.+ (HostName -> PortNumber -> IO ()) ->+ -- | The action to run with the 'Context' once a peer connects, providing the peer's address and port.+ (HostName -> PortNumber -> Context -> IO a) -> IO a-runTCPBindTLS destAddr destPort config params notify client = do+runTCPBindTLS destHost destPort config params notify client = do runTCPClient (proxyHost config) (proxyPort config) $ \sock -> do ctx <- contextNew sock params handshake ctx- res <- startBind destAddr destPort config ctx notify client+ (hostAddr, hostPort) <- execBind destHost destPort config ctx notify+ res <- client hostAddr hostPort ctx bye ctx return res -startBind ::+-- | Executes the BIND command on an established connection.+-- This function handles the two-stage reply from the proxy.+execBind :: (Connection c) =>- Address ->- ServiceName ->+ -- | The hostname or IP address the proxy should connect to (often ignored).+ HostName ->+ -- | The port the proxy should connect to (often ignored).+ PortNumber ->+ -- | SOCKS5 proxy configuration. ClientConfig ->+ -- | An existing connection to the proxy (e.g., a 'Socket' or 'Context'). c ->- (Address -> PortNumber -> IO ()) ->- (Address -> PortNumber -> c -> IO a) ->- IO a-startBind destAddr destPort config conn notify client =+ -- | An action called after the proxy successfully binds.+ (HostName -> PortNumber -> IO ()) ->+ -- | Returns the hostname/IP and port of the connecting peer.+ IO (HostName, PortNumber)+execBind destHost destPort config conn notify = flip evalStateT B.empty $ do performAuth config conn- portNum <- resolvePort destPort- encodeAndSend conn $ Request Bind destAddr portNum+ let destAddr = hostToAddress destHost+ encodeAndSend conn $ Request Bind destAddr destPort reply1 <- recv' conn case reply1 of Reply Succeeded listenAddr listenPort -> do- liftIO $ notify listenAddr listenPort+ liftIO $ notify (show listenAddr) listenPort reply2 <- recv' conn case reply2 of Reply Succeeded hostAddr hostPort ->- liftIO $ client hostAddr hostPort conn+ return (show hostAddr, hostPort) _ -> liftIO $ throwIO $ HandshakeFail reply2 _ -> liftIO $ throwIO $ HandshakeFail reply1 --- | Start a UDP relay on proxy server and run a client function with the connected UDP socket.+-- | Establishes a UDP ASSOCIATION with the SOCKS5 proxy, enabling UDP traffic to be relayed. runUDPAssociate ::- -- | Proxy configuration ClientConfig ->- -- | Client function to run with the connected UDP socket, receives a function to send data and a function to receive data+ -- | Action to run with the connected UDP socket, providing function to send and receive datagrams. ((B.ByteString -> SockAddr -> IO ()) -> IO (B.ByteString, SockAddr) -> IO a) -> IO a runUDPAssociate config client = do runTCPClient (proxyHost config) (proxyPort config) $ \sockTCP -> do- startUDPAssociate config sockTCP client+ (relayHost, relayPort) <- execUDPAssociate config sockTCP+ runUDPClient relayHost (show relayPort) $ \sockUDP proxySockAddr -> do+ let sendDataTo :: B.ByteString -> SockAddr -> IO ()+ sendDataTo userData destination = do+ let (dstAddr, dstPort) = fromSockAddr_ destination+ sendUDPRequestTo sockUDP 0 dstAddr dstPort (B.fromStrict userData) proxySockAddr+ recvDataFrom :: IO (B.ByteString, SockAddr)+ recvDataFrom = do+ (datagram, _) <- SB.recvFrom sockUDP 8192+ let udpReply = decode (B.fromStrict datagram) :: UDPRequest+ originalSender = toSockAddr_ (address udpReply) (port udpReply)+ return (B.toStrict (payload udpReply), originalSender)+ client sendDataTo recvDataFrom --- | Start a UDP relay on proxy server with TLS and run a client function with the connected UDP socket. Note that the UDP socket is not encrypted, only the initial connection to the proxy is secured.+-- | Establishes a UDP association over a TLS-secured control connection.+-- Note: The control connection to the proxy is secured with TLS, but the resulting+-- UDP traffic is not encrypted. runUDPAssociateTLS ::- -- | Proxy configuration ClientConfig ->- -- | TLS parameters for the connection ClientParams ->- -- | Client function to run with the connected UDP socket, receives a function to send data and a function to receive data+ -- | Action to run with the connected UDP socket, providing function to send and receive datagrams. ((B.ByteString -> SockAddr -> IO ()) -> IO (B.ByteString, SockAddr) -> IO a) -> IO a runUDPAssociateTLS config params client = do runTCPClient (proxyHost config) (proxyPort config) $ \sockTCP -> do ctx <- contextNew sockTCP params handshake ctx- res <- startUDPAssociate config ctx client+ (relayHost, relayPort) <- execUDPAssociate config ctx+ res <- runUDPClient relayHost (show relayPort) $ \sockUDP proxySockAddr -> do+ let sendDataTo :: B.ByteString -> SockAddr -> IO ()+ sendDataTo userData destination = do+ let (dstAddr, dstPort) = fromSockAddr_ destination+ sendUDPRequestTo sockUDP 0 dstAddr dstPort (B.fromStrict userData) proxySockAddr+ recvDataFrom :: IO (B.ByteString, SockAddr)+ recvDataFrom = do+ (datagram, _) <- SB.recvFrom sockUDP 8192+ let udpReply = decode (B.fromStrict datagram) :: UDPRequest+ originalSender = toSockAddr_ (address udpReply) (port udpReply)+ return (B.toStrict (payload udpReply), originalSender)+ client sendDataTo recvDataFrom bye ctx return res -startUDPAssociate ::+-- | Executes the UDP ASSOCIATE command on an established connection.+execUDPAssociate :: (Connection c) => ClientConfig -> c ->- ((B.ByteString -> SockAddr -> IO ()) -> IO (B.ByteString, SockAddr) -> IO a) ->- IO a-startUDPAssociate config conn client =+ -- | Returns the address and port of the UDP relay on the proxy server.+ IO (HostName, PortNumber)+execUDPAssociate config conn = flip evalStateT B.empty $ do performAuth config conn encodeAndSend conn $ Request UDPAssociate (AddressIPv4 "0.0.0.0") 0 reply <- recv' conn case reply of- Reply Succeeded relayAddr relayPort -> liftIO $ do- runUDPClient (show relayAddr) (show relayPort) $ \sockUDP proxySockAddr -> do- let sendDataTo :: B.ByteString -> SockAddr -> IO ()- sendDataTo userData destination = do- let (dstAddr, dstPort) = fromSockAddr_ destination- sendUDPRequestTo sockUDP 0 dstAddr dstPort (B.fromStrict userData) proxySockAddr- recvDataFrom :: IO (B.ByteString, SockAddr)- recvDataFrom = do- (datagram, _) <- SB.recvFrom sockUDP 8192- let udpReply = decode (B.fromStrict datagram) :: UDPRequest- originalSender = toSockAddr_ (address udpReply) (port udpReply)- return (B.toStrict (payload udpReply), originalSender)- client sendDataTo recvDataFrom+ Reply Succeeded relayAddr relayPort -> return (show relayAddr, relayPort) _ -> liftIO $ throwIO $ HandshakeFail reply performAuth :: (Connection c) => ClientConfig -> c -> StateIO ()@@ -260,11 +296,11 @@ _ -> liftIO $ throwIO AuthMissingCredentials _ -> liftIO $ throwIO $ AuthUnsupported (method selectedMethod) -resolvePort :: ServiceName -> StateIO PortNumber-resolvePort port = liftIO $- case readMaybe port of- Just p -> return p- Nothing -> throwIO $ PortStringInvalid port+hostToAddress :: HostName -> Address+hostToAddress host = case readMaybe host :: Maybe IP of+ Just (IPv4 ipv4) -> AddressIPv4 ipv4+ Just (IPv6 ipv6) -> AddressIPv6 ipv6+ Nothing -> AddressDomain (LT.pack host) recv' :: (Binary b, Connection c) => c -> StateIO b recv' conn = do
Network/SOCKS5/Internal.hs view
@@ -1,17 +1,21 @@ module Network.SOCKS5.Internal- ( Hello (..),+ ( Method (..),+ Hello (..), MethodSelection (..), Command (..),+ Address (..), Request (..), Rep (..), Reply (..), UDPRequest (..), sendUDPRequestTo, UserPassRequest (..),- UserPassResponse (..), Status (..),+ UserPassResponse (..), SOCKSException (..), Connection (..),+ fromSockAddr_,+ toSockAddr_, recvAndDecode, encodeAndSend, )@@ -25,9 +29,9 @@ import Data.Binary.Put import Data.ByteString qualified as B import Data.ByteString.Lazy qualified as LB+import Data.IP import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Encoding qualified as LTE-import Network.SOCKS5.Types import Network.Socket import Network.Socket.ByteString qualified as SB import Network.Socket.ByteString.Lazy qualified as LSB@@ -42,6 +46,46 @@ -- | 1 | 1 | 1 to 255 | -- +-----+----------+----------+ +-- The values currently defined for METHOD are:+-- X'00': NO AUTHENTICATION REQUIRED+-- X'01': GSSAPI+-- X'02': USERNAME/PASSWORD+-- X'03': to X'7F' IANA ASSIGNED+-- X'80': to X'FE' RESERVED FOR PRIVATE METHODS+-- X'FF': NO ACCEPTABLE METHODS++data Method+ = NoAuth+ | GSSAPI+ | UserPass+ | IANAAssigned Word8+ | PrivateMethod Word8+ | NoAcceptableMethods+ | UnknownMethod Word8+ deriving (Eq, Show)++instance Binary Method where+ put :: Method -> Put+ put NoAuth = putWord8 0x00+ put GSSAPI = putWord8 0x01+ put UserPass = putWord8 0x02+ put (IANAAssigned m) = putWord8 m+ put (PrivateMethod m) = putWord8 m+ put NoAcceptableMethods = putWord8 0xFF+ put (UnknownMethod m) = putWord8 m++ get :: Get Method+ get = do+ m <- getWord8+ case m of+ 0x00 -> return NoAuth+ 0x01 -> return GSSAPI+ 0x02 -> return UserPass+ _ | m >= 0x03 && m <= 0x7F -> return $ IANAAssigned m+ _ | m >= 0x80 && m <= 0xFE -> return $ PrivateMethod m+ 0xFF -> return NoAcceptableMethods+ _ -> return $ UnknownMethod m+ newtype Hello = Hello { methods :: [Method] }@@ -126,6 +170,62 @@ 0x03 -> return UDPAssociate _ -> fail $ "Unknown command: " ++ show cmd +-- | Address type used for destination addresses in SOCKS5 protocol.+data Address+ = AddressIPv4 IPv4+ | AddressIPv6 IPv6+ | AddressDomain LT.Text+ deriving (Eq)++instance Show Address where+ show :: Address -> String+ show (AddressIPv4 ip) = show ip+ show (AddressIPv6 ip) = show ip+ show (AddressDomain name) = LT.unpack name++instance Binary Address where+ put :: Address -> Put+ put (AddressIPv4 ip) = do+ putWord8 0x01+ putWord32be $ fromIPv4w ip+ put (AddressIPv6 ip) = do+ putWord8 0x04+ let (a1, a2, a3, a4) = fromIPv6w ip+ in mapM_ putWord32be [a1, a2, a3, a4]+ put (AddressDomain name) = do+ putWord8 0x03+ let encodedName = LTE.encodeUtf8 name+ byteLength = LB.length encodedName+ if byteLength > fromIntegral (maxBound :: Word8)+ then+ error $+ "DomainName (UTF-8 encoded) has length of "+ ++ show byteLength+ ++ " bytes. Max allowed is "+ ++ show (maxBound :: Word8)+ else putWord8 $ fromIntegral byteLength+ putLazyByteString encodedName++ get :: Get Address+ get = do+ addrType <- getWord8+ case addrType of+ 0x01 -> do+ AddressIPv4 . toIPv4w <$> getWord32be+ 0x04 -> do+ a1 <- getWord32be+ a2 <- getWord32be+ a3 <- getWord32be+ a4 <- getWord32be+ return $ AddressIPv6 $ toIPv6w (a1, a2, a3, a4)+ 0x03 -> do+ len <- getWord8+ encodedName <- getLazyByteString (fromIntegral len)+ case LTE.decodeUtf8' encodedName of+ Left err -> fail $ "Failed to decode UTF-8 domain name: " ++ show err+ Right name -> return $ AddressDomain name+ _ -> fail $ "Unknown address type: " ++ show addrType+ data Request = Request { command :: Command, destinationAddress :: Address,@@ -405,6 +505,16 @@ connSend :: Context -> LB.ByteString -> IO () connSend = sendData++fromSockAddr_ :: SockAddr -> (Address, PortNumber)+fromSockAddr_ (SockAddrInet port host) = (AddressIPv4 (fromHostAddress host), fromIntegral port)+fromSockAddr_ (SockAddrInet6 port _ host _) = (AddressIPv6 (toIPv6w host), fromIntegral port)+fromSockAddr_ path = error $ "Unexpected Unix socket address: " ++ show path++toSockAddr_ :: Address -> PortNumber -> SockAddr+toSockAddr_ (AddressIPv4 host) port = SockAddrInet (fromIntegral port) $ toHostAddress host+toSockAddr_ (AddressIPv6 host) port = SockAddrInet6 (fromIntegral port) 0 (toHostAddress6 host) 0+toSockAddr_ (AddressDomain host) _ = error $ "Address conversion for DomainName not implemented: " ++ show host recvAndDecode :: (Binary a, Connection c, MonadIO m) => c -> B.ByteString -> m (a, B.ByteString) recvAndDecode conn buffer = liftIO $ go $ pushChunk (runGetIncremental get) buffer
Network/SOCKS5/Server.hs view
@@ -1,14 +1,11 @@ {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE OverloadedStrings #-} --- | Functions to run a SOCKS5 server.+-- |+-- This module provides a server implementation for the SOCKS5 proxy protocol. module Network.SOCKS5.Server ( ServerConfig (..),-- -- * SOCKS5 Server runSOCKS5Server,-- -- * SOCKS5 Server with TLS runSOCKS5ServerTLS, ) where@@ -25,18 +22,20 @@ import Data.Text.Lazy qualified as LT import Network.Run.TCP import Network.SOCKS5.Internal-import Network.SOCKS5.Types import Network.Socket import Network.Socket.ByteString qualified as SB import Network.TLS import System.IO.Error +-- | Configuration for the SOCKS5 server. data ServerConfig = ServerConfig- { -- | Hostname or IP address to bind the server to+ { -- | Hostname or IP address to bind the server to. serverHost :: HostName,- -- | Port number to listen on+ -- | Port number to listen on. serverPort :: ServiceName, -- | List of User/Password pairs for UserPass authentication+ -- If empty, only 'NoAuth' method will be available.+ -- Otherwise, 'UserPass' authentication is required. users :: [(LT.Text, LT.Text)] } @@ -54,8 +53,12 @@ handle (\(e :: SomeException) -> putStrLn $ show clientAddr ++ ": " ++ show e) $ newClient config sockTCP sockTCP --- | Run a SOCKS5 server with TLS support.-runSOCKS5ServerTLS :: ServerConfig -> ServerParams -> IO ()+-- | Run a SOCKS5 server with the control channel secured by TLS.+runSOCKS5ServerTLS ::+ ServerConfig ->+ -- | TLS parameters for the server (e.g., certificate and key).+ ServerParams ->+ IO () runSOCKS5ServerTLS config params = do let host = serverHost config port = serverPort config
− Network/SOCKS5/Types.hs
@@ -1,123 +0,0 @@--- Types you may want to import.-module Network.SOCKS5.Types- ( Method (..),- Address (..),- fromSockAddr_,- toSockAddr_,- )-where--import Data.Binary-import Data.Binary.Get-import Data.Binary.Put-import Data.ByteString.Lazy qualified as LB-import Data.IP-import Data.Text.Lazy qualified as LT-import Data.Text.Lazy.Encoding qualified as LTE-import Network.Socket---- The values currently defined for METHOD are:--- X'00': NO AUTHENTICATION REQUIRED--- X'01': GSSAPI--- X'02': USERNAME/PASSWORD--- X'03': to X'7F' IANA ASSIGNED--- X'80': to X'FE' RESERVED FOR PRIVATE METHODS--- X'FF': NO ACCEPTABLE METHODS--data Method- = NoAuth- | GSSAPI- | UserPass- | IANAAssigned Word8- | PrivateMethod Word8- | NoAcceptableMethods- | UnknownMethod Word8- deriving (Eq, Show)--instance Binary Method where- put :: Method -> Put- put NoAuth = putWord8 0x00- put GSSAPI = putWord8 0x01- put UserPass = putWord8 0x02- put (IANAAssigned m) = putWord8 m- put (PrivateMethod m) = putWord8 m- put NoAcceptableMethods = putWord8 0xFF- put (UnknownMethod m) = putWord8 m-- get :: Get Method- get = do- m <- getWord8- case m of- 0x00 -> return NoAuth- 0x01 -> return GSSAPI- 0x02 -> return UserPass- _ | m >= 0x03 && m <= 0x7F -> return $ IANAAssigned m- _ | m >= 0x80 && m <= 0xFE -> return $ PrivateMethod m- 0xFF -> return NoAcceptableMethods- _ -> return $ UnknownMethod m---- | Address type used for destination addresses in SOCKS5 protocol.-data Address- = AddressIPv4 IPv4- | AddressIPv6 IPv6- | AddressDomain LT.Text- deriving (Eq)--instance Show Address where- show :: Address -> String- show (AddressIPv4 ip) = show ip- show (AddressIPv6 ip) = show ip- show (AddressDomain name) = LT.unpack name--instance Binary Address where- put :: Address -> Put- put (AddressIPv4 ip) = do- putWord8 0x01- putWord32be $ fromIPv4w ip- put (AddressIPv6 ip) = do- putWord8 0x04- let (a1, a2, a3, a4) = fromIPv6w ip- in mapM_ putWord32be [a1, a2, a3, a4]- put (AddressDomain name) = do- putWord8 0x03- let encodedName = LTE.encodeUtf8 name- byteLength = LB.length encodedName- if byteLength > fromIntegral (maxBound :: Word8)- then- error $- "DomainName (UTF-8 encoded) has length of "- ++ show byteLength- ++ " bytes. Max allowed is "- ++ show (maxBound :: Word8)- else putWord8 $ fromIntegral byteLength- putLazyByteString encodedName-- get :: Get Address- get = do- addrType <- getWord8- case addrType of- 0x01 -> do- AddressIPv4 . toIPv4w <$> getWord32be- 0x04 -> do- a1 <- getWord32be- a2 <- getWord32be- a3 <- getWord32be- a4 <- getWord32be- return $ AddressIPv6 $ toIPv6w (a1, a2, a3, a4)- 0x03 -> do- len <- getWord8- encodedName <- getLazyByteString (fromIntegral len)- case LTE.decodeUtf8' encodedName of- Left err -> fail $ "Failed to decode UTF-8 domain name: " ++ show err- Right name -> return $ AddressDomain name- _ -> fail $ "Unknown address type: " ++ show addrType--fromSockAddr_ :: SockAddr -> (Address, PortNumber)-fromSockAddr_ (SockAddrInet port host) = (AddressIPv4 (fromHostAddress host), fromIntegral port)-fromSockAddr_ (SockAddrInet6 port _ host _) = (AddressIPv6 (toIPv6w host), fromIntegral port)-fromSockAddr_ path = error $ "Unexpected Unix socket address: " ++ show path--toSockAddr_ :: Address -> PortNumber -> SockAddr-toSockAddr_ (AddressIPv4 host) port = SockAddrInet (fromIntegral port) $ toHostAddress host-toSockAddr_ (AddressIPv6 host) port = SockAddrInet6 (fromIntegral port) 0 (toHostAddress6 host) 0-toSockAddr_ (AddressDomain host) _ = error $ "Address conversion for DomainName not implemented: " ++ show host
example/Main.hs view
@@ -2,7 +2,6 @@ import qualified Data.ByteString.Char8 as C8 import Network.SOCKS5.Client-import Network.SOCKS5.Types import Network.Socket.ByteString (recv, sendAll) main :: IO ()@@ -13,10 +12,8 @@ proxyPort = "11451", userPass = Nothing }- let destAddr = AddressDomain "example.com"- let destPort = "80" - runTCPConnect destAddr destPort proxyConfig $ \sock -> do+ runTCPConnect "example.com" 80 proxyConfig $ \sock -> do putStrLn "Connected to example.com through SOCKS5 proxy!" sendAll sock "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n" response <- recv sock 4096
socks5.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: socks5-version: 0.6.0.1+version: 0.8.0.0 synopsis: A SOCKS5 (RFC 1928) implementation description: This package provides a library with simple functions to create SOCKS5 clients and servers, along with a standalone server executable.@@ -30,7 +30,6 @@ Network.SOCKS5.Client Network.SOCKS5.Internal Network.SOCKS5.Server- Network.SOCKS5.Types build-depends: async ^>=2.2,
tests/Spec.hs view
@@ -10,7 +10,6 @@ import Network.Run.TCP import Network.SOCKS5.Client import Network.SOCKS5.Server-import Network.SOCKS5.Types import Network.Socket import Network.Socket.ByteString import Network.TLS@@ -22,8 +21,8 @@ aroundAll withSOCKS5Server $ do describe "Network.SOCKS5.Client" $ do it "should run TCP connect successfully to ipinfo.io" $ \proxyConfig -> do- let destAddr = AddressDomain "ipinfo.io"- destPort = "80"+ let destAddr = "ipinfo.io"+ destPort = 80 runTCPConnect destAddr destPort proxyConfig $ \sock -> do let request = C8.pack "GET / HTTP/1.1\r\nHost: ipinfo.io\r\nConnection: close\r\n\r\n" putStrLn "Sending HTTP GET request..."@@ -37,8 +36,8 @@ response `shouldSatisfy` ("HTTP/1.1 200 OK" `B.isPrefixOf`) it "should run BIND command successfully" $ \proxyConfig -> do- let destAddr = AddressIPv4 "8.8.8.8"- destPort = "80"+ let destAddr = "8.8.8.8"+ destPort = 80 runTCPBind destAddr destPort@@ -47,7 +46,7 @@ void $ async $ do threadDelay 200000 putStrLn $ "Third-party connector attempting to connect to " ++ show boundAddr ++ ":" ++ show boundPort- runTCPClient (show boundAddr) (show boundPort) $ \remoteSock -> do+ runTCPClient boundAddr (show boundPort) $ \remoteSock -> do putStrLn "Third-party connector: connection successful." forever $ sendAll remoteSock "PING" )@@ -79,8 +78,8 @@ aroundAll withSOCKS5ServerTLS $ do describe "Network.SOCKS5.Client" $ do it "should run TCP connect successfully to ipinfo.io with TLS" $ \(proxyConfig, clientParams) -> do- let destAddr = AddressDomain "ipinfo.io"- destPort = "80"+ let destAddr = "ipinfo.io"+ destPort = 80 runTCPConnectTLS destAddr destPort proxyConfig clientParams $ \ctx -> do let request = C8.pack "GET / HTTP/1.1\r\nHost: ipinfo.io\r\nConnection: close\r\n\r\n" putStrLn "Sending HTTP GET request..."@@ -94,8 +93,8 @@ response `shouldSatisfy` ("HTTP/1.1 200 OK" `B.isPrefixOf`) it "should run BIND command successfully with TLS" $ \(proxyConfig, clientParams) -> do- let destAddr = AddressIPv4 "8.8.8.8"- destPort = "80"+ let destAddr = "8.8.8.8"+ destPort = 80 runTCPBindTLS destAddr destPort@@ -105,7 +104,7 @@ void $ async $ do threadDelay 200000 putStrLn $ "Third-party connector attempting to connect to " ++ show boundAddr ++ ":" ++ show boundPort- runTCPClient (show boundAddr) (show boundPort) $ \remoteSock -> do+ runTCPClient boundAddr (show boundPort) $ \remoteSock -> do putStrLn "Third-party connector: connection successful." sendAll remoteSock "PING" )