/* * The Dragonite Project * ------------------------- * See the LICENSE file in the root directory for license information. */ package com.vecsight.dragonite.proxy.network.client; import com.vecsight.dragonite.mux.conn.MultiplexedConnection; import com.vecsight.dragonite.mux.conn.Multiplexer; import com.vecsight.dragonite.mux.exception.ConnectionAlreadyExistsException; import com.vecsight.dragonite.mux.exception.MultiplexerClosedException; import com.vecsight.dragonite.proxy.acl.ACLItemMethod; import com.vecsight.dragonite.proxy.acl.ParsedACL; import com.vecsight.dragonite.proxy.config.ProxyClientConfig; import com.vecsight.dragonite.proxy.exception.IncorrectHeaderException; import com.vecsight.dragonite.proxy.exception.SOCKS5Exception; import com.vecsight.dragonite.proxy.exception.ServerRejectedException; import com.vecsight.dragonite.proxy.header.ClientInfoHeader; import com.vecsight.dragonite.proxy.header.ServerResponseHeader; import com.vecsight.dragonite.proxy.misc.ProxyGlobalConstants; import com.vecsight.dragonite.proxy.network.StreamPipe; import com.vecsight.dragonite.proxy.network.socks5.SOCKS5Header; import com.vecsight.dragonite.proxy.network.socks5.SOCKS5SocketHelper; import com.vecsight.dragonite.sdk.config.DragoniteSocketParameters; import com.vecsight.dragonite.sdk.exception.ConnectionNotAliveException; import com.vecsight.dragonite.sdk.exception.DragoniteException; import com.vecsight.dragonite.sdk.exception.IncorrectSizeException; import com.vecsight.dragonite.sdk.exception.SenderClosedException; import com.vecsight.dragonite.sdk.socket.DragoniteClientSocket; import com.vecsight.dragonite.utils.system.SystemInfo; import com.vecsight.dragonite.utils.type.UnitConverter; import org.pmw.tinylog.Logger; import java.io.IOException; import java.net.*; public class ProxyClient { private final InetSocketAddress remoteAddress; private final int socks5port; private final int downMbps, upMbps; private final DragoniteSocketParameters dragoniteSocketParameters; private volatile boolean doAccept = true; private final ServerSocket serverSocket; private volatile DragoniteClientSocket dragoniteClientSocket; private volatile Multiplexer multiplexer; private final Thread acceptThread; private volatile Thread muxReceiveThread; private short nextConnID = 0; //single-threaded internal private final Object connectLock = new Object(); private final ParsedACL acl; private volatile boolean closed = false; public ProxyClient(final ProxyClientConfig config) throws IOException, InterruptedException, DragoniteException, ServerRejectedException, IncorrectHeaderException { this.remoteAddress = config.getRemoteAddress(); this.socks5port = config.getSocks5port(); this.downMbps = config.getDownMbps(); this.upMbps = config.getUpMbps(); this.acl = config.getAcl(); this.dragoniteSocketParameters = config.getDragoniteSocketParameters(); if (acl != null) { Logger.info("ACL loaded: {} by {}", acl.getTitle(), acl.getAuthor()); } serverSocket = new ServerSocket(socks5port); prepareUnderlyingConnection(dragoniteSocketParameters); acceptThread = new Thread(() -> { Socket socket; try { while (doAccept && (socket = serverSocket.accept()) != null) { final Socket finalSocket = socket; final Thread socketHandleThread = new Thread(() -> handleConnection(finalSocket), "PC-ConnHandler"); socketHandleThread.start(); } } catch (final IOException e) { Logger.error(e, "Unable to accept TCP connections"); } }, "PC-Accept"); acceptThread.start(); } private void prepareUnderlyingConnection(final DragoniteSocketParameters dragoniteSocketParameters) throws IOException, InterruptedException, DragoniteException, ServerRejectedException, IncorrectHeaderException { dragoniteClientSocket = new DragoniteClientSocket(remoteAddress, UnitConverter.mbpsToSpeed(upMbps), dragoniteSocketParameters); dragoniteClientSocket.setDescription("Proxy"); try { //Send info header final byte[] infoHeaderBytes = new ClientInfoHeader(downMbps, upMbps, SystemInfo.getUsername(), ProxyGlobalConstants.APP_VERSION, SystemInfo.getOS()).toBytes(); dragoniteClientSocket.send(infoHeaderBytes); //Receive response final byte[] response = dragoniteClientSocket.read(); final ServerResponseHeader responseHeader = new ServerResponseHeader(response); //Check response if (responseHeader.getStatus() != 0) { Logger.error("The server has rejected this connection (Error code {}): {}", responseHeader.getStatus(), responseHeader.getMsg()); throw new ServerRejectedException(responseHeader.getMsg()); } else if (responseHeader.getMsg().length() > 0) { Logger.info("Server welcome message: {}", responseHeader.getMsg()); } } catch (final InterruptedException | IOException | DragoniteException | IncorrectHeaderException | ServerRejectedException e) { Logger.error(e, "Unable to connect to remote server"); try { dragoniteClientSocket.closeGracefully(); } catch (InterruptedException | SenderClosedException | IOException ignored) { } throw e; } multiplexer = new Multiplexer(bytes -> { try { dragoniteClientSocket.send(bytes); } catch (InterruptedException | IncorrectSizeException | IOException | SenderClosedException e) { Logger.error(e, "Multiplexer is unable to send data"); } }, ProxyGlobalConstants.MAX_FRAME_SIZE); if (muxReceiveThread != null) muxReceiveThread.interrupt(); muxReceiveThread = new Thread(() -> { byte[] buf; try { while ((buf = dragoniteClientSocket.read()) != null) { multiplexer.onReceiveBytes(buf); } } catch (InterruptedException | ConnectionNotAliveException e) { Logger.error(e, "Cannot receive data from underlying socket"); } finally { synchronized (connectLock) { try { dragoniteClientSocket.closeGracefully(); } catch (final Exception ignored) { } multiplexer.close(); } } }, "PC-MuxReceive"); muxReceiveThread.start(); Logger.info("Connection established with {}", remoteAddress.toString()); } private void handleConnection(final Socket socket) { Logger.debug("New connection from {}", socket.getRemoteSocketAddress().toString()); //Parse header final SOCKS5Header socks5Header; try { socks5Header = SOCKS5SocketHelper.getHeader(socket); } catch (final IOException | SOCKS5Exception e) { Logger.error(e, "Failed to parse SOCKS5 request"); try { socket.close(); } catch (final IOException ignored) { } return; } Logger.debug("Parsed SOCKS5 request: {}", socks5Header.toString()); //Check ACL final ACLItemMethod method; if (!socks5Header.isUdp()) { if (acl != null) { if (socks5Header.isDomain()) { method = acl.checkDomain(new String(socks5Header.getAddr(), ProxyGlobalConstants.HEADER_ADDRESS_CHARSET)); } else { method = acl.checkIP(socks5Header.getAddr()); } } else { method = ACLItemMethod.PROXY; } } else { method = ACLItemMethod.PROXY; //We handle UDP rules later, inside ProxyConnectionHandler & ProxyClientUDPRelay } //Connect if (method == ACLItemMethod.PROXY) { final MultiplexedConnection multiplexedConnection; //Check reconnect & create mux connection synchronized (connectLock) { if (!closed) { if (!dragoniteClientSocket.isAlive()) { multiplexer.close(); Logger.warn("The underlying connection is no longer alive, reconnecting"); try { prepareUnderlyingConnection(dragoniteSocketParameters); } catch (IOException | InterruptedException | DragoniteException | IncorrectHeaderException | ServerRejectedException e) { Logger.error(e, "Unable to reconnect, there may be a network error or the server has been shut down"); try { socket.close(); } catch (final IOException ignored) { } return; } } } else { //ProxyClient already closed Logger.error("ProxyClient is already closed"); try { socket.close(); } catch (final IOException ignored) { } return; } try { multiplexedConnection = multiplexer.createConnection(nextConnID++); } catch (ConnectionAlreadyExistsException | MultiplexerClosedException e) { Logger.error(e, "Cannot create multiplexed connection"); try { socket.close(); } catch (final IOException ignored) { } return; } } final ProxyConnectionHandler handler = new ProxyConnectionHandler(socks5Header, multiplexedConnection, remoteAddress.getAddress(), socket, acl, dragoniteSocketParameters.getPacketCryptor()); handler.run(); } else if (method == ACLItemMethod.DIRECT) { //DIRECT local handleDirect(socks5Header, socket); } else { try { SOCKS5SocketHelper.sendRejected(socket); } catch (final IOException ignored) { } try { socket.close(); } catch (final IOException ignored) { } } } private void handleDirect(final SOCKS5Header socks5Header, final Socket socket) { final InetAddress remoteAddress; try { if (socks5Header.isDomain()) { remoteAddress = InetAddress.getByName(new String(socks5Header.getAddr(), ProxyGlobalConstants.HEADER_ADDRESS_CHARSET)); } else { remoteAddress = InetAddress.getByAddress(socks5Header.getAddr()); } } catch (final UnknownHostException e) { Logger.error(e, "Unknown host"); try { SOCKS5SocketHelper.sendFailed(socket); } catch (final IOException ignored) { } try { socket.close(); } catch (final IOException ignored) { } return; } //Let's connect then final InetSocketAddress socketAddress = new InetSocketAddress(remoteAddress, socks5Header.getPort()); Logger.debug("Connecting {}", socketAddress.toString()); final Socket remoteSocket = new Socket(); try { remoteSocket.connect(socketAddress, ProxyGlobalConstants.TCP_CONNECT_TIMEOUT_MS); } catch (final IOException e) { Logger.warn(e, "Unable to establish connection with {}", socketAddress.toString()); try { SOCKS5SocketHelper.sendFailed(socket); } catch (final IOException ignored) { } try { socket.close(); } catch (final IOException ignored) { } return; } //Send OK to SOCKS5 client try { SOCKS5SocketHelper.sendSucceed(socket); } catch (final IOException e) { Logger.error(e, "Unable to send response to SOCKS5 client"); try { socket.close(); } catch (final IOException ignored) { } return; } startPipe(socket, remoteSocket); } private void startPipe(final Socket socket, final Socket remoteSocket) { final Thread pipeFromRemoteThread = new Thread(() -> { final StreamPipe pipeFromRemotePipe = new StreamPipe(ProxyGlobalConstants.PIPE_BUFFER_SIZE); try { pipeFromRemotePipe.pipe(remoteSocket.getInputStream(), socket.getOutputStream()); } catch (final Exception e) { Logger.debug(e, "Pipe closed"); } finally { try { socket.close(); } catch (final IOException ignored) { } try { remoteSocket.close(); } catch (final IOException ignored) { } } }, "PC-DIRECT-R2L"); pipeFromRemoteThread.start(); final Thread pipeFromLocalThread = new Thread(() -> { final StreamPipe pipeFromLocalPipe = new StreamPipe(ProxyGlobalConstants.PIPE_BUFFER_SIZE); try { pipeFromLocalPipe.pipe(socket.getInputStream(), remoteSocket.getOutputStream()); } catch (final Exception e) { Logger.debug(e, "Pipe closed"); } finally { try { socket.close(); } catch (final IOException ignored) { } try { remoteSocket.close(); } catch (final IOException ignored) { } } }, "PC-DIRECT-L2R"); pipeFromLocalThread.start(); } public void close() { synchronized (connectLock) { if (!closed) { closed = true; acceptThread.interrupt(); doAccept = false; try { serverSocket.close(); } catch (final IOException ignored) { } muxReceiveThread.interrupt(); multiplexer.close(); try { dragoniteClientSocket.closeGracefully(); } catch (final InterruptedException | IOException | SenderClosedException ignored) { } } } } }