diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,25 @@
+# Revision history for socks5
+
+## 0.1.0.0 -- 2025-06-30
+
+- First version. Released on an unsuspecting world.
+
+## 0.2.0.0 -- 2025-07-06
+
+- TLS version API and server executable's CLI args added.
+
+## 0.3.0.0 -- 2025-07-07
+
+- Adjusted the order of argument for client side API.
+
+## 0.4.0.0 -- 2025-07-07
+
+- UserPass authentication added, adjusted server API and CLI.
+
+## 0.5.0.0 -- 2025-07-07
+
+- Moved some types to separate file. Haddock documentation added. Default server port changed to 1080.
+
+## 0.6.0.0 -- 2025-07-08
+
+- Removed a record from client config. Rename test directory to tests. Added tests for TLS and UserPass authentication.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+Copyright (c) 2025, Shi Rao
+
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of the copyright holder nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Network/SOCKS5/Client.hs b/Network/SOCKS5/Client.hs
new file mode 100644
--- /dev/null
+++ b/Network/SOCKS5/Client.hs
@@ -0,0 +1,274 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Functions to start connection using SOCKS5 proxy.
+--
+-- Here is an 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 ()
+-- > main = do
+-- >   let proxyConfig =
+-- >         ClientConfig
+-- >           { proxyHost = "127.0.0.1",
+-- >             proxyPort = "11451",
+-- >             userPass = Nothing
+-- >           }
+-- >   let destAddr = AddressDomain "example.com"
+-- >   let destPort = "80"
+-- >
+-- >   runTCPConnect destAddr destPort 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
+-- >     C8.putStrLn response
+module Network.SOCKS5.Client
+  ( ClientConfig (..),
+
+    -- * SOCKS5
+    runTCPConnect,
+    runTCPBind,
+    runUDPAssociate,
+
+    -- * SOCKS5 with TLS
+    runTCPConnectTLS,
+    runTCPBindTLS,
+    runUDPAssociateTLS,
+  )
+where
+
+import Control.Exception
+import Control.Monad.State
+import Data.Binary (Binary, decode)
+import Data.ByteString qualified as B
+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)
+
+data ClientConfig = ClientConfig
+  { -- | Hostname or IP address of the SOCKS5 proxy server
+    proxyHost :: HostName,
+    -- | Port number of the SOCKS5 proxy server
+    proxyPort :: ServiceName,
+    -- | Username and password for UserPass authentication, `Nothing` will then use `NoAuth`
+    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.
+runTCPConnect ::
+  -- | Destination address
+  Address ->
+  -- | Destination port
+  ServiceName ->
+  -- | Proxy configuration
+  ClientConfig ->
+  -- | Client function to run with the connected socket
+  (Socket -> IO a) ->
+  IO a
+runTCPConnect destAddr destPort config client = do
+  runTCPClient (proxyHost config) (proxyPort config) $ \sock -> do
+    startConnect destAddr destPort config sock client
+
+-- | Run a TCP connection to target address and port through proxy with TLS.
+runTCPConnectTLS ::
+  -- | Destination address
+  Address ->
+  -- | Destination port
+  ServiceName ->
+  -- | Proxy configuration
+  ClientConfig ->
+  -- | TLS parameters for the connection
+  ClientParams ->
+  -- | Client function to run with the connected TLS context
+  (Context -> IO a) ->
+  IO a
+runTCPConnectTLS destAddr 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
+    bye ctx
+    return res
+
+startConnect ::
+  (Connection c) =>
+  Address ->
+  ServiceName ->
+  ClientConfig ->
+  c ->
+  (c -> IO a) ->
+  IO a
+startConnect destAddr destPort config conn client =
+  flip evalStateT B.empty $ do
+    performAuth config conn
+    portNum <- resolvePort destPort
+    encodeAndSend conn $ Request Connect destAddr portNum
+    reply <- recv' conn
+    case reply of
+      Reply Succeeded _ _ -> liftIO $ client conn
+      _ -> liftIO $ throwIO $ HandshakeFail reply
+
+-- | run a TCP connection to target by making proxy listen on a port and bind to it.
+runTCPBind ::
+  -- | Destination address
+  Address ->
+  -- | Destination port
+  ServiceName ->
+  -- | Proxy configuration
+  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) ->
+  IO a
+runTCPBind destAddr destPort config notify client = do
+  runTCPClient (proxyHost config) (proxyPort config) $ \sock -> do
+    startBind destAddr destPort config sock notify client
+
+-- | run a TCP connection to target by making proxy listen on a port and bind to it with TLS.
+runTCPBindTLS ::
+  -- | Destination address
+  Address ->
+  -- | Destination port
+  ServiceName ->
+  -- | Proxy configuration
+  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) ->
+  IO a
+runTCPBindTLS destAddr 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
+    bye ctx
+    return res
+
+startBind ::
+  (Connection c) =>
+  Address ->
+  ServiceName ->
+  ClientConfig ->
+  c ->
+  (Address -> PortNumber -> IO ()) ->
+  (Address -> PortNumber -> c -> IO a) ->
+  IO a
+startBind destAddr destPort config conn notify client =
+  flip evalStateT B.empty $ do
+    performAuth config conn
+    portNum <- resolvePort destPort
+    encodeAndSend conn $ Request Bind destAddr portNum
+    reply1 <- recv' conn
+    case reply1 of
+      Reply Succeeded listenAddr listenPort -> do
+        liftIO $ notify listenAddr listenPort
+        reply2 <- recv' conn
+        case reply2 of
+          Reply Succeeded hostAddr hostPort ->
+            liftIO $ client hostAddr hostPort conn
+          _ -> 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.
+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
+  ((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
+
+-- | 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.
+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
+  ((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
+    bye ctx
+    return res
+
+startUDPAssociate ::
+  (Connection c) =>
+  ClientConfig ->
+  c ->
+  ((B.ByteString -> SockAddr -> IO ()) -> IO (B.ByteString, SockAddr) -> IO a) ->
+  IO a
+startUDPAssociate config conn client =
+  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
+      _ -> liftIO $ throwIO $ HandshakeFail reply
+
+performAuth :: (Connection c) => ClientConfig -> c -> StateIO ()
+performAuth config conn = do
+  case userPass config of
+    Nothing -> encodeAndSend conn $ Hello [NoAuth]
+    Just _ -> encodeAndSend conn $ Hello [NoAuth, UserPass]
+  selectedMethod <- recv' conn
+  case method selectedMethod of
+    NoAuth -> return ()
+    UserPass -> do
+      case userPass config of
+        Just (user, pass) -> do
+          encodeAndSend conn $ UserPassRequest user pass
+          response <- recv' conn
+          case response of
+            UserPassResponse Success -> return ()
+            UserPassResponse status -> liftIO $ throwIO $ AuthFailed status
+        _ -> 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
+
+recv' :: (Binary b, Connection c) => c -> StateIO b
+recv' conn = do
+  buffer <- get
+  (val, left) <- recvAndDecode conn buffer
+  put left
+  return val
diff --git a/Network/SOCKS5/Internal.hs b/Network/SOCKS5/Internal.hs
new file mode 100644
--- /dev/null
+++ b/Network/SOCKS5/Internal.hs
@@ -0,0 +1,423 @@
+module Network.SOCKS5.Internal
+  ( Hello (..),
+    MethodSelection (..),
+    Command (..),
+    Request (..),
+    Rep (..),
+    Reply (..),
+    UDPRequest (..),
+    sendUDPRequestTo,
+    UserPassRequest (..),
+    UserPassResponse (..),
+    Status (..),
+    SOCKSException (..),
+    Connection (..),
+    recvAndDecode,
+    encodeAndSend,
+  )
+where
+
+import Control.Exception
+import Control.Monad
+import Control.Monad.State (MonadIO (liftIO))
+import Data.Binary
+import Data.Binary.Get
+import Data.Binary.Put
+import Data.ByteString qualified as B
+import Data.ByteString.Lazy qualified as LB
+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
+import Network.TLS
+import Prelude hiding (getContents)
+
+--  The client connects to the server, and sends a version
+--  identifier/method selection message:
+--    +-----+----------+----------+
+--    | VER | NMETHODS | METHODS  |
+--    +-----+----------+----------+
+--    |  1  |    1     | 1 to 255 |
+--    +-----+----------+----------+
+
+newtype Hello = Hello
+  { methods :: [Method]
+  }
+  deriving (Eq, Show)
+
+instance Binary Hello where
+  put :: Hello -> Put
+  put (Hello methods) = do
+    putWord8 5
+    putWord8 (fromIntegral $ length methods)
+    mapM_ put methods
+
+  get :: Get Hello
+  get = do
+    void getWord8
+    nMethods <- getWord8
+    methods <- replicateM (fromIntegral nMethods) get
+    return $ Hello methods
+
+--  The server selects from one of the methods given in METHODS, and
+--  sends a METHOD selection message:
+--    +-----+--------+
+--    | VER | METHOD |
+--    +-----+--------+
+--    |  1  |   1    |
+--    +-----+--------+
+
+newtype MethodSelection = MethodSelection
+  { method :: Method
+  }
+  deriving (Eq, Show)
+
+instance Binary MethodSelection where
+  put :: MethodSelection -> Put
+  put (MethodSelection method) = do
+    putWord8 5
+    put method
+
+  get :: Get MethodSelection
+  get = do
+    void getWord8
+    MethodSelection <$> get
+
+--  The SOCKS request is formed as follows:
+--    +-----+-----+-------+------+----------+----------+
+--    | VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
+--    +-----+-----+-------+------+----------+----------+
+--    |  1  |  1  | X'00' |  1   | Variable |    2     |
+--    +-----+-----+-------+------+----------+----------+
+--  Where:
+--    VER: protocol version: X'05'
+--    CMD:
+--      CONNECT:       X'01'
+--      BIND:          X'02'
+--      UDP ASSOCIATE: X'03'
+--    RSV: RESERVED
+--    ATYP: address type of following address:
+--      IP V4 address: X'01'
+--      DOMAINNAME:    X'03'
+--      IP V6 address: X'04'
+--    DST.ADDR: desired destination address
+--    DST.PORT: desired destination port in network octet order
+
+data Command
+  = Connect
+  | Bind
+  | UDPAssociate
+  deriving (Eq, Show)
+
+instance Binary Command where
+  put :: Command -> Put
+  put Connect = putWord8 0x01
+  put Bind = putWord8 0x02
+  put UDPAssociate = putWord8 0x03
+
+  get :: Get Command
+  get = do
+    cmd <- getWord8
+    case cmd of
+      0x01 -> return Connect
+      0x02 -> return Bind
+      0x03 -> return UDPAssociate
+      _ -> fail $ "Unknown command: " ++ show cmd
+
+data Request = Request
+  { command :: Command,
+    destinationAddress :: Address,
+    destinationPort :: PortNumber
+  }
+  deriving (Eq, Show)
+
+instance Binary Request where
+  put :: Request -> Put
+  put (Request command destinationAddress destinationPort) = do
+    putWord8 0x05
+    put command
+    putWord8 0x00 -- RSV
+    put destinationAddress
+    putWord16be $ fromIntegral destinationPort
+
+  get :: Get Request
+  get = do
+    void getWord8
+    command <- get
+    void getWord8 -- RSV, ignored
+    address <- get
+    port <- getWord16be
+    let destinationPort = fromIntegral port
+    return $ Request command address destinationPort
+
+--  The server evaluates the request, and
+--  returns a reply formed as follows:
+--    +-----+-----+-------+------+----------+----------+
+--    | VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
+--    +-----+-----+-------+------+----------+----------+
+--    |  1  |  1  | X'00' |  1   | Variable |    2     |
+--    +-----+-----+-------+------+----------+----------+
+--  Where:
+--  VER: protocol version: X'05'
+--  REP: Reply field:
+--    X'00': succeeded
+--    X'01': general SOCKS server failure
+--    X'02': connection not allowed by ruleset
+--    X'03': Network unreachable
+--    X'04': Host unreachable
+--    X'05': Connection refused
+--    X'06': TTL expired
+--    X'07': Command not supported
+--    X'08': Address type not supported
+--    X'09': to X'FF' unassigned
+--  RSV: RESERVED
+--  ATYP: address type of following address
+--    IP V4 address: X'01'
+--    DOMAINNAME: X'03'
+--    IP V6 address: X'04'
+--  BND.ADDR: server bound address
+--  BND.PORT: server bound port in network octet order
+
+data Rep
+  = Succeeded
+  | GeneralSOCKSFailure
+  | ConnectionNotAllowedByRuleset
+  | NetworkUnreachable
+  | HostUnreachable
+  | ConnectionRefused
+  | TTLExpired
+  | CommandNotSupported
+  | AddressTypeNotSupported
+  | Unassigned Word8
+  deriving (Eq, Show)
+
+instance Binary Rep where
+  put :: Rep -> Put
+  put Succeeded = putWord8 0x00
+  put GeneralSOCKSFailure = putWord8 0x01
+  put ConnectionNotAllowedByRuleset = putWord8 0x02
+  put NetworkUnreachable = putWord8 0x03
+  put HostUnreachable = putWord8 0x04
+  put ConnectionRefused = putWord8 0x05
+  put TTLExpired = putWord8 0x06
+  put CommandNotSupported = putWord8 0x07
+  put AddressTypeNotSupported = putWord8 0x08
+  put (Unassigned m) = putWord8 m
+
+  get :: Get Rep
+  get = do
+    rep <- getWord8
+    case rep of
+      0x00 -> return Succeeded
+      0x01 -> return GeneralSOCKSFailure
+      0x02 -> return ConnectionNotAllowedByRuleset
+      0x03 -> return NetworkUnreachable
+      0x04 -> return HostUnreachable
+      0x05 -> return ConnectionRefused
+      0x06 -> return TTLExpired
+      0x07 -> return CommandNotSupported
+      0x08 -> return AddressTypeNotSupported
+      _ -> return $ Unassigned rep
+
+data Reply = Reply
+  { reply :: Rep,
+    boundAddress :: Address,
+    boundPort :: PortNumber
+  }
+  deriving (Eq, Show)
+
+instance Binary Reply where
+  put :: Reply -> Put
+  put (Reply reply bindAddress boundPort) = do
+    putWord8 0x05
+    put reply
+    putWord8 0x00 -- RSV
+    put bindAddress
+    putWord16be $ fromIntegral boundPort
+
+  get :: Get Reply
+  get = do
+    void getWord8
+    reply <- get
+    void getWord8 -- RSV, ignored
+    address <- get
+    port <- getWord16be
+    let boundPort = fromIntegral port
+    return $ Reply reply address boundPort
+
+--  Each UDP datagram carries a UDP request header with it:
+--    +-----+------+------+----------+----------+----------+
+--    | RSV | FRAG | ATYP | DST.ADDR | DST.PORT |   DATA   |
+--    +-----+------+------+----------+----------+----------+
+--    |  2  |  1   |  1   | Variable |    2     | Variable |
+--    +-----+------+------+----------+----------+----------+
+--  The fields in the UDP request header are:
+--    RSV: Reserved X'0000'
+--    FRAG: Current fragment number
+--    ATYP: address type of following addresses:
+--      IP V4 address: X'01'
+--      DOMAINNAME:    X'03'
+--      IP V6 address: X'04'
+--    DST.ADDR: desired destination address
+--    DST.PORT: desired destination port
+--    DATA: user data
+
+data UDPRequest = UDPRequest
+  { frag :: Word8,
+    address :: Address,
+    port :: PortNumber,
+    payload :: LB.ByteString
+  }
+  deriving (Eq, Show)
+
+instance Binary UDPRequest where
+  put :: UDPRequest -> Put
+  put (UDPRequest frag addr port p) = do
+    putWord16be 0x0000 -- RSV
+    putWord8 frag
+    put addr
+    putWord16be $ fromIntegral port
+    putLazyByteString p
+
+  get :: Get UDPRequest
+  get = do
+    void getWord16be -- RSV
+    frag <- getWord8
+    addr <- get
+    port <- getWord16be
+    let portNum = fromIntegral port
+    UDPRequest frag addr portNum <$> getRemainingLazyByteString
+
+sendUDPRequestTo :: Socket -> Word8 -> Address -> PortNumber -> LB.ByteString -> SockAddr -> IO ()
+sendUDPRequestTo sock frag addr port payload =
+  SB.sendAllTo sock $ LB.toStrict $ encode $ UDPRequest frag addr port payload
+
+--  Once the SOCKS V5 server has started, and the client has selected the
+--  Username/Password Authentication protocol, the Username/Password
+--  subnegotiation begins. This begins with the client producing a
+--  Username/Password request:
+--    +-----+------+----------+------+----------+
+--    | VER | ULEN |  UNAME   | PLEN |  PASSWD  |
+--    +-----+------+----------+------+----------+
+--    |  1  |  1   | 1 to 255 |  1   | 1 to 255 |
+--    +-----+------+----------+------+----------+
+
+data UserPassRequest = UserPassRequest
+  { username :: LT.Text,
+    password :: LT.Text
+  }
+  deriving (Eq, Show)
+
+instance Binary UserPassRequest where
+  put :: UserPassRequest -> Put
+  put (UserPassRequest uname passwd) = do
+    let encodedUname = LTE.encodeUtf8 uname
+        encodedPasswd = LTE.encodeUtf8 passwd
+        uLen = fromIntegral $ LB.length encodedUname
+        pLen = fromIntegral $ LB.length encodedPasswd
+    putWord8 0x01 -- VER
+    putWord8 uLen
+    putLazyByteString encodedUname
+    putWord8 pLen
+    putLazyByteString encodedPasswd
+
+  get :: Get UserPassRequest
+  get = do
+    void getWord8 -- VER, ignored
+    uLen <- getWord8
+    uname <- getLazyByteString (fromIntegral uLen)
+    pLen <- getWord8
+    passwd <- getLazyByteString (fromIntegral pLen)
+    case (LTE.decodeUtf8' uname, LTE.decodeUtf8' passwd) of
+      (Left err, _) -> fail $ "Failed to decode username: " ++ show err
+      (_, Left err) -> fail $ "Failed to decode password: " ++ show err
+      (Right u, Right p) -> return $ UserPassRequest u p
+
+--  The server verifies the supplied UNAME and PASSWD, and sends the
+--  following response:
+--    +-----+--------+
+--    | VER | STATUS |
+--    +-----+--------+
+--    |  1  |   1    |
+--    +-----+--------+
+
+data Status
+  = Success
+  | Failure
+  deriving (Eq, Show)
+
+instance Binary Status where
+  put :: Status -> Put
+  put Success = putWord8 0x00
+  put Failure = putWord8 0x01
+
+  get :: Get Status
+  get = do
+    status <- getWord8
+    case status of
+      0x00 -> return Success
+      _ -> return Failure
+
+newtype UserPassResponse = UserPassResponse
+  { status :: Status
+  }
+  deriving (Eq, Show)
+
+instance Binary UserPassResponse where
+  put :: UserPassResponse -> Put
+  put (UserPassResponse status) = do
+    putWord8 0x01 -- VER
+    put status
+
+  get :: Get UserPassResponse
+  get = do
+    void getWord8 -- VER, ignored
+    UserPassResponse <$> get
+
+data SOCKSException
+  = HandshakeFail Reply
+  | AuthUnsupported Method
+  | AuthFailed Status
+  | AuthMissingCredentials
+  | ConnectionToTargetFailed SomeException
+  | NoAcceptableAuthMethods
+  | PortStringInvalid ServiceName
+  deriving (Show)
+
+instance Exception SOCKSException
+
+class Connection c where
+  connRecv :: c -> IO B.ByteString
+  connSend :: c -> LB.ByteString -> IO ()
+
+instance Connection Socket where
+  connRecv :: Socket -> IO B.ByteString
+  connRecv sock = SB.recv sock 4096
+
+  connSend :: Socket -> LB.ByteString -> IO ()
+  connSend = LSB.sendAll
+
+instance Connection Context where
+  connRecv :: Context -> IO B.ByteString
+  connRecv = recvData
+
+  connSend :: Context -> LB.ByteString -> IO ()
+  connSend = sendData
+
+recvAndDecode :: (Binary a, Connection c, MonadIO m) => c -> B.ByteString -> m (a, B.ByteString)
+recvAndDecode conn buffer = liftIO $ go $ pushChunk (runGetIncremental get) buffer
+  where
+    go :: Decoder a -> IO (a, B.ByteString)
+    go (Done left _ val) = return (val, left)
+    go (Fail _ _ err) = throwIO $ userError $ "SOCKS5 parse error: " ++ err
+    go (Partial k) = do
+      chunk <- connRecv conn
+      if B.null chunk
+        then go (k Nothing)
+        else go (k (Just chunk))
+
+encodeAndSend :: (Binary a, Connection c, MonadIO m) => c -> a -> m ()
+encodeAndSend conn val = do
+  liftIO $ connSend conn $ encode val
diff --git a/Network/SOCKS5/Server.hs b/Network/SOCKS5/Server.hs
new file mode 100644
--- /dev/null
+++ b/Network/SOCKS5/Server.hs
@@ -0,0 +1,246 @@
+{-# LANGUAGE MultiWayIf #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Functions to run a SOCKS5 server.
+module Network.SOCKS5.Server
+  ( ServerConfig (..),
+
+    -- * SOCKS5 Server
+    runSOCKS5Server,
+
+    -- * SOCKS5 Server with TLS
+    runSOCKS5ServerTLS,
+  )
+where
+
+import Control.Concurrent.Async
+import Control.Exception
+import Control.Monad
+import Control.Monad.State
+import Data.Bifunctor
+import Data.Binary (Binary, decode)
+import Data.ByteString qualified as B
+import Data.IORef
+import Data.List.NonEmpty qualified as NE
+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
+
+data ServerConfig = ServerConfig
+  { -- | Hostname or IP address to bind the server to
+    serverHost :: HostName,
+    -- | Port number to listen on
+    serverPort :: ServiceName,
+    -- | List of User/Password pairs for UserPass authentication
+    users :: [(LT.Text, LT.Text)]
+  }
+
+type StateIO a = StateT B.ByteString IO a
+
+-- | Run a SOCKS5 server with the given configuration.
+runSOCKS5Server :: ServerConfig -> IO ()
+runSOCKS5Server config = do
+  let host = serverHost config
+      port = serverPort config
+  putStrLn $ "Starting SOCKS5 server on " ++ host ++ ":" ++ port
+  runTCPServer (Just host) port $ \sockTCP -> do
+    clientAddr <- getPeerName sockTCP
+    putStrLn $ "Accepted new client connection from " ++ show clientAddr
+    handle (\(e :: SomeException) -> putStrLn $ show clientAddr ++ ": " ++ show e) $
+      newClient config sockTCP sockTCP
+
+-- | Run a SOCKS5 server with TLS support.
+runSOCKS5ServerTLS :: ServerConfig -> ServerParams -> IO ()
+runSOCKS5ServerTLS config params = do
+  let host = serverHost config
+      port = serverPort config
+  putStrLn $ "Starting SOCKS5 server on " ++ host ++ ":" ++ port ++ " (TLS: on)"
+  runTCPServer (Just host) port $ \sockTCP -> do
+    clientAddr <- getPeerName sockTCP
+    putStrLn $ "Accepted new client connection from " ++ show clientAddr
+    handle (\(e :: SomeException) -> putStrLn $ show clientAddr ++ ": " ++ show e) $ do
+      ctx <- contextNew sockTCP params
+      handshake ctx
+      newClient config ctx sockTCP
+      bye ctx
+
+newClient :: (Connection c) => ServerConfig -> c -> Socket -> IO ()
+newClient config conn clientSock = do
+  void $ flip runStateT B.empty $ do
+    handleAuthentication config conn
+    req <- recv' conn
+    liftIO $ putStrLn $ "Received SOCKS5 Request: " ++ show req
+    case command req of
+      Connect -> liftIO $ handleConnect req conn clientSock
+      Bind -> liftIO $ handleBind req conn clientSock
+      UDPAssociate -> liftIO $ handleUDPAssociate req conn clientSock
+
+handleAuthentication :: (Connection c) => ServerConfig -> c -> StateIO ()
+handleAuthentication config conn = do
+  auths <- methods <$> recv' conn
+  let noUsers = null $ users config
+  selectedMethod <-
+    if
+      | NoAuth `elem` auths && noUsers -> do
+          encodeAndSend conn $ MethodSelection NoAuth
+          return NoAuth
+      | UserPass `elem` auths && not noUsers -> do
+          encodeAndSend conn $ MethodSelection UserPass
+          return UserPass
+      | otherwise -> liftIO $ throwIO NoAcceptableAuthMethods
+  when (selectedMethod == UserPass) $ do
+    UserPassRequest uname passwd <- recv' conn
+    let credentials = (uname, passwd)
+    if credentials `elem` users config
+      then encodeAndSend conn $ UserPassResponse Success
+      else do
+        encodeAndSend conn $ UserPassResponse Failure
+        liftIO $ throwIO $ AuthFailed Failure
+
+handleConnect :: (Connection c) => Request -> c -> Socket -> IO ()
+handleConnect (Request _ addr port) conn clientSock = do
+  handle (replyError conn) $
+    runTCPClient (show addr) (show port) $ \destSock -> do
+      putStrLn "Successfully connected to destination server."
+      (selfAddr, selfPort) <- fromSockAddr_ <$> getSocketName clientSock
+      encodeAndSend conn $ Reply Succeeded selfAddr selfPort
+      putStrLn $ "Sent SOCKS5 Reply: Succeeded, bound to " ++ show selfAddr ++ ":" ++ show selfPort
+      let forwardToDest = do
+            clientData <- connRecv conn
+            unless (B.null clientData) $ do
+              SB.sendAll destSock clientData
+              forwardToDest
+      let forwardToClient = do
+            serverData <- SB.recv destSock 4096
+            unless (B.null serverData) $ do
+              connSend conn $ B.fromStrict serverData
+              forwardToClient
+      concurrently_ forwardToDest forwardToClient
+      putStrLn "Data forwarding complete. Closing dest connection."
+
+handleBind :: (Connection c) => Request -> c -> Socket -> IO ()
+handleBind (Request _ _destAddr _destPort) conn clientSock =
+  handle (replyError conn) $
+    bracket (newTCPSocket "0.0.0.0") close $ \listenSock -> do
+      (selfAddr, _) <- fromSockAddr_ <$> getSocketName clientSock
+      (_, listenPort) <- fromSockAddr_ <$> getSocketName listenSock
+      putStrLn $ "Listening for connections on " ++ show selfAddr ++ ":" ++ show listenPort
+      encodeAndSend conn $ Reply Succeeded selfAddr listenPort
+      bracket (accept listenSock) (\(destSock, _) -> close destSock) $ \(destSock, destSockAddr) -> do
+        let (destAddr, destPort) = fromSockAddr_ destSockAddr
+        putStrLn $ "Accepted connection from " ++ show destAddr ++ ":" ++ show destPort
+        encodeAndSend conn $ Reply Succeeded destAddr destPort
+        let forwardToDest = do
+              clientData <- connRecv conn
+              unless (B.null clientData) $ do
+                SB.sendAll destSock clientData
+                forwardToDest
+        let forwardToClient = do
+              serverData <- SB.recv destSock 4096
+              unless (B.null serverData) $ do
+                connSend conn $ B.fromStrict serverData
+                forwardToClient
+        concurrently_ forwardToDest forwardToClient
+        putStrLn "Data forwarding complete. Closing dest connection."
+  where
+    newTCPSocket host = do
+      addrInfo <- head <$> getAddrInfo (Just defaultHints {addrFlags = [AI_PASSIVE], addrSocketType = Stream}) (Just host) (Just "0")
+      sock <- openSocket addrInfo
+      bind sock (addrAddress addrInfo)
+      listen sock 1
+      return sock
+
+-- The UDP relay server MUST acquire from the SOCKS server the expected
+-- IP address of the client that will send datagrams to the BND.PORT
+-- given in the reply to UDP ASSOCIATE.  It MUST drop any datagrams
+-- arriving from any source IP address other than the one recorded for
+-- the particular association.
+
+handleUDPAssociate :: (Connection c) => Request -> c -> Socket -> IO ()
+handleUDPAssociate (Request _ destAddr destPort) conn clientSock =
+  handle (replyError clientSock) $
+    bracket (newUDPSocket "0.0.0.0") close $ \relaySock -> do
+      (selfAddr, _) <- fromSockAddr_ <$> getSocketName clientSock
+      (_, relayPort) <- fromSockAddr_ <$> getSocketName relaySock
+      encodeAndSend conn $ Reply Succeeded selfAddr relayPort
+      (expectedClient, _) <- fromSockAddr_ <$> getPeerName clientSock
+      putStrLn $ "UDP association created for " ++ show expectedClient ++ ". Relaying on port " ++ show relayPort ++ "."
+      -- If the client is not in possesion of the information at the time of the UDP
+      -- ASSOCIATE, the client MUST use a port number and address of all zeros.
+      let expectedDest = case (destAddr, destPort) of
+            (AddressIPv4 "0.0.0.0", 0) -> Nothing
+            (AddressIPv6 "::", 0) -> Nothing
+            (AddressDomain _, _) -> Nothing
+            _ -> Just destAddr
+      -- A UDP association terminates when the TCP connection that the UDP ASSOCIATE request arrived on terminates.
+      withAsync (forwardUDP expectedClient expectedDest relaySock) $ \_ -> do
+        putStrLn "UDP association active. Waiting for TCP control connection to close."
+        waitForTCPClose clientSock
+      putStrLn "TCP control connection closed. Terminating UDP association."
+  where
+    newUDPSocket host = do
+      addrInfo <- head <$> getAddrInfo (Just defaultHints {addrFlags = [AI_PASSIVE], addrSocketType = Datagram}) (Just host) (Just "0")
+      sock <- openSocket addrInfo
+      bind sock (addrAddress addrInfo)
+      return sock
+    waitForTCPClose sock = do
+      chunk <- SB.recv sock 1024
+      unless (B.null chunk) $ waitForTCPClose sock
+
+forwardUDP :: Address -> Maybe Address -> Socket -> IO ()
+forwardUDP expectedClientAddr expectedDestAddr relaySock = do
+  putStrLn $ "Waiting UDP packets from client: " ++ show expectedClientAddr ++ " and dest: " ++ show expectedDestAddr
+  -- Use an IORef to store the client address once received, we don't know port yet
+  clientUDPSockAddr <- newIORef Nothing
+  forever $ do
+    (datagram, sourceUDPSockAddr) <- first B.fromStrict <$> SB.recvFrom relaySock 8192
+    let (sourceUDPAddr, _) = fromSockAddr_ sourceUDPSockAddr
+    -- sourceUDPAddr is used to compare only the address, not the port
+    mClientSockAddr <- readIORef clientUDPSockAddr
+    case mClientSockAddr of
+      Nothing ->
+        when (sourceUDPAddr == expectedClientAddr) $ do
+          putStrLn $ "Accepted first UDP packet from client at " ++ show sourceUDPSockAddr
+          writeIORef clientUDPSockAddr (Just sourceUDPSockAddr)
+          handleClientPacket datagram
+      Just clientSockAddr ->
+        if
+          | sourceUDPSockAddr == clientSockAddr -> handleClientPacket datagram
+          | isExpectedDest sourceUDPAddr -> handleRemotePacket datagram sourceUDPSockAddr clientSockAddr
+          | otherwise -> putStrLn $ "Received packet from unexpected source: " ++ show sourceUDPSockAddr
+  where
+    handleClientPacket datagram = do
+      let (UDPRequest _ addr port load) = decode datagram :: UDPRequest
+      destAddr <- NE.head <$> getAddrInfo (Just defaultHints {addrSocketType = Datagram}) (Just $ show addr) (Just $ show port)
+      SB.sendAllTo relaySock (B.toStrict load) (addrAddress destAddr)
+    isExpectedDest addr = case expectedDestAddr of
+      Nothing -> True
+      Just destAddr -> addr == destAddr
+    handleRemotePacket datagram sourceSockAddr clientSockAddr = do
+      let (remoteAddr, remotePort) = fromSockAddr_ sourceSockAddr
+      sendUDPRequestTo relaySock 0 remoteAddr remotePort datagram clientSockAddr
+
+recv' :: (Binary b, Connection c) => c -> StateIO b
+recv' conn = do
+  buffer <- get
+  (val, left) <- liftIO $ recvAndDecode conn buffer
+  put left
+  return val
+
+replyError :: (Connection c) => c -> SomeException -> IO ()
+replyError conn e = do
+  let rep = case fromException e of
+        Just (ioe :: IOException) ->
+          if
+            | isDoesNotExistError ioe -> HostUnreachable
+            | isPermissionError ioe -> ConnectionNotAllowedByRuleset
+            | otherwise -> GeneralSOCKSFailure
+        _ -> GeneralSOCKSFailure
+  putStrLn $ "Replying with error: " ++ show rep
+  encodeAndSend conn $ Reply rep (AddressIPv4 "0.0.0.0") 0
+  throwIO e
diff --git a/Network/SOCKS5/Types.hs b/Network/SOCKS5/Types.hs
new file mode 100644
--- /dev/null
+++ b/Network/SOCKS5/Types.hs
@@ -0,0 +1,123 @@
+-- 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
diff --git a/example/Main.hs b/example/Main.hs
new file mode 100644
--- /dev/null
+++ b/example/Main.hs
@@ -0,0 +1,23 @@
+{-# 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 ()
+main = do
+  let proxyConfig =
+        ClientConfig
+          { proxyHost = "127.0.0.1",
+            proxyPort = "11451",
+            userPass = Nothing
+          }
+  let destAddr = AddressDomain "example.com"
+  let destPort = "80"
+
+  runTCPConnect destAddr destPort 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
+    C8.putStrLn response
diff --git a/server/Main.hs b/server/Main.hs
new file mode 100644
--- /dev/null
+++ b/server/Main.hs
@@ -0,0 +1,105 @@
+import Data.Default
+import Data.Text.Lazy qualified as LT
+import Network.SOCKS5.Server
+import Network.TLS
+import Network.TLS.Extra.Cipher
+import Options.Applicative
+
+data Args = Args
+  { host :: String,
+    port :: String,
+    useTLS :: Bool,
+    cert :: FilePath,
+    key :: FilePath,
+    userPass :: [String]
+  }
+
+argsParser :: Parser Args
+argsParser =
+  Args
+    <$> strOption
+      ( long "host"
+          <> short 'H'
+          <> metavar "HOSTNAME"
+          <> value "0.0.0.0"
+          <> showDefault
+          <> help "The hostname to bind the server to."
+      )
+    <*> strOption
+      ( long "port"
+          <> short 'P'
+          <> metavar "PORT"
+          <> value "1080"
+          <> showDefault
+          <> help "The port to listen on."
+      )
+    <*> switch
+      ( long "tls"
+          <> help "Enable TLS encryption. Requires --cert and --key."
+      )
+    <*> strOption
+      ( long "cert"
+          <> metavar "CERT_FILE"
+          <> value "cert.pem"
+          <> showDefault
+          <> help "The TLS certificate file."
+      )
+    <*> strOption
+      ( long "key"
+          <> metavar "KEY_FILE"
+          <> value "key.pem"
+          <> showDefault
+          <> help "The TLS private key file."
+      )
+    <*> many
+      ( strOption
+          ( long "user-pass"
+              <> short 'u'
+              <> metavar "USERNAME:PASSWORD"
+              <> help "User/password pair for authentication. Can be specified multiple times."
+          )
+      )
+
+parseUserPass :: String -> (LT.Text, LT.Text)
+parseUserPass s = case break (== ':') s of
+  (user, ':' : pass) | not (null user) && not (null pass) -> (LT.pack user, LT.pack pass)
+  _ -> error $ "Invalid format for user-pass. Expected USERNAME:PASSWORD, but got: " ++ s
+
+main :: IO ()
+main = do
+  args <-
+    execParser $
+      info
+        (argsParser <**> helper)
+        ( fullDesc
+            <> progDesc "Run a SOCKS5 proxy server."
+            <> header "socks5-server - A simple SOCKS5 proxy"
+        )
+
+  let users = map parseUserPass (userPass args)
+  let serverConfig =
+        ServerConfig
+          { serverHost = host args,
+            serverPort = port args,
+            users = users
+          }
+  if useTLS args
+    then do
+      params <- credential (cert args) (key args)
+      runSOCKS5ServerTLS serverConfig params
+    else runSOCKS5Server serverConfig
+
+credential :: FilePath -> FilePath -> IO ServerParams
+credential cert key = do
+  cred <- either error id <$> credentialLoadX509 cert key
+  return
+    (defaultParamsServer :: ServerParams)
+      { serverShared =
+          def
+            { sharedCredentials = Credentials [cred]
+            },
+        serverSupported =
+          def
+            { supportedCiphers = ciphersuite_strong
+            }
+      }
diff --git a/socks5.cabal b/socks5.cabal
new file mode 100644
--- /dev/null
+++ b/socks5.cabal
@@ -0,0 +1,96 @@
+cabal-version: 3.0
+name: socks5
+version: 0.6.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.
+
+  It supports all standard SOCKS5 commands (CONNECT, BIND, and UDP ASSOCIATE), Username/Password authentication (RFC 1929), and TLS-secured connections.
+
+license: BSD-3-Clause
+license-file: LICENSE
+author: Shi Rao
+maintainer: github@homovetus.com
+category: Network
+build-type: Simple
+extra-doc-files: CHANGELOG.md
+
+source-repository head
+  type: git
+  location: https://github.com/homovetus/socks5
+
+common warnings
+  ghc-options:
+    -Wall
+    -threaded
+
+library
+  import: warnings
+  exposed-modules:
+    Network.SOCKS5.Client
+    Network.SOCKS5.Internal
+    Network.SOCKS5.Server
+    Network.SOCKS5.Types
+
+  build-depends:
+    async ^>=2.2,
+    base >=4.18 && <5,
+    binary ^>=0.8,
+    bytestring ^>=0.12,
+    iproute ^>=1.7,
+    mtl ^>=2.3,
+    network ^>=3.2,
+    network-run ^>=0.4,
+    text ^>=2.1,
+    tls ^>=2.1,
+
+  default-language: GHC2021
+
+executable socks5
+  import: warnings
+  main-is: Main.hs
+  build-depends:
+    base >=4.18 && <5,
+    data-default ^>=0.8,
+    optparse-applicative ^>=0.19,
+    socks5,
+    text ^>=2.1,
+    tls ^>=2.1,
+
+  hs-source-dirs: server
+  default-language: GHC2021
+
+executable socks5-example
+  import: warnings
+  main-is: Main.hs
+  build-depends:
+    base ^>=4.18,
+    bytestring ^>=0.12,
+    network ^>=3.2,
+    socks5,
+
+  hs-source-dirs: example
+  default-language: Haskell2010
+
+test-suite socks5-test
+  import: warnings
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  hs-source-dirs: tests
+  ghc-options:
+    -threaded
+    -rtsopts
+    -with-rtsopts=-N
+
+  build-depends:
+    async ^>=2.2,
+    base ^>=4.18,
+    bytestring ^>=0.12,
+    data-default ^>=0.8,
+    hspec ^>=2.11,
+    network ^>=3.2,
+    network-run ^>=0.4,
+    socks5,
+    tls ^>=2.1,
+
+  default-language: GHC2021
diff --git a/tests/Spec.hs b/tests/Spec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Spec.hs
@@ -0,0 +1,196 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+import Control.Concurrent
+import Control.Concurrent.Async
+import Control.Monad
+import Data.Bits
+import Data.ByteString qualified as B
+import Data.ByteString.Char8 qualified as C8
+import Data.Default
+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
+import Network.TLS.Extra.Cipher
+import Test.Hspec
+
+main :: IO ()
+main = hspec $ do
+  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"
+        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..."
+          sendAll sock request
+          response <- recv sock 4096
+          putStrLn "Received HTTP Response (first 4096 bytes):"
+          putStrLn "--------------------------"
+          C8.putStrLn response
+          putStrLn "--------------------------"
+          response `shouldSatisfy` (B.length |> (> 0))
+          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"
+        runTCPBind
+          destAddr
+          destPort
+          proxyConfig
+          ( \boundAddr boundPort -> do
+              void $ async $ do
+                threadDelay 200000
+                putStrLn $ "Third-party connector attempting to connect to " ++ show boundAddr ++ ":" ++ show boundPort
+                runTCPClient (show boundAddr) (show boundPort) $ \remoteSock -> do
+                  putStrLn "Third-party connector: connection successful."
+                  forever $ sendAll remoteSock "PING"
+          )
+          ( \_ _ sock -> do
+              putStrLn "SOCKS5 client: received connection, waiting for data..."
+              ping <- recv sock 4
+              putStrLn "SOCKS5 client: received data."
+              ping `shouldBe` "PING"
+          )
+
+      it "should run UDP associate successfully and perform a DNS query" $ \proxyConfig -> do
+        runUDPAssociate proxyConfig $ \sendUDP recvUDP -> do
+          let dnsServerAddr = SockAddrInet 53 (tupleToHostAddress (8, 8, 8, 8))
+          let dnsQuery =
+                B.pack [0x12, 0x34, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01]
+          putStrLn $ "Sending DNS query for 'google.com' to " ++ show dnsServerAddr ++ " via proxy..."
+          sendUDP dnsQuery dnsServerAddr
+          (response, from) <- recvUDP
+          putStrLn $ "Received " ++ show (B.length response) ++ " bytes from " ++ show from
+          response `shouldSatisfy` (B.length |> (> 0))
+          B.take 2 response `shouldBe` B.take 2 dnsQuery
+          when (B.length response >= 8) $ do
+            let flags = B.unpack $ B.take 2 $ B.drop 2 response
+                anCountBytes = B.unpack $ B.take 2 $ B.drop 6 response
+                answerCount = (fromIntegral (head anCountBytes) `shiftL` 8) + fromIntegral (last anCountBytes) :: Int
+            head flags `shouldSatisfy` (>= 0x80)
+            answerCount `shouldSatisfy` (> 0)
+
+  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"
+        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..."
+          sendData ctx $ C8.fromStrict request
+          response <- recvData ctx
+          putStrLn "Received HTTP Response:"
+          putStrLn "--------------------------"
+          C8.putStrLn response
+          putStrLn "--------------------------"
+          response `shouldSatisfy` (B.length |> (> 0))
+          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"
+        runTCPBindTLS
+          destAddr
+          destPort
+          proxyConfig
+          clientParams
+          ( \boundAddr boundPort -> do
+              void $ async $ do
+                threadDelay 200000
+                putStrLn $ "Third-party connector attempting to connect to " ++ show boundAddr ++ ":" ++ show boundPort
+                runTCPClient (show boundAddr) (show boundPort) $ \remoteSock -> do
+                  putStrLn "Third-party connector: connection successful."
+                  sendAll remoteSock "PING"
+          )
+          ( \_ _ ctx -> do
+              putStrLn "SOCKS5 client: received connection, waiting for data..."
+              ping <- recvData ctx
+              putStrLn "SOCKS5 client: received data."
+              ping `shouldBe` "PING"
+          )
+
+      it "should run UDP associate successfully and perform a DNS query with TLS" $ \(proxyConfig, clientParams) -> do
+        runUDPAssociateTLS proxyConfig clientParams $ \sendUDP recvUDP -> do
+          let dnsServerAddr = SockAddrInet 53 (tupleToHostAddress (8, 8, 8, 8))
+          let dnsQuery =
+                B.pack [0x12, 0x34, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01]
+          putStrLn $ "Sending DNS query for 'google.com' to " ++ show dnsServerAddr ++ " via proxy..."
+          sendUDP dnsQuery dnsServerAddr
+          (response, from) <- recvUDP
+          putStrLn $ "Received " ++ show (B.length response) ++ " bytes from " ++ show from
+          response `shouldSatisfy` (B.length |> (> 0))
+          B.take 2 response `shouldBe` B.take 2 dnsQuery
+          when (B.length response >= 8) $ do
+            let flags = B.unpack $ B.take 2 $ B.drop 2 response
+                anCountBytes = B.unpack $ B.take 2 $ B.drop 6 response
+                answerCount = (fromIntegral (head anCountBytes) `shiftL` 8) + fromIntegral (last anCountBytes) :: Int
+            head flags `shouldSatisfy` (>= 0x80)
+            answerCount `shouldSatisfy` (> 0)
+
+withSOCKS5Server :: (ClientConfig -> IO a) -> IO a
+withSOCKS5Server action = do
+  let serverConfig =
+        ServerConfig
+          { serverHost = "0.0.0.0",
+            serverPort = "10800",
+            users = [("testuser", "testpass")]
+          }
+  withAsync (runSOCKS5Server serverConfig) $ \_ -> do
+    threadDelay 200000
+    putStrLn "SOCKS5 server started. Running tests..."
+    result <- action $ ClientConfig "127.0.0.1" "10800" (Just ("testuser", "testpass"))
+    putStrLn "Tests finished. Stopping SOCKS5 server..."
+    return result
+
+withSOCKS5ServerTLS :: ((ClientConfig, ClientParams) -> IO a) -> IO a
+withSOCKS5ServerTLS action = do
+  let serverConfig =
+        ServerConfig
+          { serverHost = "0.0.0.0",
+            serverPort = "10800",
+            users = [("testuser", "testpass")]
+          }
+  serverParams <- credential "tests/cert.pem" "tests/key.pem"
+  withAsync (runSOCKS5ServerTLS serverConfig serverParams) $ \_ -> do
+    threadDelay 200000
+    putStrLn "SOCKS5 TLS server started. Running tests..."
+    let clientConfig = ClientConfig "127.0.0.1" "10800" (Just ("testuser", "testpass"))
+    let clientParams =
+          (defaultParamsClient "127.0.0.1" "")
+            { clientSupported =
+                def
+                  { supportedCiphers = ciphersuite_strong
+                  },
+              clientShared =
+                def
+                  { sharedValidationCache = ValidationCache (\_ _ _ -> return ValidationCachePass) (\_ _ _ -> return ())
+                  }
+            }
+    result <- action (clientConfig, clientParams)
+    putStrLn "Tests finished. Stopping SOCKS5 TLS server..."
+    return result
+
+credential :: FilePath -> FilePath -> IO ServerParams
+credential cert key = do
+  cred <- either error id <$> credentialLoadX509 cert key
+  return
+    (defaultParamsServer :: ServerParams)
+      { serverShared =
+          def
+            { sharedCredentials = Credentials [cred]
+            },
+        serverSupported =
+          def
+            { supportedCiphers = ciphersuite_strong
+            }
+      }
+
+(|>) :: (a -> b) -> (b -> Bool) -> a -> Bool
+f |> p = p . f
