diff --git a/README.md b/README.md index 5928247..cb42e8a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It will remove complex funtions. This need JDK 8. #Current state: -Simple server + client. Version 0.6.1 +Simple server + client. Version 0.6.2 Support these args: @@ -20,8 +20,8 @@ Support these args: 4. -a OTA enforcing mode 5. -l local port 6. -s server - 7. -S server mode(default) - 8. -L Local mode(client) + 7. -S server mode + 8. -L Local mode(client, default) 9. -c config file Crypto method: @@ -63,12 +63,12 @@ $ bin/shadowsocks -L ... $ gradle fatJar ``` -Then you will get shadowsocks-java-xx.jar in build/libs. +Then you will get shadowsocks-fat-xx.jar in build/libs. #### How to run ``` //Server -$ java -jar shadowsocks-java-xx.jar -S ... +$ java -jar shadowsocks-fat-xx.jar -S ... //Local -$ java -jar shadowsocks-java-xx.jar -L ... +$ java -jar shadowsocks-fat-xx.jar -L ... ``` diff --git a/build.gradle b/build.gradle index 5778b8a..e46c89d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,4 @@ -version '0.6.1' +version '0.6.2' apply plugin: 'java' apply plugin: 'application' @@ -28,6 +28,8 @@ task ('fatJar', type: Jar, dependsOn: classes){ 'Implementation-Version': version, 'Main-Class': 'shadowsocks.Main' } + baseName = 'shadowsocks' + appendix = 'fat' from {configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }} //pack dependent jar from 'build/classes/main' from 'build/resources/main' diff --git a/src/main/java/shadowsocks/Main.java b/src/main/java/shadowsocks/Main.java index 05f8e18..7c18dd2 100644 --- a/src/main/java/shadowsocks/Main.java +++ b/src/main/java/shadowsocks/Main.java @@ -25,7 +25,7 @@ public class Main{ public static Logger log = LogManager.getLogger(Main.class.getName()); - public static final String VERSION = "0.6.1"; + public static final String VERSION = "0.6.2"; public static void main(String argv[]) { diff --git a/src/main/java/shadowsocks/nio/tcp/BufferHelper.java b/src/main/java/shadowsocks/nio/tcp/BufferHelper.java new file mode 100644 index 0000000..482cc40 --- /dev/null +++ b/src/main/java/shadowsocks/nio/tcp/BufferHelper.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016 Author:NU11 bestoapache@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package shadowsocks.nio.tcp; + +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.io.IOException; + +public class BufferHelper { + /* return true means send success */ + public static boolean writeToRemote(SocketChannel remote, ByteBuffer buffer) throws IOException + { + //Add timeout to avoid 100% cpu when write failed for a long time. + long timeout = System.currentTimeMillis() + 15*1000L; + while(buffer.hasRemaining()) { + remote.write(buffer); + if (System.currentTimeMillis() > timeout) { + return false; + } + } + return true; + } + + public static int readFormRemote(SocketChannel remote, ByteBuffer buffer) throws IOException + { + //Add timeout to avoid 100% cpu when write failed for a long time. + long timeout = System.currentTimeMillis() + 15*1000L; + int size = 0; + int total_size = 0; + while(buffer.hasRemaining()) { + size = remote.read(buffer); + if (size < 0) + break; + else + total_size += size; + if (System.currentTimeMillis() > timeout) { + break; + } + } + return total_size; + } +} diff --git a/src/main/java/shadowsocks/nio/tcp/LocalTcpWorker.java b/src/main/java/shadowsocks/nio/tcp/LocalTcpWorker.java index 24308aa..c919a8e 100644 --- a/src/main/java/shadowsocks/nio/tcp/LocalTcpWorker.java +++ b/src/main/java/shadowsocks/nio/tcp/LocalTcpWorker.java @@ -63,7 +63,7 @@ public class LocalTcpWorker extends TcpWorker { * OTA will add 10 bytes HMAC-SHA1 in the end of the head. * */ - private void parseHead(SocketChannel local, SocketChannel remote) throws IOException, CryptoException, AuthException + private void parseHeader(SocketChannel local, SocketChannel remote) throws IOException, CryptoException, AuthException { //skip method list (max 1+1+255) mBufferWrap.prepare(257); @@ -101,7 +101,7 @@ private void parseHead(SocketChannel local, SocketChannel remote) throws IOExcep mStreamUpData.write(data[3]); //get addr - InetAddress addr; + StringBuffer addr = new StringBuffer(); if (addrtype == Session.ADDR_TYPE_IPV4) { //get IPV4 address mBufferWrap.prepare(4); @@ -109,7 +109,7 @@ private void parseHead(SocketChannel local, SocketChannel remote) throws IOExcep data = mBuffer.array(); byte [] ipv4 = new byte[4]; System.arraycopy(data, 0, ipv4, 0, 4); - addr = InetAddress.getByAddress(ipv4); + addr.append(InetAddress.getByAddress(ipv4).toString()); mStreamUpData.write(data, 0, 4); }else if (addrtype == Session.ADDR_TYPE_HOST) { //get address len @@ -122,13 +122,15 @@ private void parseHead(SocketChannel local, SocketChannel remote) throws IOExcep mBufferWrap.prepare(len); mBufferWrap.readWithCheck(local, len); data = mBuffer.array(); - addr = InetAddress.getByName(new String(data, 0, len)); + addr.append(new String(data, 0, len)); mStreamUpData.write(data, 0, len); } else { //do not support other addrtype now. throw new IOException("Unsupport addr type: " + addrtype + "!"); } + addr.append(':'); + //get port mBufferWrap.prepare(2); mBufferWrap.readWithCheck(local, 2); @@ -137,6 +139,10 @@ private void parseHead(SocketChannel local, SocketChannel remote) throws IOExcep mStreamUpData.write(mBuffer.array(), 0, 2); + addr.append(port); + mSession.set(addr.toString(), false); + log.info("Target address: " + addr); + //reply mBufferWrap.prepare(10); // 05 00 00 01 + 0.0.0.0:8888 @@ -147,10 +153,6 @@ private void parseHead(SocketChannel local, SocketChannel remote) throws IOExcep while(mBuffer.hasRemaining()) local.write(mBuffer); - InetSocketAddress target = new InetSocketAddress(addr, port); - mSession.set(target, false); - log.info("Target address: " + target); - // Create auth head if (mOneTimeAuth){ byte [] authKey = SSAuth.prepareKey(mCryptor.getIV(true), mCryptor.getKey()); @@ -163,8 +165,7 @@ private void parseHead(SocketChannel local, SocketChannel remote) throws IOExcep data = mStreamUpData.toByteArray(); byte [] result = mCryptor.encrypt(data, data.length); ByteBuffer out = ByteBuffer.wrap(result); - while(out.hasRemaining()) - remote.write(out); + BufferHelper.writeToRemote(remote, out); } @Override @@ -207,8 +208,10 @@ protected boolean send(SocketChannel source, SocketChannel target, int direct) t result = mCryptor.decrypt(mBuffer.array(), size); } ByteBuffer out = ByteBuffer.wrap(result); - while(out.hasRemaining()) - target.write(out); + if (!BufferHelper.writeToRemote(target, out)) { + mSession.dump(log, new IOException("Some data send failed.")); + return true; + } return false; } @@ -222,7 +225,7 @@ protected InetSocketAddress getRemoteAddress(SocketChannel local/*unused*/) protected void preTcpRelay(SocketChannel local, SocketChannel remote) throws IOException, CryptoException, AuthException { - parseHead(local, remote); + parseHeader(local, remote); } @Override protected void postTcpTelay(SocketChannel local, SocketChannel remote) diff --git a/src/main/java/shadowsocks/nio/tcp/ServerTcpWorker.java b/src/main/java/shadowsocks/nio/tcp/ServerTcpWorker.java index 8328f2c..6c93de4 100644 --- a/src/main/java/shadowsocks/nio/tcp/ServerTcpWorker.java +++ b/src/main/java/shadowsocks/nio/tcp/ServerTcpWorker.java @@ -41,35 +41,14 @@ public class ServerTcpWorker extends TcpWorker { // For OTA // Store the data to do one time auth - private ByteArrayOutputStream mAuthData; - // Store the expect auth result from client - private byte [] mExpectAuthResult; - private StateMachine mSM; + private ByteArrayOutputStream mStreamUpData; private boolean mOneTimeAuth = false; private SSAuth mAuthor; private int mChunkCount = 0; - private class StateMachine{ - public final static int START_STATE = 0; - public final static int AUTH_HEAD = 0; - public final static int DATA = 1; - public final static int END_STATE = 1; - - // OTA auth head is 12 bytes - // 2 bytes for data len, 10 bytes for HMAC-SHA1 - public int mLenToRead[] = {12, 0}; - - private int mState; - - public int getState(){ - return mState; - } - public void nextState(){ - if (++mState > END_STATE){ - mState = START_STATE; - } - } - } + // Store the expect auth result from client + private byte [] mExpectAuthResult; + private int mChunkLeft = 0; /* * IV |addr type: 1 byte| addr | port: 2 bytes with big endian| @@ -81,9 +60,9 @@ public void nextState(){ * OTA will add 10 bytes HMAC-SHA1 in the end of the head. * */ - private InetSocketAddress parseHead(SocketChannel local) throws IOException, CryptoException, AuthException + private InetSocketAddress parseHeader(SocketChannel local) throws IOException, CryptoException, AuthException { - mAuthData.reset(); + mStreamUpData.reset(); // Read IV + address type length. int len = mCryptor.getIVLength() + 1; mBufferWrap.prepare(len); @@ -96,7 +75,7 @@ private InetSocketAddress parseHead(SocketChannel local) throws IOException, Cry mOneTimeAuth = true; addrtype &= 0x0f; } - mAuthData.write(result[0]); + mStreamUpData.write(result[0]); if (!mOneTimeAuth && Config.get().isOTAEnabled()) { throw new AuthException("OTA is not enabled!"); @@ -110,20 +89,20 @@ private InetSocketAddress parseHead(SocketChannel local) throws IOException, Cry mBufferWrap.readWithCheck(local, 4); result = mCryptor.decrypt(mBuffer.array(), 4); addr = InetAddress.getByAddress(result); - mAuthData.write(result, 0, 4); + mStreamUpData.write(result, 0, 4); }else if (addrtype == Session.ADDR_TYPE_HOST) { //get address len mBufferWrap.prepare(1); mBufferWrap.readWithCheck(local, 1); result = mCryptor.decrypt(mBuffer.array(), 1); len = result[0]; - mAuthData.write(result[0]); + mStreamUpData.write(result[0]); //get address mBufferWrap.prepare(len); mBufferWrap.readWithCheck(local, len); result = mCryptor.decrypt(mBuffer.array(), len); addr = InetAddress.getByName(new String(result, 0, len)); - mAuthData.write(result, 0, len); + mStreamUpData.write(result, 0, len); } else { //do not support other addrtype now. throw new IOException("Unsupport addr type: " + addrtype + "!"); @@ -136,7 +115,7 @@ private InetSocketAddress parseHead(SocketChannel local) throws IOException, Cry mBufferWrap.prepare(2); mBuffer.put(result[0]); mBuffer.put(result[1]); - mAuthData.write(result, 0, 2); + mStreamUpData.write(result, 0, 2); // if port > 32767 the short will < 0 int port = (int)(mBuffer.getShort(0)&0xFFFF); @@ -146,13 +125,13 @@ private InetSocketAddress parseHead(SocketChannel local) throws IOException, Cry mBufferWrap.readWithCheck(local, HmacSHA1.AUTH_LEN); result = mCryptor.decrypt(mBuffer.array(), HmacSHA1.AUTH_LEN); byte [] authKey = SSAuth.prepareKey(mCryptor.getIV(false), mCryptor.getKey()); - byte [] authData = mAuthData.toByteArray(); + byte [] authData = mStreamUpData.toByteArray(); if (!mAuthor.doAuth(authKey, authData, result)){ throw new AuthException("Auth head failed"); } } InetSocketAddress target = new InetSocketAddress(addr, port); - mSession.set(target, false); + mSession.set(target.toString(), false); log.info("Connecting " + target + " from " + local.socket().getRemoteSocketAddress()); return target; } @@ -163,20 +142,13 @@ private InetSocketAddress parseHead(SocketChannel local) throws IOException, Cry private boolean readAuthHead(SocketChannel sc) throws IOException,CryptoException { int size = 0; - int total_size = 0; - int authHeadLen = mSM.mLenToRead[StateMachine.AUTH_HEAD]; + // Data len(2) + HMAC-SHA1 + int authHeadLen = HmacSHA1.AUTH_LEN + 2; mBufferWrap.prepare(authHeadLen); - //In fact it should be send together, but we'd better to ensure we could read full head. - while(mBuffer.hasRemaining()){ - size = sc.read(mBuffer); - if (size < 0) - break; - else - total_size += size; - } - if (total_size < authHeadLen){ + size = BufferHelper.readFormRemote(sc, mBuffer); + if (size < authHeadLen){ // Actually, we reach the end of stream. - if (total_size == 0) + if (size == 0) return true; throw new IOException("Auth head is too short"); @@ -185,13 +157,17 @@ private boolean readAuthHead(SocketChannel sc) throws IOException,CryptoExceptio mBufferWrap.prepare(2); mBuffer.put(result[0]); mBuffer.put(result[1]); - mSM.mLenToRead[StateMachine.DATA] = (int)(mBuffer.getShort(0)&0xFFFF); - mSM.nextState(); + mChunkLeft = (int)(mBuffer.getShort(0)&0xFFFF); + + // Windows ss may just send a empty package, handle it. + if (mChunkLeft == 0) { + mChunkCount++; + } // store the pre-calculated auth result System.arraycopy(result, 2, mExpectAuthResult, 0, HmacSHA1.AUTH_LEN); - mAuthData.reset(); + mStreamUpData.reset(); return false; } @@ -200,16 +176,12 @@ private boolean readAuthHead(SocketChannel sc) throws IOException,CryptoExceptio protected boolean send(SocketChannel source, SocketChannel target, int direct) throws IOException,CryptoException,AuthException { int size; - boolean chunkFinish = false; if (mOneTimeAuth && direct == Session.LOCAL2REMOTE) { - switch (mSM.getState()){ - case StateMachine.AUTH_HEAD: - return readAuthHead(source); - case StateMachine.DATA: - mBufferWrap.prepare(mSM.mLenToRead[StateMachine.DATA]); - break; - } + if (mChunkLeft == 0) + return readAuthHead(source); + else + mBufferWrap.prepare(mChunkLeft); }else{ mBufferWrap.prepare(); } @@ -219,14 +191,6 @@ protected boolean send(SocketChannel source, SocketChannel target, int direct) t mSession.record(size, direct); - if (mOneTimeAuth && direct == Session.LOCAL2REMOTE) - { - mSM.mLenToRead[StateMachine.DATA] -= size; - if (mSM.mLenToRead[StateMachine.DATA] == 0){ - chunkFinish = true; - mSM.nextState(); - } - } byte [] result; if (direct == Session.LOCAL2REMOTE) { result = mCryptor.decrypt(mBuffer.array(), size); @@ -235,10 +199,11 @@ protected boolean send(SocketChannel source, SocketChannel target, int direct) t } if (mOneTimeAuth && direct == Session.LOCAL2REMOTE) { - mAuthData.write(result, 0, size); - if (chunkFinish) { + mStreamUpData.write(result, 0, size); + mChunkLeft -= size; + if (mChunkLeft == 0) { byte [] authKey = SSAuth.prepareKey(mCryptor.getIV(false), mChunkCount); - byte [] authData = mAuthData.toByteArray(); + byte [] authData = mStreamUpData.toByteArray(); if (!mAuthor.doAuth(authKey, authData, mExpectAuthResult)){ throw new AuthException("Auth chunk " + mChunkCount + " failed!"); } @@ -246,14 +211,9 @@ protected boolean send(SocketChannel source, SocketChannel target, int direct) t } } ByteBuffer out = ByteBuffer.wrap(result); - //Add timeout to avoid 100% cpu when write failed for a long time. - long timeout = System.currentTimeMillis() + 10*1000L; - while(out.hasRemaining()) { - target.write(out); - if (System.currentTimeMillis() > timeout) { - mSession.dump(log, new IOException("Some data send failed.")); - return true; - } + if (!BufferHelper.writeToRemote(target, out)) { + mSession.dump(log, new IOException("Some data send failed.")); + return true; } return false; } @@ -261,7 +221,7 @@ protected boolean send(SocketChannel source, SocketChannel target, int direct) t protected InetSocketAddress getRemoteAddress(SocketChannel local) throws IOException, CryptoException, AuthException { - return parseHead(local); + return parseHeader(local); } @Override protected void preTcpRelay(SocketChannel local, SocketChannel remote) @@ -278,9 +238,8 @@ protected void postTcpTelay(SocketChannel local, SocketChannel remote) @Override protected void localInit() throws Exception{ // for one time auth - mSM = new StateMachine(); mAuthor = new HmacSHA1(); - mAuthData = new ByteArrayOutputStream(); + mStreamUpData = new ByteArrayOutputStream(); mExpectAuthResult = new byte[HmacSHA1.AUTH_LEN]; } diff --git a/src/main/java/shadowsocks/nio/tcp/Session.java b/src/main/java/shadowsocks/nio/tcp/Session.java index e0e32c8..0eedfcf 100644 --- a/src/main/java/shadowsocks/nio/tcp/Session.java +++ b/src/main/java/shadowsocks/nio/tcp/Session.java @@ -45,9 +45,9 @@ private static void dec(){ } //For server is client IP, for client is local IP(from LAN/localhost) - private SocketAddress mLocal; + private String mLocal; //Target IP - private SocketAddress mRemote; + private String mRemote; //Stream up private int mL2RSize; @@ -57,7 +57,7 @@ private static void dec(){ //Current Session number as ID private int mSessionID; - public void set(SocketAddress addr, boolean local) { + public void set(String addr, boolean local) { if (local) mLocal = addr; else diff --git a/src/main/java/shadowsocks/nio/tcp/TcpWorker.java b/src/main/java/shadowsocks/nio/tcp/TcpWorker.java index 8781348..6e40556 100644 --- a/src/main/java/shadowsocks/nio/tcp/TcpWorker.java +++ b/src/main/java/shadowsocks/nio/tcp/TcpWorker.java @@ -115,7 +115,7 @@ protected void doTcpRelay(Selector selector, SocketChannel local, SocketChannel protected void TcpRelay(SocketChannel local) throws IOException, CryptoException, AuthException { - int CONNECT_TIMEOUT = 3000; + int CONNECT_TIMEOUT = 5000; //For local this is server address, get from config //For server this is target address, get from parse head. @@ -139,7 +139,7 @@ protected void TcpRelay(SocketChannel local) throws IOException, CryptoException postTcpTelay(local, remote); }catch(SocketTimeoutException e){ - log.warn("Remote address " + remoteAddress + " is unreachable", e); + log.warn("Connect " + remoteAddress + " timeout."); }catch(InterruptedException e){ //ignore }catch(IOException | CryptoException e){ @@ -155,7 +155,7 @@ public void run(){ try(SocketChannel local = mLocal){ mSession = new Session(); - mSession.set(local.socket().getRemoteSocketAddress(), true); + mSession.set(local.socket().getRemoteSocketAddress().toString(), true); mCryptor = CryptoFactory.create(Config.get().getMethod(), Config.get().getPassword()); diff --git a/src/main/java/shadowsocks/util/Config.java b/src/main/java/shadowsocks/util/Config.java index 1d49146..acf747b 100644 --- a/src/main/java/shadowsocks/util/Config.java +++ b/src/main/java/shadowsocks/util/Config.java @@ -133,7 +133,7 @@ public Config() mPort = DEFAULT_PORT; mLocalPort = DEFAULT_LOCAL_PORT; mOneTimeAuth = false; - mIsServerMode = true; + mIsServerMode = false; mConfigFile = null; } diff --git a/src/test/java/shadowsocks/SystemTest.java b/src/test/java/shadowsocks/SystemTest.java index 8b3b97d..fecd83d 100644 --- a/src/test/java/shadowsocks/SystemTest.java +++ b/src/test/java/shadowsocks/SystemTest.java @@ -28,18 +28,23 @@ import shadowsocks.util.Config; import shadowsocks.Shadowsocks; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.net.Proxy; +import java.net.Socket; +import java.net.Proxy.Type; +import java.net.InetSocketAddress; +import java.net.URL; +import java.net.HttpURLConnection; +import java.util.Arrays; + public class SystemTest{ public static Logger log = LogManager.getLogger(SystemTest.class.getName()); - private static Shadowsocks mLocal; - private static Shadowsocks mServer; - - private static void createShadowsocks(){ - mServer = new Shadowsocks(true); - mLocal = new Shadowsocks(false); - } - @Before public void setUp(){ log.info("Set up"); @@ -48,7 +53,7 @@ public void setUp(){ Config.get().setServer("127.0.0.1"); Config.get().setPort(1024); Config.get().setLocalPort(2048); - createShadowsocks(); + Config.get().setOTAEnabled(true); } @After public void tearDown(){ @@ -56,17 +61,64 @@ public void tearDown(){ } @Test public void testStartStop() { + + Shadowsocks server = new Shadowsocks(true); + Shadowsocks local = new Shadowsocks(false); //Can't shutdown before boot. - assertFalse(mServer.shutdown()); + assertFalse(server.shutdown()); //Boot and shutdown - assertTrue(mServer.boot()); - assertTrue(mLocal.boot()); - assertTrue(mServer.shutdown()); - assertTrue(mLocal.shutdown()); + assertTrue(server.boot()); + assertTrue(local.boot()); + assertTrue(server.shutdown()); + assertTrue(local.shutdown()); //Boot again - assertTrue(mServer.boot()); + assertTrue(server.boot()); //Two instances is not allowed. - assertFalse(mServer.boot()); - assertTrue(mServer.shutdown()); + assertFalse(server.boot()); + assertTrue(server.shutdown()); + + } + + private void testSimpleHttp(boolean ota) { + + Config.get().setOTAEnabled(ota); + + Shadowsocks server = new Shadowsocks(true); + Shadowsocks local = new Shadowsocks(false); + + assertTrue(server.boot()); + assertTrue(local.boot()); + Proxy proxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("127.0.0.1", 2048)); + HttpURLConnection conn = null; + try{ + 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")); + byte [] expect = new byte[8192]; + in2.read(expect); + assertTrue(Arrays.equals(result, expect)); + }catch(IOException e){ + log.error("Failed with exception.", e); + fail(); + }finally{ + if (conn != null) { + conn.disconnect(); + } + assertTrue(server.shutdown()); + assertTrue(local.shutdown()); + } + } + @Test + public void testHttp() { + testSimpleHttp(true); + } + + @Test + public void testHttpWithoutOTA() { + testSimpleHttp(false); } } diff --git a/src/test/resources/result-example-com b/src/test/resources/result-example-com new file mode 100644 index 0000000..e1624e8 --- /dev/null +++ b/src/test/resources/result-example-com @@ -0,0 +1,50 @@ + + + + Example Domain + + + + + + + + +
+

Example Domain

+

This domain is established to be used for illustrative examples in documents. You may use this + domain in examples without prior coordination or asking for permission.

+

More information...

+
+ +