QNWZFDUGC4UT6B6BOCKAPDTLNMU5V5GAENSMYWZX6D4URBYIIWZAC 66RIHWIV3IQ7EX7JQBR4GBXBV5VS53LFUXG4GVQFYWEAFTYTEDWAC R62UQY34MWC67MS52BDUNUZKNZM4IVOYAWZND6O37GVR472JMYTQC OXU4CGLDG44NLRFO5HDX23JSOKIMQ6OXEPWUNWCZZ4YWX3THOS5AC CSY27FWPSKRAT37RJNIEQRVDNIO6L4QLENSPOE3XLIGGQZDETGRAC WXFIZKTKYWQAT7EQ6KZKUTVDICW6IHX3J45T2JPZ43YLLLYBAZXAC Y7LXAWJMKAU75ESS6E4OQFVW5XMJE7UIIEPX5OKGS6Y53RTNBS4AC N6TINDVBWXSHH6I7G4GQBIGKGSSLA36B6HK7MHKKBICITMYWT2XQC 7ZJW2CM7Y6YAUHUE37SPYI4JP2GFYBXJRKQE544NGTFQNPEX5ZXQC QV3AJVRIBWZIGR2BQKBHMG5ZRB7D4NLEF5ZJMYUUG3GAAG5J3ZQAC NLX73UKSI67TAREMATK6FIEOTMG25OT7O5SAJQNDCELQWTQK56PQC D44XFSLPFX5F2L4J5XATIHWGAF6SYJQ4CBSW34YU7MN332V3CNPQC 4LMXOSYC2Y6PRPOEPK5NSMAQXG2W4M5CFKQWXFMUNUNBO6L6FMWAC {-# LANGUAGE OverloadedStrings #-}module Env( Env(..), loadEnv) whereimport System.IOimport Control.Exception (catch, IOException)import Data.List (isPrefixOf)import Data.Maybe (fromMaybe)import Data.Char (toUpper)import Text.Read (readMaybe)import Udp2Raw (RawMode(..)) -- Import the RawMode type (ICMP, UDP, FakeTCP)--------------------------------------------------------------------------------- | Environment configuration-------------------------------------------------------------------------------data Env = Env{ ip :: String, rawMode :: RawMode} deriving (Show, Eq)--------------------------------------------------------------------------------- | Load and parse the .env file-------------------------------------------------------------------------------loadEnv :: FilePath -> IO (Either String Env)loadEnv path = catch (docontent <- readFile pathlet kvs = parseEnv contentmIP = lookup "SERVER_IP" kvsmMode = lookup "RAW_MODE" kvsmPort = lookup "PORT" kvscase (mIP, mMode) of(Just ipStr, Just modeStr) ->case parseRawMode modeStr mPort ofJust raw -> pure $ Right $ Env ipStr rawNothing -> pure $ Left $ "Invalid RAW_MODE or missing PORT for mode: " ++ modeStr_ -> pure $ Left "Missing SERVER_IP or RAW_MODE in .env") handleReadErrorwherehandleReadError :: IOException -> IO (Either String Env)handleReadError e = pure $ Left $ "Error reading .env: " ++ show e--------------------------------------------------------------------------------- | Parse RAW_MODE + optional PORT into RawMode-------------------------------------------------------------------------------parseRawMode :: String -> Maybe String -> Maybe RawModeparseRawMode modeStr mPort =let mode = map toUpper modeStrport = mPort >>= readMaybein case mode of"ICMP" -> Just ICMP"UDP" -> UDP <$> port -- mandatory port"FAKETCP" -> FakeTCP <$> port -- mandatory port"TCP" -> FakeTCP <$> port -- alias for FakeTCP_ -> Nothing--------------------------------------------------------------------------------- | Parse .env content into key-value pairs-------------------------------------------------------------------------------parseEnv :: String -> [(String, String)]parseEnv = mapMaybe parseLine . lineswhereparseLine line =case break (== '=') (trim line) of(key, '=':value)| not (null key) && not (isComment key) ->Just (trim key, trim value)_ -> NothingisComment ('#':_) = TrueisComment _ = Falsetrim = f . fwhere f = reverse . dropWhile (`elem` [' ', '\t', '\r'])-- | Helper: mapMaybe for base < 4.8mapMaybe :: (a -> Maybe b) -> [a] -> [b]mapMaybe f = foldr (\x acc -> case f x ofJust y -> y:accNothing -> acc) []
{-# LANGUAGE OverloadedStrings #-}module ConfigParserUtils (getYamlConfigEntry) whereimport ConfigParser (ConfigEntry, parseConfigFile)import Data.List (find)import Data.Maybe (listToMaybe)import ConfigParser (ConfigEntry, parseConfigFile, name) -- add `name`import qualified Data.Text as T-- | Parse YAML file and return either:-- * The entry matching the given name, or-- * The first entry if no name is providedgetYamlConfigEntry :: FilePath -- ^ YAML config file path-> Maybe String -- ^ Optional name-> IO (Either String ConfigEntry)getYamlConfigEntry file mName = doparseResult <- parseConfigFile filecase parseResult ofLeft err -> pure $ Left errRight cfg -> case mName ofJust cfgNameStr -> case find (\e -> T.unpack (name e) == cfgNameStr) cfg ofJust entry -> pure $ Right entryNothing -> pure $ Left $ "No config entry named: " ++ cfgNameStrNothing -> case listToMaybe cfg ofJust entry -> pure $ Right entryNothing -> pure $ Left "YAML config is empty"
#!/bin/shSOCK=/run/tproxy/socketif [[ ! -S $SOCK ]]; thenecho "tproxy service not running"exit 1fiif [[ $# -lt 1 ]]; thenecho "usage: $0 {start|stop|status|exit} [name]"exit 1fi# Send all arguments joined by spacesecho "$*" | nc -U "$SOCK"
import Control.Concurrent (forkIO, MVar, newEmptyMVar, putMVar, takeMVar, killThread, forkFinally, isEmptyMVar)import Control.Exception (catch, try, SomeException, IOException)
import Control.Concurrent( forkIO, MVar, newEmptyMVar, putMVar, takeMVar, killThread, isEmptyMVar )import Control.Exception (catch, IOException)
import System.Exit (exitFailure)import System.IO (hPutStrLn, stderr, IOMode( ReadWriteMode ), hSetBuffering, BufferMode( LineBuffering ), hGetLine, hClose, stdout, BufferMode( NoBuffering ))import Control.Monad (forM_, unless, forever, void, when)
import System.IO( hPutStrLn, stderr, hSetBuffering, BufferMode(LineBuffering, NoBuffering), hGetLine, hClose, stdout, IOMode(ReadWriteMode), Handle )
-- Install signal handlers for SIGTERM (systemd stop) and SIGINT (Ctrl+C)installHandler sigTERM (Catch $ putMVar stopFlag ()) Nothing-- 1. Get the configuration file pathargs <- getArgslet configFile = case args of(f:_) -> f[] -> "servers.yaml"
args <- getArgslet configFile = case args of(f:_) -> f[] -> "servers.yaml"
-- 2. Call the exported function from the ConfigParser moduleparseResult <- ConfigParser.parseConfigFile configFile
-- Try to load .env firsteEnv <- Env.loadEnv "/run/udp2raw/.env"case eEnv ofRight envVal -> doputStrLn $ "Loaded environment: " ++ show envValstart (Env.ip envVal) (Env.rawMode envVal)Left _ -> do-- Fallback to YAMLyamlCfg <- parseConfigFile configFilecase yamlCfg ofRight (entry:_) -> doip <- waitForIP (T.unpack $ nameserver entry) (T.unpack $ domain entry)let raw = fromMaybe ICMP (textToRawMode (raw_mode entry) (port entry))writeEnv ip (T.unpack $ udp2raw_password entry) (T.unpack $ udpspeeder_password entry) rawstart ip rawRight [] -> putStrLn "YAML configuration is empty"Left err -> hPutStrLn stderr $ "Failed to load .env and YAML: " ++ err
case parseResult of-- Handle failure (The Left String contains the formatted error)Left errorMessage -> dohPutStrLn stderr errorMessage
-- Clean up stale socketcatch (removeFile socketPath) (\(_ :: IOException) -> pure ())sock <- socket AF_UNIX Stream 0bind sock (SockAddrUnix socketPath)setFileMode socketPath 0o775listen sock 10
-- Accessing the first entry's nameservercase cfg of(entry:_) -> doputStrLn $ "The server is " ++ show (name entry)ip <- waitForIP (T.unpack $ nameserver entry) (T.unpack $ domain entry)let raw = fromMaybe ICMP (textToRawMode (raw_mode entry) (port entry))let udp2raw_pwd = T.unpack $ udp2raw_password entrylet udpspeeder_pwd = T.unpack $ udpspeeder_password entrywriteEnv ip udp2raw_pwd udpspeeder_pwd rawstart ip raw
acceptThread <- forkIO $ acceptLoop sock stopFlag configFile envVal
-- Clean up any existing socket filecatch (removeFile socketPath)(\(e :: IOException) ->if isDoesNotExistError e then pure () else ioError e)
-- Wait for SIGTERMtakeMVar stopFlagputStrLn "Received SIGTERM, shutting down gracefully..."killThread acceptThreadclose sockremoveFile socketPath-- Stop proxy on shutdowncase eEnv ofRight envVal -> stop (Env.ip envVal) (Env.rawMode envVal)Left _ -> pure ()
-- Run the accept loop in a separate threadacceptThread <- forkIO $acceptLoop sock stopFlag (\conn -> handleClient conn ip raw)
-- | Accept loopacceptLoop :: Socket -> MVar () -> FilePath -> Maybe Env.Env -> IO ()acceptLoop sock stopFlag configFile envFallback = loopwhereloop = doempty <- isEmptyMVar stopFlagif emptythen do(conn, _) <- accept sockforkIO $ handleClient conn configFile envFallbackloopelse pure ()
-- Wait until SIGTERMtakeMVar stopFlagputStrLn "Received SIGTERM, shutting down gracefully..."
-- | Handle a single client connection-- | Handle a single client connectionhandleClient :: Socket -> FilePath -> Maybe Env.Env -> IO ()handleClient sock configFile envFallback = doh <- socketToHandle sock ReadWriteModehSetBuffering h LineBufferingcmdLine <- hGetLine hlet cmdWords = words cmdLinecase cmdWords of("start":nameParts) -> dolet mName = if null nameParts then Nothing else Just (unwords nameParts)hPutStrLn h $ "Starting " ++ (unwords nameParts)
--clear the iptablesstop ip raw
-- Parse YAML configyamlCfg <- parseConfigFile configFilemEntry <- case yamlCfg ofRight cfg -> case mName of-- Select entry by nameJust cfgNameStr ->case find (\e -> T.strip (name e) == T.pack cfgNameStr) cfg ofJust e -> dohPutStrLn h $ "Found " ++ cfgNameStrpure (Just e)Nothing -> dohPutStrLn h $ "No config entry named: " ++ cfgNameStrpure Nothing-- No name provided, pick the first entryNothing -> pure $ listToMaybe cfgLeft _ -> pure Nothing(ipAddr, rawModeVal) <- case mEntry ofJust entry -> doip <- waitForIP (T.unpack $ nameserver entry) (T.unpack $ domain entry)let raw = fromMaybe ICMP (textToRawMode (raw_mode entry) (port entry))writeEnv ip (T.unpack $ udp2raw_password entry) (T.unpack $ udpspeeder_password entry) rawpure (ip, raw)Nothing -> case envFallback ofJust envVal -> pure (Env.ip envVal, Env.rawMode envVal)Nothing -> dohPutStrLn h "No matching config entry and no .env"pure ("", ICMP)
-- Stop accepting, clean upkillThread acceptThreadclose sockremoveFile socketPath[] -> putStrLn "Configuration file was empty."
-- Start proxy with resolved IP and RawModestart ipAddr rawModeValhPutStrLn h "started"["stop"] -> docase envFallback ofJust envVal -> dostop (Env.ip envVal) (Env.rawMode envVal)hPutStrLn h "stopped"Nothing -> hPutStrLn h "No .env loaded, cannot stop"_ -> hPutStrLn h "unknown command"hClose h
-- | Loop accepting connections until stopFlag is setacceptLoop :: Socket -> MVar () -> (Socket -> IO ()) -> IO ()acceptLoop sock stopFlag handleConn = dolet loop = dostillRunning <- isEmptyMVar stopFlagwhen stillRunning $ do(conn, _) <- accept sockvoid $ forkFinally (handleConn conn) (\_ -> close conn)looploop
handleClient :: Socket-> String -- ^ server IP (resolved at runtime)-> RawMode-> IO ()handleClient conn ip raw = doh <- socketToHandle conn ReadWriteModehSetBuffering h LineBufferingcmd <- hGetLine hcase cmd of"start" -> doputStrLn "Enabling global proxy"start ip rawhPutStrLn h "started""stop" -> doputStrLn "Disabling global proxy"stop ip rawhPutStrLn h "stopped"_ -> hPutStrLn h "unknown command"hClose h
clearRulescase raw ofICMP -> do clearICMP ipUDP p -> do clearUDP ip pFakeTCP p-> do clearFakeTCP ip p
case raw ofICMP -> clearICMP ipUDP p -> clearUDP ip pFakeTCP p -> clearFakeTCP ip p