diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java index 014a0beea..6101d15c5 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java @@ -986,8 +986,9 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { * @throws RequestTooLongException if too long * @throws BadRequestException on bad headers * @throws IOException on other errors in the underlying stream + * @since public since 0.9.57 for SOCKS */ - static Map> readHeaders(I2PSocket socket, InputStream in, StringBuilder command, + public static Map> readHeaders(I2PSocket socket, InputStream in, StringBuilder command, String[] skipHeaders, I2PAppContext ctx) throws IOException { HashMap> headers = new HashMap>(); StringBuilder buf = new StringBuilder(128); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java index a3ecbbc41..89f85dda1 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java @@ -23,6 +23,7 @@ import net.i2p.i2ptunnel.I2PTunnel; import net.i2p.i2ptunnel.I2PTunnelClientBase; import net.i2p.i2ptunnel.I2PTunnelRunner; import net.i2p.i2ptunnel.Logging; +import net.i2p.i2ptunnel.TunnelController; import net.i2p.socks.SOCKSException; import net.i2p.util.EventDispatcher; import net.i2p.util.Log; @@ -37,7 +38,12 @@ public class I2PSOCKSTunnel extends I2PTunnelClientBase { */ protected static final int INITIAL_SO_TIMEOUT = 15*1000; - private HashMap> proxies = null; // port# + "" or "default" -> hostname list + private final HashMap> proxies; // port# + "" or "default" -> hostname list + + /** @since 0.9.57 for storing passwords */ + public static final String AUTH_REALM = "I2P SOCKS Proxy"; + /** @since 0.9.57 */ + public static final String PROP_OUTPROXY_TYPE = "outproxyType"; //public I2PSOCKSTunnel(int localPort, Logging l, boolean ownDest) { // I2PSOCKSTunnel(localPort, l, ownDest, (EventDispatcher)null); @@ -57,6 +63,7 @@ public class I2PSOCKSTunnel extends I2PTunnelClientBase { opts.remove("i2p.streaming.maxWindowSize"); setName("SOCKS Proxy on " + tunnel.listenHost + ':' + localPort); + proxies = new HashMap>(1); parseOptions(); notifyEvent("openSOCKSTunnelResult", "ok"); } @@ -93,9 +100,27 @@ public class I2PSOCKSTunnel extends I2PTunnelClientBase { public static final String DEFAULT = "default"; public static final String PROP_PROXY_DEFAULT = PROP_PROXY_PREFIX + DEFAULT; + /** + * Update the outproxy list then call super. + * + * @since 0.9.57 + */ + @Override + public void optionsUpdated(I2PTunnel tunnel) { + if (getTunnel() != tunnel) + return; + proxies.clear(); + parseOptions(); + super.optionsUpdated(tunnel); + } + private void parseOptions() { Properties opts = getTunnel().getClientOptions(); - proxies = new HashMap>(1); + if (!opts.containsKey(PROP_PROXY_DEFAULT)) { + String proxyList = opts.getProperty(TunnelController.PROP_PROXIES); + if (proxyList != null) + opts.setProperty(PROP_PROXY_DEFAULT, proxyList); + } for (Map.Entry e : opts.entrySet()) { String prop = (String)e.getKey(); if ((!prop.startsWith(PROP_PROXY_PREFIX)) || prop.length() <= PROP_PROXY_PREFIX.length()) @@ -119,7 +144,7 @@ public class I2PSOCKSTunnel extends I2PTunnelClientBase { } public List getProxies(int port) { - List rv = proxies.get(port + ""); + List rv = proxies.get(Integer.toString(port)); if (rv == null) rv = getDefaultProxies(); return rv; diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java index f1c4d170a..83e1c40eb 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java @@ -18,6 +18,7 @@ import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Properties; import net.i2p.I2PAppContext; @@ -31,10 +32,15 @@ import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase; +import net.i2p.i2ptunnel.I2PTunnelHTTPServer; import net.i2p.i2ptunnel.I2PTunnel; import static net.i2p.socks.SOCKS5Constants.*; +import net.i2p.util.Addresses; import net.i2p.util.HexDump; +import net.i2p.util.LHMCache; import net.i2p.util.Log; +import net.i2p.util.PasswordManager; +import net.i2p.util.SipHash; import net.i2p.socks.SOCKS5Client; import net.i2p.socks.SOCKSException; @@ -42,12 +48,23 @@ import net.i2p.socks.SOCKSException; * Class that manages SOCKS5 connections, and forwards them to * destination hosts or (eventually) some outproxy. * + * Supports torsocks as of 0.9.57. + * * @author human */ class SOCKS5Server extends SOCKSServer { - private boolean setupCompleted = false; + private boolean setupCompleted; private final boolean authRequired; + /** + * torsocks support + * Cache of fake IPv4 255.x.x.x (as returned from RESOLVE) to hostname + * torsocks 2.3.0 does NOT support IPv6, even though Tor does now + * The fake IP is a partial SipHash of the hostname, so collisions aren't predictable. + * The IPs will change at restart, but torsocks doesn't appear to do any caching. + */ + private static final Map _torCache = new LHMCache(256); + private static final String[] _skipHeaders = new String[0]; /** * Create a SOCKS5 server that communicates with the client using @@ -63,9 +80,7 @@ class SOCKS5Server extends SOCKSServer { 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); + Boolean.parseBoolean(props.getProperty(I2PTunnelHTTPClientBase.PROP_AUTH)); } public Socket getClientSocket() throws SOCKSException { @@ -104,9 +119,10 @@ class SOCKS5Server extends SOCKSServer { for (int i = 0; i < nMethods; ++i) { int meth = in.readUnsignedByte(); if (((!authRequired) && meth == Method.NO_AUTH_REQUIRED) || - (authRequired && meth == Method.USERNAME_PASSWORD)) { + meth == Method.USERNAME_PASSWORD) { // That's fine, we do support this method method = meth; + break; } } @@ -116,10 +132,12 @@ class SOCKS5Server extends SOCKSServer { 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); @@ -143,8 +161,8 @@ class SOCKS5Server extends SOCKSServer { throw new SOCKSException("Bad authentication"); } byte[] user = new byte[c]; - String u = new String(user, "UTF-8"); in.readFully(user); + String u = DataHelper.getUTF8(user); c = in.readUnsignedByte(); if (c <= 0) { _log.logAlways(Log.WARN, "SOCKS proxy authentication failed, user: " + u); @@ -152,18 +170,25 @@ class SOCKS5Server extends SOCKSServer { } 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 (authRequired) { + // Hopefully these are in UTF-8, since that's what our config file is in + String p = DataHelper.getUTF8(pw); + String psha256 = I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_PREFIX + u + + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_SHA256_SUFFIX; + String configPW = props.getProperty(psha256); + String hex = PasswordManager.sha256Hex(I2PSOCKSTunnel.AUTH_REALM, u, p); + if (configPW == null || !configPW.equals(hex)) { + _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 + '"'); + // torsocks -i + // user "torsocks-77673:1668695377" pw "0" + //_log.info("PW: \"" + DataHelper.getUTF8(pw) + '"'); } - if (_log.shouldLog(Log.INFO)) - _log.info("SOCKS authorization success, user: " + u); sendAuthReply(AUTH_SUCCESS, out); } @@ -176,29 +201,48 @@ class SOCKS5Server extends SOCKSServer { private int manageRequest(DataInputStream in, DataOutputStream out) throws IOException { int socksVer = in.readUnsignedByte(); if (socksVer != SOCKS_VERSION_5) { - _log.debug("error in SOCKS5 request (protocol != 5?)"); + if (_log.shouldDebug()) + _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: + case Command.CONNECT: break; - case Command.BIND: - _log.debug("BIND command is not supported!"); + + case Command.BIND: + if (_log.shouldDebug()) + _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: + + 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) + ")"); + + case Command.TOR_RESOLVE: + // https://github.com/torproject/torspec/blob/main/socks-extensions.txt + // reply will be sent below + break; + + case Command.TOR_RESOLVE_PTR: + case Command.TOR_CONNECT_DIR: + // https://github.com/torproject/torspec/blob/main/socks-extensions.txt + if (_log.shouldDebug()) + _log.debug("Tor command unsupported (" + Integer.toHexString(command) + ")"); sendRequestReply(Reply.COMMAND_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); - throw new SOCKSException("Invalid command in request"); + throw new SOCKSException("Unsupported command in request"); + + default: + if (_log.shouldDebug()) + _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("Unsupported command in request"); } // Reserved byte, should be 0x00 @@ -206,54 +250,96 @@ class SOCKS5Server extends SOCKSServer { 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(); + case AddressType.IPV4: { + byte[] ip = new byte[4]; + in.readFully(ip); + connHostName = Addresses.toString(ip); // 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); + if (_log.shouldDebug()) + _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?"); + } else if (command != Command.UDP_ASSOCIATE) { + if (_log.shouldWarn()) + _log.warn("IPV4 address type in request: " + connHostName + ". Is your client secure?"); + } break; - case AddressType.DOMAINNAME: + } + + case AddressType.DOMAINNAME: { int addrLen = in.readUnsignedByte(); if (addrLen == 0) { - _log.debug("0-sized address length?"); + if (_log.shouldDebug()) + _log.debug("0-sized address length?"); throw new SOCKSException("Illegal DOMAINNAME length"); } byte addr[] = new byte[addrLen]; in.readFully(addr); - connHostName = DataHelper.getUTF8(addr); + String host = DataHelper.getUTF8(addr); + if (command == Command.TOR_RESOLVE) { + // For Tor, save hostname for the CONNECT + int hash = SipHash.hashCode(addr) & 0xffffff; + byte[] fake = new byte[4]; + fake[0] = (byte) 255; + DataHelper.toLong(fake, 1, 3, hash); + String fakeIP = Addresses.toString(fake); + String old; + synchronized(_torCache) { + old = _torCache.put(fakeIP, host); + } + if (old != null && !old.equals(host)) { + if (_log.shouldWarn()) + _log.warn("Hash collision " + old + " and " + host); + } + if (_log.shouldDebug()) + _log.debug("Cached host " + host + " at address " + fakeIP); + sendRequestReply(Reply.SUCCEEDED, AddressType.IPV4, InetAddress.getByName(fakeIP), null, 1, out); + throw new SOCKSException("ignore"); + } + //if (host.startsWith("4fff:")) { + if (host.startsWith("255.")) { + // For Tor, where hostname was sent previously in the RESOLVE + synchronized(_torCache) { + connHostName = _torCache.get(host); + } + if (connHostName == null) { + sendRequestReply(Reply.ADDRESS_TYPE_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); + throw new SOCKSException("No cache entry found for Tor IP " + host); + } + if (_log.shouldDebug()) + _log.debug("Using hostname from previous RESOLVE: " + connHostName); + } else { + connHostName = host; + } } - _log.debug("DOMAINNAME address type in request: " + connHostName); + if (_log.shouldDebug()) + _log.debug("DOMAINNAME address type in request: " + connHostName); break; - case AddressType.IPV6: + + 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"); + byte[] ip = new byte[16]; + in.readFully(ip); + connHostName = Addresses.toString(ip); + if (_log.shouldWarn()) + _log.warn("IPV6 address type in request! Is your client secure?"); } break; - default: - _log.debug("unknown address type in request (" + Integer.toHexString(command) + ")"); + + default: + if (_log.shouldDebug()) + _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!"); + if (_log.shouldDebug()) + _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"); } @@ -315,14 +401,17 @@ class SOCKS5Server extends SOCKSServer { dreps.write(addressType); switch (addressType) { - case AddressType.IPV4: + case AddressType.IPV4: + case AddressType.IPV6: dreps.write(inetAddr.getAddress()); break; - case AddressType.DOMAINNAME: + + case AddressType.DOMAINNAME: dreps.writeByte(domainName.length()); dreps.writeBytes(domainName); break; - default: + + default: _log.error("unknown address type passed to sendReply() (" + Integer.toHexString(addressType) + ")!"); return; } @@ -431,6 +520,7 @@ class SOCKS5Server extends SOCKSServer { } catch (IOException ioe) {} throw new SOCKSException(err); } + // TODO sticky proxy selection like in HTTP client int p = _context.random().nextInt(proxies.size()); String proxy = proxies.get(p); if (_log.shouldLog(Log.DEBUG)) @@ -475,6 +565,8 @@ class SOCKS5Server extends SOCKSServer { /** * Act as a SOCKS 5 client to connect to an outproxy + * Caller must send success or error to local socks client. + * * @return open socket or throws error * @since 0.8.2 */ @@ -482,6 +574,16 @@ class SOCKS5Server extends SOCKSServer { Properties overrides = new Properties(); overrides.setProperty("option.i2p.streaming.connectDelay", "200"); I2PSocketOptions proxyOpts = tun.buildOptions(overrides); + int proxyPort = 0; + int colon = proxy.indexOf(':'); + if (colon > 0) { + try { + proxyPort = Integer.parseInt(proxy.substring(colon + 1)); + if (proxyPort > 0) + proxyOpts.setPort(proxyPort); + } catch (NumberFormatException nfe) {} + proxy = proxy.substring(0, colon); + } Destination dest = _context.namingService().lookup(proxy); if (dest == null) throw new SOCKSException("Outproxy not found"); @@ -490,7 +592,6 @@ class SOCKS5Server extends SOCKSServer { InputStream in = null; try { out = destSock.getOutputStream(); - in = destSock.getInputStream(); boolean authAvail = Boolean.parseBoolean(props.getProperty(I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH)); String configUser = null; String configPW = null; @@ -502,7 +603,13 @@ class SOCKS5Server extends SOCKSServer { configPW = props.getProperty(I2PTunnelHTTPClientBase.PROP_OUTPROXY_PW); } } - SOCKS5Client.connect(in, out, connHostName, connPort, configUser, configPW); + boolean https = "connect".equals(props.getProperty(I2PSOCKSTunnel.PROP_OUTPROXY_TYPE)); + if (https) { + httpsConnect(destSock, out, connHostName, connPort, configUser, configPW); + } else { + in = destSock.getInputStream(); + SOCKS5Client.connect(in, out, connHostName, connPort, configUser, configPW); + } } catch (IOException e) { try { destSock.close(); } catch (IOException ioe) {} if (in != null) try { in.close(); } catch (IOException ioe) {} @@ -513,6 +620,49 @@ class SOCKS5Server extends SOCKSServer { return destSock; } + /** + * Act as a https client to connect to a CONNECT outproxy. + * + * Caller must send success or error to local socks client. + * Caller must close destSock and pout. + * + * @param destSock socket to the proxy + * @param pout output stream to the proxy + * @param connHostName hostname or IP for the proxy to connect to + * @param connPort port for the proxy to connect to + * @param configUser username unsupported + * @param configPW password unsupported + * @since 0.9.57 + */ + public void httpsConnect(I2PSocket destSock, OutputStream pout, String connHostName, + int connPort, String configUser, String configPW) throws IOException { + StringBuilder buf = new StringBuilder(64); + buf.append("CONNECT "); + boolean v6 = connHostName.contains(":"); + if (v6) + buf.append('['); + buf.append(connHostName); + if (v6) + buf.append(']'); + buf.append(':'); + buf.append(connPort); + buf.append(" HTTP/1.1\r\n\r\n"); + if (_log.shouldDebug()) + _log.debug("Request to outproxy: " + buf); + pout.write(DataHelper.getASCII(buf.toString())); + pout.flush(); + // eat the response and headers + buf.setLength(0); + I2PTunnelHTTPServer.readHeaders(destSock, null, buf, _skipHeaders, _context); + String[] f = DataHelper.split(buf.toString(), " ", 2); + if (f.length < 2) + throw new IOException("Bad response from proxy"); + if (!f[1].startsWith("200 ")) + throw new IOException("Error from proxy: " + f[1]); + if (_log.shouldDebug()) + _log.debug("Response from proxy: " + buf); + } + // 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(); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java index 29f8e0542..896a7df99 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java @@ -31,6 +31,7 @@ import net.i2p.i2ptunnel.I2PTunnelServer; import net.i2p.i2ptunnel.SSLClientUtil; import net.i2p.i2ptunnel.TunnelController; import net.i2p.i2ptunnel.TunnelControllerGroup; +import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel; import net.i2p.util.ConvertToHash; import net.i2p.util.FileUtil; import net.i2p.util.Log; @@ -983,6 +984,17 @@ public class GeneralHelper { return getBooleanProperty(tunnel, I2PTunnelHTTPClientBase.PROP_USE_OUTPROXY_PLUGIN, true); } + /** + * @return "connect" or "socks", default depends on tunnel type + * @since 0.9.57 + */ + public String getOutproxyType(int tunnel) { + String type = getTunnelType(tunnel); + if (!type.equals("sockstunnel") && !type.equals("socksirctunnel")) + return "connect"; + return getProperty(tunnel, I2PSOCKSTunnel.PROP_OUTPROXY_TYPE, "socks"); + } + /** all of these are @since 0.8.3 */ public int getLimitMinute(int tunnel) { return getProperty(tunnel, TunnelController.PROP_MAX_CONNS_MIN, TunnelController.DEFAULT_MAX_CONNS_MIN); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/TunnelConfig.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/TunnelConfig.java index 7b4e296bd..a861ea042 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/TunnelConfig.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/TunnelConfig.java @@ -32,6 +32,7 @@ import net.i2p.i2ptunnel.I2PTunnelHTTPServer; import net.i2p.i2ptunnel.I2PTunnelIRCClient; import net.i2p.i2ptunnel.I2PTunnelServer; import net.i2p.i2ptunnel.TunnelController; +import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel; import net.i2p.util.ConcurrentHashSet; import net.i2p.util.PasswordManager; @@ -586,6 +587,15 @@ public class TunnelConfig { else _booleanOptions.remove(I2PTunnelHTTPClientBase.PROP_USE_OUTPROXY_PLUGIN); } + + /** + * @param s "connect" or "socks" + * @since 0.9.57 + */ + public void setOutproxyType(String s) { + if (s != null) + _otherOptions.put(I2PSOCKSTunnel.PROP_OUTPROXY_TYPE, s.trim()); + } /** * all of these are @since 0.8.3 (moved from IndexBean) @@ -827,6 +837,20 @@ public class TunnelConfig { config.setProperty(psha256, hex); } } + } else if (TunnelController.TYPE_SOCKS.equals(_type) || TunnelController.TYPE_SOCKS_IRC.equals(_type)) { + // As of 0.9.57, was in UI but unimplemented before, + // so use SHA256 + String auth = _otherOptions.get(I2PTunnelHTTPClientBase.PROP_AUTH); + if (auth != null && !auth.equals("false")) { + if (_newProxyUser != null && _newProxyPW != null && + _newProxyUser.length() > 0 && _newProxyPW.length() > 0) { + String psha256 = OPT + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_PREFIX + + _newProxyUser + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_SHA256_SUFFIX; + String hex = PasswordManager.sha256Hex(I2PSOCKSTunnel.AUTH_REALM, _newProxyUser, _newProxyPW); + if (hex != null) + config.setProperty(psha256, hex); + } + } } if (TunnelController.TYPE_IRC_CLIENT.equals(_type) || @@ -1152,6 +1176,7 @@ public class TunnelConfig { private static final String _otherClientOpts[] = { "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.closeIdleTime", "outproxyUsername", "outproxyPassword", + I2PSOCKSTunnel.PROP_OUTPROXY_TYPE, I2PTunnelHTTPClient.PROP_JUMP_SERVERS, I2PTunnelHTTPClientBase.PROP_AUTH, I2PClient.PROP_SIGTYPE, diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java index db934cc37..3e674a5da 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java @@ -419,6 +419,14 @@ public class EditBean extends IndexBean { return _helper.getUseOutproxyPlugin(tunnel); } + /** + * @return "connect" or "socks", default depends on tunnel type + * @since 0.9.57 + */ + public String getOutproxyType(int tunnel) { + return _helper.getOutproxyType(tunnel); + } + /** all of these are @since 0.8.3 */ public int getLimitMinute(int tunnel) { return _helper.getLimitMinute(tunnel); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index 92d6a23ec..6a6859d54 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -1083,7 +1083,10 @@ public class IndexBean { /** all proxy auth @since 0.8.2 */ public void setProxyAuth(String s) { - _config.setProxyAuth(I2PTunnelHTTPClientBase.DIGEST_AUTH); + String type = getType(); + boolean isSOCKS = TunnelController.TYPE_SOCKS.equals(type) || + TunnelController.TYPE_SOCKS_IRC.equals(type); + _config.setProxyAuth(isSOCKS ? "true" : I2PTunnelHTTPClientBase.DIGEST_AUTH); } public void setProxyUsername(String s) { @@ -1116,6 +1119,15 @@ public class IndexBean { _config.setUseOutproxyPlugin(true); } + /** + * @param s "connect" or "socks" + * @since 0.9.57 + */ + public void setOutproxyType(String s) { + _config.setOutproxyType(s); + } + + public void setLimitMinute(String s) { if (s != null) { try { diff --git a/apps/i2ptunnel/jsp/editClient.jsi b/apps/i2ptunnel/jsp/editClient.jsi index 5610945c1..6548bcc37 100644 --- a/apps/i2ptunnel/jsp/editClient.jsi +++ b/apps/i2ptunnel/jsp/editClient.jsi @@ -162,8 +162,28 @@ <%=intl._t("Outproxies")%> - +<% + if ("sockstunnel".equals(tunnelType) || "socksirctunnel".equals(tunnelType)) { + %><% + } else { + %><% + } +%>  <%=intl._t("For a random selection from a pool, separate with commas e.g. server1.i2p,server2.i2p")%>" value="<%=editBean.getClientDestination(curTunnel)%>" class="freetext proxyList" /> +<% + if ("sockstunnel".equals(tunnelType) || "socksirctunnel".equals(tunnelType)) { + boolean isHTTPS = editBean.getOutproxyType(curTunnel).equals("connect"); + %> + <%=intl._t("Outproxy Type")%>: + class="tickbox" /> + SOCKS + + class="tickbox" /> + HTTPS<% + } +%> <% diff --git a/core/java/src/net/i2p/socks/SOCKS5Constants.java b/core/java/src/net/i2p/socks/SOCKS5Constants.java index 76bfc3d21..bb428515c 100644 --- a/core/java/src/net/i2p/socks/SOCKS5Constants.java +++ b/core/java/src/net/i2p/socks/SOCKS5Constants.java @@ -34,6 +34,13 @@ public class SOCKS5Constants { public static final int CONNECT = 0x01; public static final int BIND = 0x02; public static final int UDP_ASSOCIATE = 0x03; + // https://github.com/torproject/torspec/blob/main/socks-extensions.txt + /** @since 0.9.57 */ + public static final int TOR_RESOLVE = 0xf0; + /** @since 0.9.57 */ + public static final int TOR_RESOLVE_PTR = 0xf1; + /** @since 0.9.57 */ + public static final int TOR_CONNECT_DIR = 0xf2; } public static class Reply {