/* * Copyright 2017 ZhangJiupeng * * 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 cc.agentx.util.proxy.socks5.io; import java.io.*; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /* * a single class version of socks5 proxy * block-io with cached thread pool */ public class Socks5Proxy extends Thread { public static final int ATYP_IP_V4 = 0x1; public static final int ATYP_DOMAIN_NAME = 0x3; public static final int ATYP_IP_V6 = 0x4; public static final byte[] MSG_REJECT_SOCKS4 = {0, 0x5b}; public static final byte[] MSG_ECHO = {0x5, 0}; public static final byte[] MSG_VERIFY = {0x5, 0, 0, 0x1, 0, 0, 0, 0, 0, 0}; private static final int SOCKS_VERSION_5 = 0x5; private static final int SOCKS_VERSION_4 = 0x4; public static boolean _DEBUG_MODE = false; private static Random random = new Random(); private Class<? extends FilterInputStream> inputStreamClazz = BufferedInputStream.class; private Class<? extends FilterOutputStream> outputStreamClazz = BufferedOutputStream.class; private ExecutorService cachedThreadPool; private InetSocketAddress serverAddress; private ServerSocket serverSocket; private PrintStream out, err; public Socks5Proxy() { this(1080); } public Socks5Proxy(int port) { this(null, port); } public Socks5Proxy(String host) { this(host, 1080); } public Socks5Proxy(String host, int port) { if (_DEBUG_MODE) out = System.out; cachedThreadPool = Executors.newCachedThreadPool(); serverAddress = host == null ? new InetSocketAddress(port) : new InetSocketAddress(host, port); } /** * Reserve for extensions * * @param inputStreamClazz user-defined inputStream * @param outputStreamClazz user-defined outputStream */ public Socks5Proxy(String host, int port, Class<? extends FilterInputStream> inputStreamClazz, Class<? extends FilterOutputStream> outputStreamClazz) { this(host, port); this.inputStreamClazz = inputStreamClazz; this.outputStreamClazz = outputStreamClazz; } public void setInputStreamClazz(Class<? extends FilterInputStream> inputStreamClazz) { this.inputStreamClazz = inputStreamClazz; } public void setOutputStreamClazz(Class<? extends FilterOutputStream> outputStreamClazz) { this.outputStreamClazz = outputStreamClazz; } public void run() { Thread.currentThread().setPriority(Thread.MAX_PRIORITY); try { serverSocket = new ServerSocket(); serverSocket.bind(serverAddress, 50); } catch (IOException e) { warn("SOCKS5Proxy: " + e.getMessage()); } if (serverSocket.isBound()) { log("SOCKS5Proxy: Waiting for connections on " + serverSocket.getLocalSocketAddress() + "..."); while (!serverSocket.isClosed()) { try { Socket socket = serverSocket.accept(); socket.setTcpNoDelay(true); // avoid nagle algorithm socket.setKeepAlive(true); cachedThreadPool.execute(getSocks5Tunnel(socket)); } catch (IOException e) { e.printStackTrace(); } } } else { warn("SOCKS5Proxy: Socket bind failure."); Thread.currentThread().interrupt(); } } private void log(String text) { if (out != null) out.println(text); } private void warn(String text) { if (err != null) err.println(text); else System.err.println(text); } public Socks5Proxy setOut(PrintStream out) { this.out = out; return this; } public Socks5Proxy setErr(PrintStream err) { this.err = err; return this; } public Socks5Tunnel getSocks5Tunnel(Socket socket) { return new Socks5Tunnel(socket); } private int getRandomIdentifier(int length) { int base = (int) Math.pow(10, length - 1); return random.nextInt(base * 9) + base; } class Socks5Tunnel implements Runnable { // adaption parameters - for optimize, read the code before changing them private static final int LOCAL_TIME_OUT_MILLIS = 0; private static final int REMOTE_TIME_OUT_MILLIS = 30 * 1000; private static final int HANDSHAKE_BUF_SIZE = 64; private static final int UPLOAD_BUF_SIZE = 256 * 1024; // 256kb private static final int DOWNLOAD_BUF_SIZE = 1024 * 1024; // 1mb private int registerNumber; private long pollingDelayMillis; private InetSocketAddress registerAddress; private Socket localSocket; private FilterInputStream inputStream; private FilterOutputStream outputStream; public Socks5Tunnel(Socket socket) { this.registerNumber = getRandomIdentifier(5); this.localSocket = socket; } @Override public void run() { // local handshake log(registerNumber + "\tClient -> Proxy \tFrom " + localSocket.getRemoteSocketAddress().toString().substring(1)); try { handshake(localSocket, this.inputStream = inputStreamClazz.getConstructor(InputStream.class).newInstance(localSocket.getInputStream()), this.outputStream = outputStreamClazz.getConstructor(OutputStream.class).newInstance(localSocket.getOutputStream()) ); } catch (Exception e) { warn(registerNumber + "\tBad Handshake! (" + e.getMessage() + ")"); try { localSocket.close(); log(registerNumber + "\tClient <- Proxy \tDisconnect"); } catch (IOException ignored) { } return; } // connect to remote Socket remoteSocket = new Socket(); try { log(registerNumber + "\t Proxy -> Target \tPing"); long datum = System.currentTimeMillis(); remoteSocket.connect(registerAddress, REMOTE_TIME_OUT_MILLIS); remoteSocket.setKeepAlive(localSocket.getKeepAlive()); long shift = System.currentTimeMillis() - datum; pollingDelayMillis = Math.max(50, shift); // slow down the polling frequency to save CPU resources log(registerNumber + "\t Proxy <- Target \tPong " + ((pollingDelayMillis <= 50) ? "" : "~" + pollingDelayMillis + "ms")); pollingDelayMillis = Math.max(pollingDelayMillis, 600); // avoid slow transmission (50 <= delay <= 600) } catch (Exception e) { warn(registerNumber + "\tBad Connection! (" + e.getMessage() + ")"); try { remoteSocket.close(); localSocket.close(); log(registerNumber + "\tClient <- Proxy \tDisconnect"); } catch (IOException ignored) { } return; } // transmit data via tunnel cachedThreadPool.execute(getDownloaderInstance(localSocket, outputStream, remoteSocket, pollingDelayMillis)); cachedThreadPool.execute(getUploaderInstance(localSocket, inputStream, remoteSocket, pollingDelayMillis)); } private void handshake(Socket localSocket, InputStream localIn, OutputStream localOut) throws Exception { byte[] buffer = new byte[HANDSHAKE_BUF_SIZE]; // get rid of zombie connections from local endpoint localSocket.setSoTimeout(LOCAL_TIME_OUT_MILLIS); // recognize protocol int length = localIn.read(buffer); if (length <= 0) { throw new IOException("connection refused"); } if (buffer[0] != SOCKS_VERSION_5) { if (_DEBUG_MODE) { String hexString = ""; for (byte b : buffer) { hexString += String.format("%02X", b); } warn(registerNumber + "\t<Unrecognized Data> HEX\t" + hexString); if (Character.isLetterOrDigit(buffer[0])) { warn(registerNumber + "\t TXT\t" + new String(buffer)); } } if (buffer[0] == SOCKS_VERSION_4) { localOut.write(Socks5Proxy.MSG_REJECT_SOCKS4); localOut.flush(); } throw new IOException(String.format("protocol version not supported: 0x%2X", buffer[0] & 0xFF)); } // send echo - no authentication support because its rarely been used or supported localOut.write(Socks5Proxy.MSG_ECHO); localOut.flush(); // parse dst address length = localIn.read(buffer); if (length <= 0) { throw new IOException("connection refused"); } switch (buffer[3]) { case ATYP_IP_V4: signByIPv4(buffer); break; case ATYP_DOMAIN_NAME: signByDomain(buffer, length); break; case ATYP_IP_V6: signByIPv6(buffer); default: throw new RuntimeException("unknown ATYP field (" + (buffer[3] & 0xFF) + ")"); } // echo validity localOut.write(Socks5Proxy.MSG_VERIFY); localOut.flush(); } private void signByIPv4(byte[] cReq) throws Exception { String ip = "" + (cReq[4] & 0xFF) + '.' + (cReq[5] & 0xFF) + '.' + (cReq[6] & 0xFF) + '.' + (cReq[7] & 0xFF); int port = ((cReq[8] & 0xFF) << 8) + (cReq[9] & 0xFF); log(registerNumber + "\tClient -> Proxy \tTarget " + ip + ":" + port); registerAddress = new InetSocketAddress(ip, port); } private void signByDomain(byte[] cReq, int length) throws Exception { String domain = new String(cReq, 5, length - 7); int port = ((cReq[length - 2] & 0xFF) << 8) + (cReq[length - 1] & 0xFF); log(registerNumber + "\tClient -> Proxy \tTarget " + domain + ":" + port); registerAddress = new InetSocketAddress(domain, port); } private void signByIPv6(byte[] cReq) throws Exception { String ip = String.format( "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x", cReq[4] & 0xFF , cReq[5] & 0xFF, cReq[6] & 0xFF, cReq[7] & 0xFF, cReq[8] & 0xFF, cReq[9] & 0xFF , cReq[10] & 0xFF, cReq[11] & 0xFF, cReq[12] & 0xFF, cReq[13] & 0xFF, cReq[14] & 0xFF , cReq[15] & 0xFF, cReq[16] & 0xFF, cReq[17] & 0xFF, cReq[18] & 0xFF, cReq[19] & 0xFF ); int port = ((cReq[20] & 0xFF) << 8) + (cReq[21] & 0xFF); log(registerNumber + "\tClient -> Proxy \tTarget " + ip + ":" + port); registerAddress = new InetSocketAddress(ip, port); } private Runnable getDownloaderInstance(Socket local, FilterOutputStream localOut, Socket remote, long delay) { return () -> { try { BufferedInputStream remoteInput = new BufferedInputStream(remote.getInputStream()); byte[] remoteBuffer = new byte[DOWNLOAD_BUF_SIZE]; int length; while (!remote.isClosed()) { length = remoteInput.read(remoteBuffer); if (length > 0) { localOut.write(remoteBuffer, 0, length); localOut.flush(); log(registerNumber + "\tClient <========== Target \tGet [" + length + " bytes]"); sleep(delay); } else if (length == -1) { break; } } } catch (IOException e) { try { remote.close(); local.close(); } catch (IOException e0) { e0.printStackTrace(); } } finally { try { local.close(); } catch (IOException e) { e.printStackTrace(); } // log(registerNumber + "\tClient <- Proxy \tDisconnect [Downloader]"); log(registerNumber + "\tClient <- Proxy \tDisconnect"); } }; } private Runnable getUploaderInstance(Socket local, FilterInputStream localIn, Socket remote, long delay) { return () -> { try { byte[] buffer = new byte[UPLOAD_BUF_SIZE]; BufferedOutputStream outputStream = new BufferedOutputStream(remote.getOutputStream()); int length; while (!local.isClosed()) { length = localIn.read(buffer); if (length > 0) { outputStream.write(buffer, 0, length); outputStream.flush(); log(registerNumber + "\tClient ==========> Target \tSend [" + length + " bytes]"); sleep(delay); } else if (length == -1) { break; } } } catch (IOException e) { try { local.close(); remote.close(); } catch (IOException e0) { e0.printStackTrace(); } } finally { try { remote.close(); } catch (IOException e) { e.printStackTrace(); } // log(registerNumber + "\tClient <- Proxy \tDisconnect [Uploader]"); } }; } private void sleep(long timeMillis) { try { Thread.sleep(timeMillis); } catch (InterruptedException e) { e.printStackTrace(); } } } }