/* I2PSOCKSTunnel is released under the terms of the GNU GPL, * with an additional exception. For further details, see the * licensing terms in I2PTunnel.java. * * Copyright (c) 2004 by human */ package net.i2p.i2ptunnel.socks; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.net.SocketException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Properties; import net.i2p.I2PAppContext; import net.i2p.I2PException; import net.i2p.app.ClientApp; import net.i2p.app.ClientAppManager; import net.i2p.app.Outproxy; import net.i2p.client.streaming.I2PSocket; import net.i2p.client.streaming.I2PSocketOptions; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase; import net.i2p.i2ptunnel.I2PTunnel; import net.i2p.util.HexDump; import net.i2p.util.Log; /* * Class that manages SOCKS5 connections, and forwards them to * destination hosts or (eventually) some outproxy. * * @author human */ class SOCKS5Server extends SOCKSServer { private static final int SOCKS_VERSION_5 = 0x05; private boolean setupCompleted = false; private final boolean authRequired; /** * Create a SOCKS5 server that communicates with the client using * the specified socket. This method should not be invoked * directly: new SOCKS5Server objects should be created by using * SOCKSServerFactory.createSOCSKServer(). It is assumed that the * SOCKS VER field has been stripped from the input stream of the * client socket. * * @param clientSock client socket * @param props non-null */ public SOCKS5Server(I2PAppContext ctx, Socket clientSock, Properties props) { super(ctx, clientSock, props); this.authRequired = Boolean.parseBoolean(props.getProperty(I2PTunnelHTTPClientBase.PROP_AUTH)) && props.containsKey(I2PTunnelHTTPClientBase.PROP_USER) && props.containsKey(I2PTunnelHTTPClientBase.PROP_PW); } public Socket getClientSocket() throws SOCKSException { setupServer(); return clientSock; } protected void setupServer() throws SOCKSException { if (setupCompleted) { return; } DataInputStream in; DataOutputStream out; try { in = new DataInputStream(clientSock.getInputStream()); out = new DataOutputStream(clientSock.getOutputStream()); init(in, out); if (manageRequest(in, out) == Command.UDP_ASSOCIATE) handleUDP(in, out); } catch (IOException e) { throw new SOCKSException("Connection error: " + e); } setupCompleted = true; } /** * SOCKS5 connection initialization. This method assumes that * SOCKS "VER" field has been stripped from the input stream. */ private void init(DataInputStream in, DataOutputStream out) throws IOException, SOCKSException { int nMethods = in.readUnsignedByte(); int method = Method.NO_ACCEPTABLE_METHODS; for (int i = 0; i < nMethods; ++i) { int meth = in.readUnsignedByte(); if (((!authRequired) && meth == Method.NO_AUTH_REQUIRED) || (authRequired && meth == Method.USERNAME_PASSWORD)) { // That's fine, we do support this method method = meth; } } switch (method) { case Method.USERNAME_PASSWORD: _log.debug("username/password authentication required"); sendInitReply(Method.USERNAME_PASSWORD, out); verifyPassword(in, out); return; case Method.NO_AUTH_REQUIRED: _log.debug("no authentication required"); sendInitReply(Method.NO_AUTH_REQUIRED, out); return; default: _log.debug("no suitable authentication methods found (" + Integer.toHexString(method) + ")"); sendInitReply(Method.NO_ACCEPTABLE_METHODS, out); throw new SOCKSException("Unsupported authentication method"); } } /** * Wait for the username/password message and verify or throw SOCKSException on failure * @since 0.8.2 */ private void verifyPassword(DataInputStream in, DataOutputStream out) throws IOException, SOCKSException { int c = in.readUnsignedByte(); if (c != AUTH_VERSION) { _log.logAlways(Log.WARN, "SOCKS proxy authentication failed"); throw new SOCKSException("Unsupported authentication version"); } c = in.readUnsignedByte(); if (c <= 0) { _log.logAlways(Log.WARN, "SOCKS proxy authentication failed"); throw new SOCKSException("Bad authentication"); } byte[] user = new byte[c]; String u = new String(user, "UTF-8"); in.readFully(user); c = in.readUnsignedByte(); if (c <= 0) { _log.logAlways(Log.WARN, "SOCKS proxy authentication failed, user: " + u); throw new SOCKSException("Bad authentication"); } byte[] pw = new byte[c]; in.readFully(pw); // Hopefully these are in UTF-8, since that's what our config file is in // these throw UnsupportedEncodingException which is an IOE String p = new String(pw, "UTF-8"); String configUser = props.getProperty(I2PTunnelHTTPClientBase.PROP_USER); String configPW = props.getProperty(I2PTunnelHTTPClientBase.PROP_PW); if ((!u.equals(configUser)) || (!p.equals(configPW))) { _log.logAlways(Log.WARN, "SOCKS proxy authentication failed, user: " + u); sendAuthReply(AUTH_FAILURE, out); throw new SOCKSException("SOCKS authorization failure"); } if (_log.shouldLog(Log.INFO)) _log.info("SOCKS authorization success, user: " + u); sendAuthReply(AUTH_SUCCESS, out); } /** * SOCKS5 request management. This method assumes that all the * stuff preceding or enveloping the actual request (e.g. protocol * initialization, integrity/confidentiality encapsulations, etc) * has been stripped out of the input/output streams. */ private int manageRequest(DataInputStream in, DataOutputStream out) throws IOException, SOCKSException { int socksVer = in.readUnsignedByte(); if (socksVer != SOCKS_VERSION_5) { _log.debug("error in SOCKS5 request (protocol != 5?)"); throw new SOCKSException("Invalid protocol version in request: " + socksVer); } int command = in.readUnsignedByte(); switch (command) { case Command.CONNECT: break; case Command.BIND: _log.debug("BIND command is not supported!"); sendRequestReply(Reply.COMMAND_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); throw new SOCKSException("BIND command not supported"); case Command.UDP_ASSOCIATE: /*** if(!Boolean.parseBoolean(tunnel.getOptions().getProperty("i2ptunnel.socks.allowUDP"))) { _log.debug("UDP ASSOCIATE command is not supported!"); sendRequestReply(Reply.COMMAND_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); throw new SOCKSException("UDP ASSOCIATE command not supported"); ***/ break; default: _log.debug("unknown command in request (" + Integer.toHexString(command) + ")"); sendRequestReply(Reply.COMMAND_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); throw new SOCKSException("Invalid command in request"); } // Reserved byte, should be 0x00 in.readByte(); addressType = in.readUnsignedByte(); switch (addressType) { case AddressType.IPV4: StringBuilder builder = new StringBuilder(); for (int i = 0; i < 4; ++i) { int octet = in.readUnsignedByte(); builder.append(Integer.toString(octet)); if (i != 3) { builder.append("."); } } connHostName = builder.toString(); // Check if the requested IP should be mapped to a domain name String mappedDomainName = getMappedDomainNameForIP(connHostName); if (mappedDomainName != null) { _log.debug("IPV4 address " + connHostName + " was mapped to domain name " + mappedDomainName); addressType = AddressType.DOMAINNAME; connHostName = mappedDomainName; } else if (command != Command.UDP_ASSOCIATE) _log.warn("IPV4 address type in request: " + connHostName + ". Is your client secure?"); break; case AddressType.DOMAINNAME: { int addrLen = in.readUnsignedByte(); if (addrLen == 0) { _log.debug("0-sized address length?"); throw new SOCKSException("Illegal DOMAINNAME length"); } byte addr[] = new byte[addrLen]; in.readFully(addr); connHostName = DataHelper.getUTF8(addr); } _log.debug("DOMAINNAME address type in request: " + connHostName); break; case AddressType.IPV6: if (command != Command.UDP_ASSOCIATE) { _log.warn("IP V6 address type in request! Is your client secure?" + " (IPv6 is not supported, anyway :-)"); sendRequestReply(Reply.ADDRESS_TYPE_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); throw new SOCKSException("IPV6 addresses not supported"); } break; default: _log.debug("unknown address type in request (" + Integer.toHexString(command) + ")"); sendRequestReply(Reply.ADDRESS_TYPE_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); throw new SOCKSException("Invalid addresses type in request"); } connPort = in.readUnsignedShort(); if (connPort == 0) { _log.debug("trying to connect to TCP port 0? Dropping!"); sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); throw new SOCKSException("Invalid port number in request"); } return command; } protected void confirmConnection() throws SOCKSException { DataOutputStream out; try { out = new DataOutputStream(clientSock.getOutputStream()); sendRequestReply(Reply.SUCCEEDED, AddressType.IPV4, InetAddress.getByName("127.0.0.1"), null, 1, out); } catch (IOException e) { throw new SOCKSException("Connection error: " + e); } } /** * Send the specified reply during SOCKS5 initialization */ private void sendInitReply(int replyCode, DataOutputStream out) throws IOException { byte[] reply = new byte[2]; reply[0] = SOCKS_VERSION_5; reply[1] = (byte) replyCode; if (_log.shouldLog(Log.DEBUG)) _log.debug("Sending init reply:\n" + HexDump.dump(reply)); out.write(reply); } /** * Send the specified reply during SOCKS5 authorization * @since 0.8.2 */ private void sendAuthReply(int replyCode, DataOutputStream out) throws IOException { byte[] reply = new byte[2]; reply[0] = AUTH_VERSION; reply[1] = (byte) replyCode; if (_log.shouldLog(Log.DEBUG)) _log.debug("Sending auth reply:\n" + HexDump.dump(reply)); out.write(reply); } /** * Send the specified reply to a request of the client. Either * one of inetAddr or domainName can be null, depending on * addressType. */ private void sendRequestReply(int replyCode, int addressType, InetAddress inetAddr, String domainName, int bindPort, DataOutputStream out) throws IOException { ByteArrayOutputStream reps = new ByteArrayOutputStream(); DataOutputStream dreps = new DataOutputStream(reps); dreps.write(SOCKS_VERSION_5); dreps.write(replyCode); // Reserved byte, should be 0x00 dreps.write(0x00); dreps.write(addressType); switch (addressType) { case AddressType.IPV4: dreps.write(inetAddr.getAddress()); break; case AddressType.DOMAINNAME: dreps.writeByte(domainName.length()); dreps.writeBytes(domainName); break; default: _log.error("unknown address type passed to sendReply() (" + Integer.toHexString(addressType) + ")!"); return; } dreps.writeShort(bindPort); byte[] reply = reps.toByteArray(); if (_log.shouldLog(Log.DEBUG)) { _log.debug("Sending request reply:\n" + HexDump.dump(reply)); } out.write(reply); } /** * Get an I2PSocket that can be used to send/receive 8-bit clean data * to/from the destination of the SOCKS connection. * * @return an I2PSocket connected with the destination */ public I2PSocket getDestinationI2PSocket(I2PSOCKSTunnel t) throws SOCKSException { setupServer(); if (connHostName == null) { _log.error("BUG: destination host name has not been initialized!"); throw new SOCKSException("BUG! See the logs!"); } if (connPort == 0) { _log.error("BUG: destination port has not been initialized!"); throw new SOCKSException("BUG! See the logs!"); } DataOutputStream out; // for errors try { out = new DataOutputStream(clientSock.getOutputStream()); } catch (IOException e) { throw new SOCKSException("Connection error: " + e); } // FIXME: here we should read our config file, select an // outproxy, and instantiate the proper socket class that // handles the outproxy itself (SOCKS4a, SOCKS5, HTTP CONNECT...). I2PSocket destSock; try { if (connHostName.toLowerCase(Locale.US).endsWith(".i2p")) { // Let's not do a new Dest for every request, huh? //I2PSocketManager sm = I2PSocketManagerFactory.createManager(); //destSock = sm.connect(I2PTunnel.destFromName(connHostName), null); Destination dest = _context.namingService().lookup(connHostName); if (dest == null) { try { sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe) {} throw new SOCKSException("Host not found"); } if (_log.shouldDebug()) _log.debug("connecting to " + connHostName + "..."); Properties overrides = new Properties(); I2PSocketOptions sktOpts = t.buildOptions(overrides); sktOpts.setPort(connPort); destSock = t.createI2PSocket(dest, sktOpts); } else if ("localhost".equals(connHostName) || "127.0.0.1".equals(connHostName)) { String err = "No localhost accesses allowed through the Socks Proxy"; _log.error(err); try { sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe) {} throw new SOCKSException(err); /**** } else if (connPort == 80) { // rewrite GET line to include hostname??? or add Host: line??? // or forward to local eepProxy (but that's a Socket not an I2PSocket) // use eepProxy configured outproxies? String err = "No handler for HTTP outproxy implemented"; _log.error(err); try { sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe) {} throw new SOCKSException(err); ****/ } else { Outproxy outproxy = getOutproxyPlugin(); if (outproxy != null) { // In HTTPClient, we use OutproxyRunner to run a Socket, // but here, we wrap a Socket in a I2PSocket and use the regular Runner. try { destSock = new SocketWrapper(outproxy.connect(connHostName, connPort)); } catch (IOException ioe) { try { sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe2) {} throw new SOCKSException("connect failed via outproxy plugin", ioe); } } else { List<String> proxies = t.getProxies(connPort); if (proxies == null || proxies.isEmpty()) { String err = "No outproxy configured for port " + connPort + " and no default configured either"; _log.error(err); try { sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe) {} throw new SOCKSException(err); } int p = _context.random().nextInt(proxies.size()); String proxy = proxies.get(p); if (_log.shouldLog(Log.DEBUG)) _log.debug("connecting to proxy " + proxy + " for " + connHostName + " port " + connPort); try { destSock = outproxyConnect(t, proxy); } catch (SOCKSException se) { try { sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe) {} throw se; } } } confirmConnection(); _log.debug("connection confirmed - exchanging data..."); } catch (DataFormatException e) { if (_log.shouldLog(Log.WARN)) _log.warn("socks error", e); try { sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe) {} throw new SOCKSException("Error in destination format"); } catch (SocketException e) { if (_log.shouldLog(Log.WARN)) _log.warn("socks error", e); try { sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe) {} throw new SOCKSException("Error connecting: " + e); } catch (IOException e) { if (_log.shouldLog(Log.WARN)) _log.warn("socks error", e); try { sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe) {} throw new SOCKSException("Error connecting: " + e); } catch (I2PException e) { if (_log.shouldLog(Log.WARN)) _log.warn("socks error", e); try { sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe) {} throw new SOCKSException("Error connecting: " + e); } return destSock; } /** * Act as a SOCKS 5 client to connect to an outproxy * @return open socket or throws error * @since 0.8.2 */ private I2PSocket outproxyConnect(I2PSOCKSTunnel tun, String proxy) throws IOException, SOCKSException, DataFormatException, I2PException { Properties overrides = new Properties(); overrides.setProperty("option.i2p.streaming.connectDelay", "1000"); I2PSocketOptions proxyOpts = tun.buildOptions(overrides); Destination dest = _context.namingService().lookup(proxy); if (dest == null) throw new SOCKSException("Outproxy not found"); I2PSocket destSock = tun.createI2PSocket(dest, proxyOpts); DataOutputStream out = null; DataInputStream in = null; try { out = new DataOutputStream(destSock.getOutputStream()); boolean authAvail = Boolean.parseBoolean(props.getProperty(I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH)); String configUser = null; String configPW = null; if (authAvail) { configUser = props.getProperty(I2PTunnelHTTPClientBase.PROP_OUTPROXY_USER_PREFIX + proxy); configPW = props.getProperty(I2PTunnelHTTPClientBase.PROP_OUTPROXY_PW_PREFIX + proxy); if (configUser == null || configPW == null) { configUser = props.getProperty(I2PTunnelHTTPClientBase.PROP_OUTPROXY_USER); configPW = props.getProperty(I2PTunnelHTTPClientBase.PROP_OUTPROXY_PW); if (configUser == null || configPW == null) authAvail = false; } } // send the init out.writeByte(SOCKS_VERSION_5); if (authAvail) { out.writeByte(2); out.writeByte(Method.NO_AUTH_REQUIRED); out.writeByte(Method.USERNAME_PASSWORD); } else { out.writeByte(1); out.writeByte(Method.NO_AUTH_REQUIRED); } out.flush(); // read init reply in = new DataInputStream(destSock.getInputStream()); // is this right or should we not try to do 5-to-4 conversion? int hisVersion = in.readByte(); if (hisVersion != SOCKS_VERSION_5 /* && addrtype == AddressType.DOMAINNAME */ ) throw new SOCKSException("SOCKS Outproxy is not Version 5"); //else if (hisVersion != 4) // throw new SOCKSException("Unsupported SOCKS Outproxy Version"); int method = in.readByte(); if (method == Method.NO_AUTH_REQUIRED) { // good } else if (method == Method.USERNAME_PASSWORD) { if (authAvail) { // send the auth out.writeByte(AUTH_VERSION); byte[] user = configUser.getBytes("UTF-8"); byte[] pw = configPW.getBytes("UTF-8"); out.writeByte(user.length); out.write(user); out.writeByte(pw.length); out.write(pw); out.flush(); // read the auth reply if (in.readByte() != AUTH_VERSION) throw new SOCKSException("Bad auth version from outproxy"); if (in.readByte() != AUTH_SUCCESS) throw new SOCKSException("Outproxy authorization failure"); } else { throw new SOCKSException("Outproxy requires authorization, please configure username/password"); } } else { throw new SOCKSException("Outproxy authorization failure"); } // send the connect command out.writeByte(SOCKS_VERSION_5); out.writeByte(Command.CONNECT); out.writeByte(0); // reserved out.writeByte(addressType); if (addressType == AddressType.IPV4) { out.write(InetAddress.getByName(connHostName).getAddress()); } else if (addressType == AddressType.DOMAINNAME) { byte[] d = connHostName.getBytes("ISO-8859-1"); out.writeByte(d.length); out.write(d); } else { // shouldn't happen throw new SOCKSException("Unknown address type for outproxy?"); } out.writeShort(connPort); out.flush(); // read the connect reply hisVersion = in.readByte(); if (hisVersion != SOCKS_VERSION_5) throw new SOCKSException("Outproxy response is not Version 5"); int reply = in.readByte(); in.readByte(); // reserved int type = in.readByte(); int count = 0; if (type == AddressType.IPV4) { count = 4; } else if (type == AddressType.DOMAINNAME) { count = in.readUnsignedByte(); } else if (type == AddressType.IPV6) { count = 16; } else { throw new SOCKSException("Unsupported address type in outproxy response"); } byte[] addr = new byte[count]; in.readFully(addr); // address in.readUnsignedShort(); // port if (reply != Reply.SUCCEEDED) throw new SOCKSException("Outproxy rejected request, response = " + reply); // throw away the address in the response // todo pass the response through? } catch (IOException e) { try { destSock.close(); } catch (IOException ioe) {} if (in != null) try { in.close(); } catch (IOException ioe) {} if (out != null) try { out.close(); } catch (IOException ioe) {} throw e; } catch (SOCKSException e) { try { destSock.close(); } catch (IOException ioe) {} if (in != null) try { in.close(); } catch (IOException ioe) {} if (out != null) try { out.close(); } catch (IOException ioe) {} throw e; } // that's it, caller will send confirmation to our client return destSock; } // This isn't really the right place for this, we can't stop the tunnel once it starts. private static SOCKSUDPTunnel _tunnel; private static final Object _startLock = new Object(); private static final byte[] dummyIP = new byte[4]; /** * We got a UDP associate command. * Loop here looking for more, never return normally, * or else I2PSocksTunnel will create a streaming lib connection. * * Do UDP Socks clients actually send more than one Associate request? * RFC 1928 isn't clear... maybe not. */ private void handleUDP(DataInputStream in, DataOutputStream out) throws SOCKSException { List<Integer> ports = new ArrayList<Integer>(1); synchronized (_startLock) { if (_tunnel == null) { // tunnel options? _tunnel = new SOCKSUDPTunnel(new I2PTunnel()); _tunnel.startRunning(); } } while (true) { // Set it up. connHostName and connPort are the client's info. InetAddress ia = null; try { ia = InetAddress.getByAddress(connHostName, dummyIP); } catch (UnknownHostException uhe) {} // won't happen, no resolving done here int myPort = _tunnel.add(ia, connPort); ports.add(Integer.valueOf(myPort)); try { sendRequestReply(Reply.SUCCEEDED, AddressType.IPV4, InetAddress.getByName("127.0.0.1"), null, myPort, out); } catch (IOException ioe) { break; } // wait for more ??? try { int command = manageRequest(in, out); // don't do this... if (command != Command.UDP_ASSOCIATE) break; } catch (IOException ioe) { break; } catch (SOCKSException ioe) { break; } } for (Integer i : ports) _tunnel.remove(i); // Prevent I2PSocksTunnel from calling getDestinationI2PSocket() above // to create a streaming lib connection... // This isn't very elegant... // throw new SOCKSException("End of UDP Processing"); } /* * Some namespaces to enclose SOCKS protocol codes */ private static class Method { private static final int NO_AUTH_REQUIRED = 0x00; private static final int USERNAME_PASSWORD = 0x02; private static final int NO_ACCEPTABLE_METHODS = 0xff; } private static class AddressType { private static final int IPV4 = 0x01; private static final int DOMAINNAME = 0x03; private static final int IPV6 = 0x04; } private static class Command { private static final int CONNECT = 0x01; private static final int BIND = 0x02; private static final int UDP_ASSOCIATE = 0x03; } private static class Reply { private static final int SUCCEEDED = 0x00; private static final int GENERAL_SOCKS_SERVER_FAILURE = 0x01; private static final int CONNECTION_NOT_ALLOWED_BY_RULESET = 0x02; private static final int NETWORK_UNREACHABLE = 0x03; private static final int HOST_UNREACHABLE = 0x04; private static final int CONNECTION_REFUSED = 0x05; private static final int TTL_EXPIRED = 0x06; private static final int COMMAND_NOT_SUPPORTED = 0x07; private static final int ADDRESS_TYPE_NOT_SUPPORTED = 0x08; } private static final int AUTH_VERSION = 1; private static final int AUTH_SUCCESS = 0; private static final int AUTH_FAILURE = 1; }