forked from I2P_Developers/i2p.i2p
i2ptunnel: SOCKS 5 tunnel improvements and torsocks support
- Add support for Tor RESOLVE extension by caching and returning fake IP - Handle user/pw when not required to support Tor stream isolation (not really isolating, just handling the authentication) - Fix user/pw authentication - Handle outproxy config changes after start - Support CONNECT outproxies - Add config UI for outproxy type - Enable IPv6 (untested) - Support outproxy config with :port (untested) - Various cleanups Further testing required
This commit is contained in:
@@ -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<String, List<String>> readHeaders(I2PSocket socket, InputStream in, StringBuilder command,
|
||||
public static Map<String, List<String>> readHeaders(I2PSocket socket, InputStream in, StringBuilder command,
|
||||
String[] skipHeaders, I2PAppContext ctx) throws IOException {
|
||||
HashMap<String, List<String>> headers = new HashMap<String, List<String>>();
|
||||
StringBuilder buf = new StringBuilder(128);
|
||||
|
||||
@@ -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<String, List<String>> proxies = null; // port# + "" or "default" -> hostname list
|
||||
private final HashMap<String, List<String>> 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<String, List<String>>(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<String, List<String>>(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<Object, Object> 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<String> getProxies(int port) {
|
||||
List<String> rv = proxies.get(port + "");
|
||||
List<String> rv = proxies.get(Integer.toString(port));
|
||||
if (rv == null)
|
||||
rv = getDefaultProxies();
|
||||
return rv;
|
||||
|
||||
@@ -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<String, String> _torCache = new LHMCache<String, String>(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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -162,8 +162,28 @@
|
||||
<%=intl._t("Outproxies")%>
|
||||
</th>
|
||||
</tr><tr>
|
||||
<td colspan="2">
|
||||
<%
|
||||
if ("sockstunnel".equals(tunnelType) || "socksirctunnel".equals(tunnelType)) {
|
||||
%><td><%
|
||||
} else {
|
||||
%><td colspan="2"><%
|
||||
}
|
||||
%>
|
||||
<input type="text" size="30" name="proxyList" title="<%=intl._t("Specify the .i2p address or destination (b32 or b64) of the outproxy here.")%> <%=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");
|
||||
%></td><td>
|
||||
<b><%=intl._t("Outproxy Type")%>:</b>
|
||||
<span class="multiOption"
|
||||
<label><input value="socks" type="radio" name="outproxyType" <%=(!isHTTPS ? " checked=\"checked\"" : "")%> class="tickbox" />
|
||||
SOCKS</label>
|
||||
</span>
|
||||
<span class="multiOption"
|
||||
<label><input value="connect" type="radio" name="outproxyType" <%=(isHTTPS ? " checked=\"checked\"" : "")%> class="tickbox" />
|
||||
HTTPS</label><%
|
||||
}
|
||||
%>
|
||||
</td>
|
||||
</tr>
|
||||
<%
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user