2016-05-16 21:55:06 +00:00
{- # LANGUAGE BangPatterns # -}
2016-05-14 23:50:16 +00:00
{- # LANGUAGE DeriveDataTypeable # -}
2016-10-09 18:54:08 +00:00
{- # LANGUAGE OverloadedStrings # -}
2016-05-14 23:50:16 +00:00
{- # OPTIONS_GHC - fno - cse # -}
2016-05-11 21:39:02 +00:00
module Main where
2016-10-09 18:54:08 +00:00
import ClassyPrelude hiding ( getArgs , head )
2016-05-14 23:50:16 +00:00
import qualified Data.ByteString.Char8 as BC
2016-10-09 18:54:08 +00:00
import Data.List ( head , ( !! ) )
2016-05-16 21:33:00 +00:00
import Data.Maybe ( fromMaybe )
2016-05-14 23:50:16 +00:00
import System.Console.CmdArgs
import System.Environment ( getArgs , withArgs )
2016-08-27 16:42:38 +00:00
import qualified Logger
2016-08-27 16:31:32 +00:00
import Tunnel
import Types
2019-10-05 12:31:22 +00:00
import Control.Concurrent.Async as Async
2016-08-27 16:31:32 +00:00
2016-05-14 23:50:16 +00:00
data WsTunnel = WsTunnel
2019-10-05 12:31:22 +00:00
{ localToRemote :: [ String ]
2016-05-28 20:32:52 +00:00
-- , remoteToLocal :: String
2016-08-24 13:26:25 +00:00
, dynamicToRemote :: String
, wsTunnelServer :: String
, udpMode :: Bool
2019-01-13 16:47:18 +00:00
, udpTimeout :: Int
2016-08-24 13:26:25 +00:00
, proxy :: String
2020-01-22 18:04:07 +00:00
, soMark :: Int
2016-08-24 13:26:25 +00:00
, serverMode :: Bool
, restrictTo :: String
, verbose :: Bool
, quiet :: Bool
2018-12-26 20:26:48 +00:00
, pathPrefix :: String
2016-05-14 23:50:16 +00:00
} deriving ( Show , Data , Typeable )
2016-05-15 00:09:18 +00:00
data WsServerInfo = WsServerInfo
{ useTls :: ! Bool
, host :: ! String
, port :: ! Int
} deriving ( Show )
data TunnelInfo = TunnelInfo
{ localHost :: ! String
, localPort :: ! Int
, remoteHost :: ! String
, remotePort :: ! Int
} deriving ( Show )
2016-05-14 23:50:16 +00:00
2016-05-15 23:09:56 +00:00
2016-05-14 23:50:16 +00:00
cmdLine :: WsTunnel
cmdLine = WsTunnel
2016-05-15 00:09:18 +00:00
{ localToRemote = def &= explicit &= name " L " &= name " localToRemote " &= typ " [BIND:]PORT:HOST:PORT "
2019-10-05 12:31:22 +00:00
&= help " Listen on local and forwards traffic from remote. Can be used multiple time " &= groupname " Client options "
2016-05-28 20:32:52 +00:00
-- , remoteToLocal = def &= explicit &= name "R" &= name "RemoteToLocal" &= typ "[BIND:]PORT:HOST:PORT"
-- &= help "Listen on remote and forward traffic from local"
2016-08-24 13:26:25 +00:00
, dynamicToRemote = def &= explicit &= name " D " &= name " dynamicToRemote " &= typ " [BIND:]PORT "
&= help " Listen on local and dynamically (with socks5 proxy) forwards traffic from remote " &= groupname " Client options "
2019-01-13 16:47:18 +00:00
, udpMode = def &= explicit &= name " u " &= name " udp " &= help " forward UDP traffic instead of TCP " &= groupname " Client options "
2019-10-05 12:31:22 +00:00
, udpTimeout = def &= explicit &= name " udpTimeoutSec " &= help " When using udp forwarding, timeout in seconds after when the tunnel connection is closed. Default 30sec, -1 means no timeout "
&= groupname " Client options "
2018-12-26 20:26:48 +00:00
, pathPrefix = def &= explicit &= name " upgradePathPrefix "
&= help " Use a specific prefix that will show up in the http path in the upgrade request. Useful if you need to route requests server side but don't have vhosts "
&= typ " String " &= groupname " Client options "
2016-05-28 19:17:48 +00:00
, proxy = def &= explicit &= name " p " &= name " httpProxy "
2016-06-05 20:13:09 +00:00
&= help " If set, will use this proxy to connect to the server " &= typ " USER:PASS@HOST:PORT "
2020-01-22 18:04:07 +00:00
, soMark = def &= explicit &= name " soMark "
2020-02-22 12:53:51 +00:00
&= help " (linux only) Mark network packet with SO_MARK sockoption with the specified value. You need to use {root, sudo, capabilities} to run wstunnel when using this option " &= typ " int "
2016-05-28 20:32:52 +00:00
, wsTunnelServer = def &= argPos 0 &= typ " ws[s]://wstunnelServer[:port] "
2016-05-14 23:50:16 +00:00
2016-05-15 00:09:18 +00:00
, serverMode = def &= explicit &= name " server "
&= help " Start a server that will forward traffic for you " &= groupname " Server options "
, restrictTo = def &= explicit &= name " r " &= name " restrictTo "
&= help " Accept traffic to be forwarded only to this service " &= typ " HOST:PORT "
2016-06-01 14:24:16 +00:00
, verbose = def &= groupname " Common options " &= help " Print debug information "
2016-06-01 20:01:23 +00:00
, quiet = def &= help " Print only errors "
2016-05-15 00:09:18 +00:00
} &= summary ( " Use the websockets protocol to tunnel {TCP,UDP} traffic \ n "
2016-05-14 23:50:16 +00:00
++ " wsTunnelClient <---> wsTunnelServer <---> RemoteHost \ n "
++ " Use secure connection (wss://) to bypass proxies "
)
&= helpArg [ explicit , name " help " , name " h " ]
toPort :: String -> Int
2018-12-28 08:48:50 +00:00
toPort " stdio " = 0
2016-05-14 23:50:16 +00:00
toPort str = case readMay str of
Just por -> por
2016-10-09 18:54:08 +00:00
Nothing -> error $ " Invalid port number ` " ++ str ++ " ` "
2016-05-14 23:50:16 +00:00
parseServerInfo :: WsServerInfo -> String -> WsServerInfo
2016-05-15 00:09:18 +00:00
parseServerInfo server [] = server
2016-06-05 20:13:09 +00:00
parseServerInfo server ( 'w' : 's' : ':' : '/' : '/' : xs ) = parseServerInfo ( server { Main . useTls = False , Main . port = 80 } ) xs
parseServerInfo server ( 'w' : 's' : 's' : ':' : '/' : '/' : xs ) = parseServerInfo ( server { Main . useTls = True , Main . port = 443 } ) xs
parseServerInfo server ( ':' : prt ) = server { Main . port = toPort prt }
2020-01-04 15:39:05 +00:00
parseServerInfo server ( '[' : xs ) = parseServerInfo ( server { Main . host = BC . unpack . BC . init . fst $ BC . spanEnd ( /= ']' ) ( BC . pack xs ) } ) ( BC . unpack . snd $ BC . spanEnd ( /= ']' ) ( BC . pack xs ) )
2016-06-05 20:13:09 +00:00
parseServerInfo server hostPath = parseServerInfo ( server { Main . host = takeWhile ( /= ':' ) hostPath } ) ( dropWhile ( /= ':' ) hostPath )
2016-05-14 23:50:16 +00:00
parseTunnelInfo :: String -> TunnelInfo
2020-01-04 15:39:05 +00:00
parseTunnelInfo strr = do
let str = BC . pack strr
if BC . count ']' str <= 0 then
mkIPv4 $ BC . unpack <$> BC . split ':' str
else
mkIPv6 $ str
2016-05-14 23:50:16 +00:00
where
2020-01-04 15:39:05 +00:00
mkIPv4 [ lPort , host , rPort ] = TunnelInfo { localHost = " 127.0.0.1 " , Main . localPort = toPort lPort , remoteHost = host , remotePort = toPort rPort }
mkIPv4 [ bind , lPort , host , rPort ] = TunnelInfo { localHost = bind , Main . localPort = toPort lPort , remoteHost = host , remotePort = toPort rPort }
mkIPv4 _ = error $ " Invalid tunneling information ` " ++ strr ++ " `, please use format [BIND:]PORT:HOST:PORT "
mkIPv6 str = do
let ! ( localHost , remain ) = if BC . head str == '[' then
BC . drop 2 <$> BC . span ( /= ']' ) ( BC . drop 1 str )
else if BC . head str < '0' || BC . head str > '9' then
BC . drop 1 <$> BC . span ( /= ':' ) str
else
( " " , str )
let ( remain , rPort ) = first BC . init . BC . spanEnd ( /= ':' ) $ str
let ( remain2 , remoteHost ) = if BC . last remain == ']' then
first ( BC . init . BC . init ) $ BC . spanEnd ( /= '[' ) ( BC . init remain )
else
first BC . init $ BC . spanEnd ( /= ':' ) remain
let ( remain3 , lPort ) = BC . spanEnd ( /= ':' ) $ remain2
if remain3 == mempty then
TunnelInfo { localHost = " ::1 " , Main . localPort = toPort ( BC . unpack lPort ) , remoteHost = ( BC . unpack remoteHost ) , remotePort = toPort ( BC . unpack rPort ) }
else
let localHost = BC . filter ( \ c -> c /= '[' && c /= ']' ) ( BC . init remain3 ) in
TunnelInfo { localHost = BC . unpack localHost , Main . localPort = toPort ( BC . unpack lPort ) , remoteHost = ( BC . unpack remoteHost ) , remotePort = toPort ( BC . unpack rPort ) }
2016-05-14 23:50:16 +00:00
2016-05-28 19:17:48 +00:00
parseRestrictTo :: String -> ( ( ByteString , Int ) -> Bool )
2016-05-16 21:33:00 +00:00
parseRestrictTo " " = const True
2020-01-04 15:39:05 +00:00
parseRestrictTo str = let ! ( ! h , ! p ) = fromMaybe ( error " Invalid Parameter restart " ) parse
2016-05-16 21:55:06 +00:00
in ( \ ( ! hst , ! port ) -> hst == h && port == p )
2016-05-16 21:33:00 +00:00
where
parse = do
2020-01-04 15:39:05 +00:00
let ( host , port ) = BC . spanEnd ( /= ':' ) ( BC . pack str )
guard ( host /= mempty )
portNumber <- readMay . BC . unpack $ port :: Maybe Int
return $! ( BC . filter ( \ c -> c /= '[' && c /= ']' ) ( BC . init host ) , portNumber )
2016-05-16 21:33:00 +00:00
2016-06-05 20:13:09 +00:00
parseProxyInfo :: String -> Maybe ProxySettings
2016-05-28 19:17:48 +00:00
parseProxyInfo str = do
2016-06-05 20:13:09 +00:00
let ret = BC . split ':' ( BC . pack str )
guard ( length ret >= 2 )
if length ret == 3
then do
portNumber <- readMay $ BC . unpack $ ret !! 2 :: Maybe Int
let cred = ( head ret , head ( BC . split '@' ( ret !! 1 ) ) )
let h = BC . split '@' ( ret !! 1 ) !! 1
return $ ProxySettings ( BC . unpack h ) ( fromIntegral portNumber ) ( Just cred )
else if length ret == 2
then do
portNumber <- readMay . BC . unpack $ ret !! 1 :: Maybe Int
return $ ProxySettings ( BC . unpack $ head ret ) ( fromIntegral portNumber ) Nothing
else Nothing
2016-05-28 19:17:48 +00:00
2016-05-11 21:39:02 +00:00
main :: IO ()
2016-05-14 23:50:16 +00:00
main = do
args <- getArgs
2018-12-26 20:26:48 +00:00
cfg' <- if null args then withArgs [ " --help " ] ( cmdArgs cmdLine ) else cmdArgs cmdLine
2019-01-13 16:47:18 +00:00
let cfg = cfg' { pathPrefix = if pathPrefix cfg' == mempty then " wstunnel " else pathPrefix cfg'
, Main . udpTimeout = if Main . udpTimeout cfg' == 0 then 30 * 10 ^ ( 6 :: Int )
else if Main . udpTimeout cfg' == - 1 then - 1
else Main . udpTimeout cfg' * 10 ^ ( 6 :: Int )
}
2016-05-14 23:50:16 +00:00
let serverInfo = parseServerInfo ( WsServerInfo False " " 0 ) ( wsTunnelServer cfg )
2016-08-27 16:42:38 +00:00
Logger . init ( if quiet cfg then Logger . QUIET
else if verbose cfg
then Logger . VERBOSE
else Logger . NORMAL )
2016-05-31 16:35:04 +00:00
2020-01-22 18:04:07 +00:00
_ <- writeIORef sO_MARK_Value ( soMark cfg )
2019-10-05 12:31:22 +00:00
runApp cfg serverInfo
putStrLn " Goodbye ! "
return ()
2016-05-14 23:50:16 +00:00
2019-10-05 12:31:22 +00:00
runApp :: WsTunnel -> WsServerInfo -> IO ()
runApp cfg serverInfo
-- server mode
| serverMode cfg = do
putStrLn $ " Starting server with opts " <> tshow serverInfo
runServer ( Main . useTls serverInfo ) ( Main . host serverInfo , fromIntegral $ Main . port serverInfo ) ( parseRestrictTo $ restrictTo cfg )
2016-05-14 23:50:16 +00:00
2019-10-05 12:31:22 +00:00
-- -L localToRemote tunnels
| not . null $ localToRemote cfg = do
let tunnelInfos = parseTunnelInfo <$> localToRemote cfg
let tunnelSettings = tunnelInfos >>= \ tunnelInfo -> [ toTcpLocalToRemoteTunnelSetting cfg serverInfo tunnelInfo , toUdpLocalToRemoteTunnelSetting cfg serverInfo tunnelInfo ]
Async . mapConcurrently_ runClient tunnelSettings
-- -D dynamicToRemote tunnels
| not . null $ dynamicToRemote cfg = do
let tunnelSetting = toDynamicTunnelSetting cfg serverInfo . parseTunnelInfo $ ( dynamicToRemote cfg ) ++ " :127.0.0.1:1212 "
runClient tunnelSetting
| otherwise = do
putStrLn " Cannot parse correctly the command line. Please fill an issue "
where
toTcpLocalToRemoteTunnelSetting cfg serverInfo ( TunnelInfo lHost lPort rHost rPort ) =
TunnelSettings {
localBind = lHost
, Types . localPort = fromIntegral lPort
, serverHost = Main . host serverInfo
, serverPort = fromIntegral $ Main . port serverInfo
, destHost = rHost
, destPort = fromIntegral rPort
, Types . useTls = Main . useTls serverInfo
, protocol = TCP
, proxySetting = parseProxyInfo ( proxy cfg )
, useSocks = False
, upgradePrefix = pathPrefix cfg
, udpTimeout = Main . udpTimeout cfg
}
toUdpLocalToRemoteTunnelSetting cfg serverInfo ( TunnelInfo lHost lPort rHost rPort ) =
TunnelSettings {
localBind = lHost
, Types . localPort = fromIntegral lPort
, serverHost = Main . host serverInfo
, serverPort = fromIntegral $ Main . port serverInfo
, destHost = rHost
, destPort = fromIntegral rPort
, Types . useTls = Main . useTls serverInfo
, protocol = UDP
, proxySetting = parseProxyInfo ( proxy cfg )
, useSocks = False
, upgradePrefix = pathPrefix cfg
, udpTimeout = Main . udpTimeout cfg
}
toDynamicTunnelSetting cfg serverInfo ( TunnelInfo lHost lPort _ _ ) =
TunnelSettings {
localBind = lHost
, Types . localPort = fromIntegral lPort
, serverHost = Main . host serverInfo
, serverPort = fromIntegral $ Main . port serverInfo
, destHost = " "
, destPort = 0
, Types . useTls = Main . useTls serverInfo
, protocol = SOCKS5
, proxySetting = parseProxyInfo ( proxy cfg )
, useSocks = True
, upgradePrefix = pathPrefix cfg
, udpTimeout = Main . udpTimeout cfg
}