diff --git a/README.md b/README.md index 22a7728..4547435 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Support these args: 7. -S server mode 8. -L Local mode(client, default) 9. -c config file + 10. -t timeout(unit is second) Crypto method: @@ -33,7 +34,7 @@ Crypto method: One time auth feature done. -Support JSON config file.(local\_address/timeout/fast\_open/workers is not support) +Support JSON config file.(local\_address/fast\_open/workers is not support) You could refer to demo config etc/demo.json. diff --git a/src/main/java/shadowsocks/nio/tcp/BufferHelper.java b/src/main/java/shadowsocks/nio/tcp/BufferHelper.java index cbc5e3d..a4c7823 100644 --- a/src/main/java/shadowsocks/nio/tcp/BufferHelper.java +++ b/src/main/java/shadowsocks/nio/tcp/BufferHelper.java @@ -52,11 +52,14 @@ public static ByteBuffer create(int size) return ByteBuffer.allocate(size); } + // This logic comes from Grizzly. + // Block current thread, avoid 100% cpu loading. public static void send(SocketChannel remote, byte [] newData) throws IOException { SelectionKey key = null; Selector writeSelector = null; int attempts = 0; + // 15s for each write timeout. int writeTimeout = 15 * 1000; ByteBuffer bb = ByteBuffer.wrap(newData); try { diff --git a/src/main/java/shadowsocks/nio/tcp/LocalTcpWorker.java b/src/main/java/shadowsocks/nio/tcp/LocalTcpWorker.java index cf02017..1b5be5a 100644 --- a/src/main/java/shadowsocks/nio/tcp/LocalTcpWorker.java +++ b/src/main/java/shadowsocks/nio/tcp/LocalTcpWorker.java @@ -66,6 +66,13 @@ private void parseHeader() throws IOException BufferHelper.prepare(bb, 257); local.read(bb); + //Check socks version. + //TODO: check method list. + bb.flip(); + if (bb.get() != 5) { + throw new IOException("INVALID CONNECTION"); + } + //reply 0x05(Socks version) 0x00 (no password) byte [] msg = {0x05, 0x00}; replyToProxyProgram(msg); @@ -246,7 +253,7 @@ private void init() throws IOException{ mOneTimeAuth = mConfig.oneTimeAuth; - mConfig.remoteAddress = new InetSocketAddress(InetAddress.getByName(mConfig.server), mConfig.port); + mConfig.remoteAddress = new InetSocketAddress(InetAddress.getByName(mConfig.server), mConfig.serverPort); } public LocalTcpWorker(SocketChannel sc, LocalConfig lc){ diff --git a/src/main/java/shadowsocks/nio/tcp/Session.java b/src/main/java/shadowsocks/nio/tcp/Session.java index f551159..785c118 100644 --- a/src/main/java/shadowsocks/nio/tcp/Session.java +++ b/src/main/java/shadowsocks/nio/tcp/Session.java @@ -15,6 +15,7 @@ */ package shadowsocks.nio.tcp; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.ByteArrayOutputStream; @@ -23,6 +24,8 @@ public class Session { + private static Logger log = LogManager.getLogger(Session.class.getName()); + final static public int ADDR_TYPE_IPV4 = 0x01; final static public int ADDR_TYPE_HOST = 0x03; @@ -52,7 +55,8 @@ private static void dec(){ private int mSessionID; - private long mTimeout = 30 * 1000; + // Default timeout is 300s. + private long mTimeout = 300 * 1000; private long mLastActiveTime; @@ -60,6 +64,11 @@ public void updateActiveTime(){ mLastActiveTime = System.currentTimeMillis(); } + public void setTimeout(int t) + { + mTimeout = t*1000; + } + public boolean isTimeout() { return System.currentTimeMillis() - mLastActiveTime > mTimeout; } @@ -84,7 +93,7 @@ public void record(int size, int direct) { mR2LSize += size; } - public void dump(Logger log, Exception e) { + public void dump(Exception e) { log.error("Remote: " + mRemote + ", local: " + mLocal + ". Stream down size: " + mR2LSize + ", stream up size: " + mL2RSize + ".", e); } public int getID(){ diff --git a/src/main/java/shadowsocks/nio/tcp/TcpWorker.java b/src/main/java/shadowsocks/nio/tcp/TcpWorker.java index 0821a55..6dedff9 100644 --- a/src/main/java/shadowsocks/nio/tcp/TcpWorker.java +++ b/src/main/java/shadowsocks/nio/tcp/TcpWorker.java @@ -80,7 +80,7 @@ public abstract class TcpWorker implements Runnable { protected int mChunkCount = 0; - protected void mainLoop(Selector selector, SocketChannel local, SocketChannel remote) throws IOException,InterruptedException,CryptoException,AuthException + private void mainLoop(Selector selector, SocketChannel local, SocketChannel remote) throws IOException,InterruptedException,CryptoException,AuthException { local.configureBlocking(false); remote.configureBlocking(false); @@ -118,19 +118,18 @@ protected void mainLoop(Selector selector, SocketChannel local, SocketChannel re } } - protected void TcpRelay(SocketChannel local) + protected void TcpRelay() { int CONNECT_TIMEOUT = 5000; + SocketChannel remote = mSession.get(false); + SocketChannel local = mSession.get(true); - try(SocketChannel remote = SocketChannel.open(); - Selector selector = Selector.open();) + try(Selector selector = Selector.open()) { - mSession.set(local, true); - mSession.set(remote, false); //Init subclass special field. handleStage(INIT); handleStage(PARSE_HEADER); - log.info(mSession.getID() + ": connecting " + mConfig.target + " from " + local.socket().getLocalSocketAddress()); + log.info(mSession.getID() + ": connecting " + mConfig.target + " from " + local.getRemoteAddress()); remote.socket().setTcpNoDelay(true); remote.socket().connect(mConfig.remoteAddress, CONNECT_TIMEOUT); handleStage(BEFORE_TCP_RELAY); @@ -141,7 +140,11 @@ protected void TcpRelay(SocketChannel local) }catch(InterruptedException e){ //ignore }catch(IOException | CryptoException | AuthException e){ - mSession.dump(log, e); + if (e.getMessage().equals("INVALID CONNECTION")) { + log.debug("Invalid connection"); + return; + } + mSession.dump(e); } } @@ -149,17 +152,21 @@ protected void TcpRelay(SocketChannel local) @Override public void run(){ - //make sure this channel could be closed - try(SocketChannel local = mLocal){ + //make sure the 2 channels could be closed + try(SocketChannel local = mLocal; SocketChannel remote = SocketChannel.open()) + { mSession = new Session(); + mSession.set(local, true); + mSession.set(remote, false); + mSession.setTimeout(mConfig.timeout); // for decrypt/encrypt mCryptor = CryptoFactory.create(mConfig.method, mConfig.password); // for one time auth mAuthor = new HmacSHA1(); mStreamUpData = new ByteArrayOutputStream(); - TcpRelay(local); + TcpRelay(); }catch(Exception e){ - mSession.dump(log, e); + log.error(e); }finally{ mSession.destory(); } diff --git a/src/main/java/shadowsocks/util/GlobalConfig.java b/src/main/java/shadowsocks/util/GlobalConfig.java index d7db963..d4fb25b 100644 --- a/src/main/java/shadowsocks/util/GlobalConfig.java +++ b/src/main/java/shadowsocks/util/GlobalConfig.java @@ -41,12 +41,24 @@ public class GlobalConfig{ private int mLocalPort; private boolean mOneTimeAuth; private boolean mIsServerMode; + /* UNIT second */ + private int mTimeout; final private static String DEFAULT_METHOD = "aes-256-cfb"; final private static String DEFAULT_PASSWORD = "123456"; final private static String DEFAULT_SERVER = "127.0.0.1"; final private static int DEFAULT_PORT = 8388; final private static int DEFAULT_LOCAL_PORT = 9999; + final private static int DEFAULT_TIMEOUT = 300; + + public void setTimeout(int t) + { + mTimeout = t; + } + public int getTimeout() + { + return mTimeout; + } public void setPassowrd(String p) { @@ -115,7 +127,6 @@ public String getConfigFile(){ return mConfigFile; } - public synchronized static GlobalConfig get() { if (mConfig == null) @@ -135,6 +146,7 @@ public GlobalConfig() mOneTimeAuth = false; mIsServerMode = false; mConfigFile = null; + mTimeout = DEFAULT_TIMEOUT; } public void printConfig(){ @@ -150,6 +162,7 @@ public void printConfig(){ log.info("Server port [" + getPort() + "]"); log.info("Local port [" + getLocalPort() + "]"); } + log.info("Timeout [" + getTimeout() + "]"); } public static String readConfigFile(String name){ @@ -215,11 +228,18 @@ public static void getConfigFromFile(){ }catch(JSONException e){ //No this config, ignore; } + try{ + int timeout = jsonobj.getInt("timeout"); + log.debug("CFG:timeout: " + timeout); + GlobalConfig.get().setTimeout(timeout); + }catch(JSONException e){ + //No this config, ignore; + } } public static void getConfigFromArgv(String argv[]) { - Getopt g = new Getopt("shadowsocks", argv, "SLm:k:p:as:l:c:"); + Getopt g = new Getopt("shadowsocks", argv, "SLm:k:p:as:l:c:t:"); int c; String arg; while ((c = g.getopt()) != -1) @@ -270,6 +290,12 @@ public static void getConfigFromArgv(String argv[]) log.debug("CMD:Config file: " + arg); GlobalConfig.get().setConfigFile(arg); break; + case 't': + arg = g.getOptarg(); + int timeout = Integer.parseInt(arg); + log.debug("CMD:timeout: " + timeout); + GlobalConfig.get().setTimeout(timeout); + break; case '?': default: help(); @@ -284,7 +310,9 @@ public static LocalConfig createLocalConfig() { GlobalConfig.get().getServer(), GlobalConfig.get().getPort(), GlobalConfig.get().getLocalPort(), - GlobalConfig.get().isOTAEnabled()); + GlobalConfig.get().isOTAEnabled(), + GlobalConfig.get().getTimeout() + ); } private static void help() diff --git a/src/main/java/shadowsocks/util/LocalConfig.java b/src/main/java/shadowsocks/util/LocalConfig.java index 2a6dd4a..5930599 100644 --- a/src/main/java/shadowsocks/util/LocalConfig.java +++ b/src/main/java/shadowsocks/util/LocalConfig.java @@ -21,20 +21,22 @@ public class LocalConfig{ public String password; public String method; public String server; - public int port; + public int serverPort; public int localPort; public boolean oneTimeAuth; + public int timeout; //For server is target, for local is server. public InetSocketAddress remoteAddress; public String target; - public LocalConfig(String k, String m, String s, int p, int lp, boolean ota){ + public LocalConfig(String k, String m, String s, int p, int lp, boolean ota, int t){ password = k; method = m; server = s; - port = p; + serverPort = p; localPort = lp; oneTimeAuth = ota; + timeout = t; } } diff --git a/src/test/java/shadowsocks/SystemTest.java b/src/test/java/shadowsocks/SystemTest.java index ba0f438..23154b8 100644 --- a/src/test/java/shadowsocks/SystemTest.java +++ b/src/test/java/shadowsocks/SystemTest.java @@ -94,12 +94,12 @@ private void testSimpleHttp(boolean ota) { URL url = new URL("http://example.com"); conn = (HttpURLConnection)url.openConnection(proxy); conn.setRequestMethod("GET"); - DataInputStream in1 = new DataInputStream(conn.getInputStream()); - byte [] result = new byte[8192]; - in1.read(result); - DataInputStream in2 = new DataInputStream(this.getClass().getClassLoader().getResourceAsStream("result-example-com")); + DataInputStream in1 = new DataInputStream(this.getClass().getClassLoader().getResourceAsStream("result-example-com")); byte [] expect = new byte[8192]; - in2.read(expect); + int dataLen = in1.read(expect); + DataInputStream in2 = new DataInputStream(conn.getInputStream()); + byte [] result = new byte[8192]; + in2.readFully(result, 0, dataLen); boolean compareResult = Arrays.equals(result, expect); if (!compareResult) { log.debug("====================");