* I2PTunnel:
- Add new IRCServer tunnel type - Catch OOMs in HTTPServer - Name the IRCClient filter threads
This commit is contained in:
@ -234,6 +234,8 @@ public class I2PTunnel implements Logging, EventDispatcher {
|
|||||||
runServer(args, l);
|
runServer(args, l);
|
||||||
} else if ("httpserver".equals(cmdname)) {
|
} else if ("httpserver".equals(cmdname)) {
|
||||||
runHttpServer(args, l);
|
runHttpServer(args, l);
|
||||||
|
} else if ("ircserver".equals(cmdname)) {
|
||||||
|
runIrcServer(args, l);
|
||||||
} else if ("textserver".equals(cmdname)) {
|
} else if ("textserver".equals(cmdname)) {
|
||||||
runTextServer(args, l);
|
runTextServer(args, l);
|
||||||
} else if ("client".equals(cmdname)) {
|
} else if ("client".equals(cmdname)) {
|
||||||
@ -383,6 +385,53 @@ public class I2PTunnel implements Logging, EventDispatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same args as runServer
|
||||||
|
* (we should stop duplicating all this code...)
|
||||||
|
*/
|
||||||
|
public void runIrcServer(String args[], Logging l) {
|
||||||
|
if (args.length == 3) {
|
||||||
|
InetAddress serverHost = null;
|
||||||
|
int portNum = -1;
|
||||||
|
File privKeyFile = null;
|
||||||
|
try {
|
||||||
|
serverHost = InetAddress.getByName(args[0]);
|
||||||
|
} catch (UnknownHostException uhe) {
|
||||||
|
l.log("unknown host");
|
||||||
|
_log.error(getPrefix() + "Error resolving " + args[0], uhe);
|
||||||
|
notifyEvent("serverTaskId", Integer.valueOf(-1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
portNum = Integer.parseInt(args[1]);
|
||||||
|
} catch (NumberFormatException nfe) {
|
||||||
|
l.log("invalid port");
|
||||||
|
_log.error(getPrefix() + "Port specified is not valid: " + args[1], nfe);
|
||||||
|
notifyEvent("serverTaskId", Integer.valueOf(-1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
privKeyFile = new File(args[2]);
|
||||||
|
if (!privKeyFile.canRead()) {
|
||||||
|
l.log("private key file does not exist");
|
||||||
|
_log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[2]);
|
||||||
|
notifyEvent("serverTaskId", Integer.valueOf(-1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
I2PTunnelServer serv = new I2PTunnelIRCServer(serverHost, portNum, privKeyFile, args[2], l, (EventDispatcher) this, this);
|
||||||
|
serv.setReadTimeout(readTimeout);
|
||||||
|
serv.startRunning();
|
||||||
|
addtask(serv);
|
||||||
|
notifyEvent("serverTaskId", Integer.valueOf(serv.getId()));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
l.log("server <host> <port> <privkeyfile>");
|
||||||
|
l.log(" creates a server that sends all incoming data\n" + " of its destination to host:port.");
|
||||||
|
notifyEvent("serverTaskId", Integer.valueOf(-1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the HTTP server pointing at the host and port specified using the private i2p
|
* Run the HTTP server pointing at the host and port specified using the private i2p
|
||||||
* destination loaded from the specified file, replacing the HTTP headers
|
* destination loaded from the specified file, replacing the HTTP headers
|
||||||
|
@ -124,8 +124,17 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
|
|||||||
_log.error("Error while closing the received i2p con", ex);
|
_log.error("Error while closing the received i2p con", ex);
|
||||||
}
|
}
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (IOException ioe) {}
|
||||||
if (_log.shouldLog(Log.WARN))
|
if (_log.shouldLog(Log.WARN))
|
||||||
_log.warn("Error while receiving the new HTTP request", ex);
|
_log.warn("Error while receiving the new HTTP request", ex);
|
||||||
|
} catch (OutOfMemoryError oom) {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (IOException ioe) {}
|
||||||
|
if (_log.shouldLog(Log.ERROR))
|
||||||
|
_log.error("OOM in HTTP server", oom);
|
||||||
}
|
}
|
||||||
|
|
||||||
long afterHandle = getTunnel().getContext().clock().now();
|
long afterHandle = getTunnel().getContext().clock().now();
|
||||||
|
@ -83,9 +83,9 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
|
|||||||
i2ps = createI2PSocket(dest);
|
i2ps = createI2PSocket(dest);
|
||||||
i2ps.setReadTimeout(readTimeout);
|
i2ps.setReadTimeout(readTimeout);
|
||||||
StringBuffer expectedPong = new StringBuffer();
|
StringBuffer expectedPong = new StringBuffer();
|
||||||
Thread in = new I2PThread(new IrcInboundFilter(s,i2ps, expectedPong));
|
Thread in = new I2PThread(new IrcInboundFilter(s,i2ps, expectedPong), "IRC Client " + __clientId + " in");
|
||||||
in.start();
|
in.start();
|
||||||
Thread out = new I2PThread(new IrcOutboundFilter(s,i2ps, expectedPong));
|
Thread out = new I2PThread(new IrcOutboundFilter(s,i2ps, expectedPong), "IRC Client " + __clientId + " out");
|
||||||
out.start();
|
out.start();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
if (_log.shouldLog(Log.ERROR))
|
if (_log.shouldLog(Log.ERROR))
|
||||||
|
@ -0,0 +1,193 @@
|
|||||||
|
package net.i2p.i2ptunnel;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import net.i2p.I2PAppContext;
|
||||||
|
import net.i2p.client.streaming.I2PSocket;
|
||||||
|
import net.i2p.crypto.SHA256Generator;
|
||||||
|
import net.i2p.data.DataFormatException;
|
||||||
|
import net.i2p.data.DataHelper;
|
||||||
|
import net.i2p.data.Destination;
|
||||||
|
import net.i2p.data.Hash;
|
||||||
|
import net.i2p.util.EventDispatcher;
|
||||||
|
import net.i2p.util.I2PThread;
|
||||||
|
import net.i2p.util.Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple extension to the I2PTunnelServer that filters the registration
|
||||||
|
* sequence to pass the destination hash of the client through as the hostname,
|
||||||
|
* so an IRC Server may track users across nick changes.
|
||||||
|
*
|
||||||
|
* Of course, this requires the ircd actually use the hostname sent by
|
||||||
|
* the client rather than the IP. It is common for ircds to ignore the
|
||||||
|
* hostname in the USER message (unless it's coming from another server)
|
||||||
|
* since it is easily spoofed. So you have to fix or, if you are lucky,
|
||||||
|
* configure your ircd first. At least in unrealircd and ngircd this is
|
||||||
|
* not configurable.
|
||||||
|
*
|
||||||
|
* There are three options for mangling the desthash. Put the option in the
|
||||||
|
* "custom options" section of i2ptunnel.
|
||||||
|
* - ircserver.cloakKey unset: Cloak with a random value that is persistent for
|
||||||
|
* the life of this tunnel. This is the default.
|
||||||
|
* - ircserver.cloakKey=none: Don't cloak. Users may be correlated with their
|
||||||
|
* (probably) shared clients destination.
|
||||||
|
* Of course if the ircd does cloaking than this is ok.
|
||||||
|
* - ircserver.cloakKey=somepassphrase: Cloak with the hash of the passphrase. Use this to
|
||||||
|
* have consistent mangling across restarts, or to
|
||||||
|
* have multiple IRC servers cloak consistently to
|
||||||
|
* be able to track users even when they switch servers.
|
||||||
|
* Note: don't quote or put spaces in the passphrase,
|
||||||
|
* the i2ptunnel gui can't handle it.
|
||||||
|
*
|
||||||
|
* There is no outbound filtering.
|
||||||
|
*
|
||||||
|
* @author zzz
|
||||||
|
*/
|
||||||
|
public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable {
|
||||||
|
|
||||||
|
private static final Log _log = new Log(I2PTunnelIRCServer.class);
|
||||||
|
private static final String PROP_CLOAK="ircserver.cloakKey";
|
||||||
|
private boolean _cloak;
|
||||||
|
private byte[] _cloakKey; // 32 bytes of stuff to scramble the dest with
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IllegalArgumentException if the I2PTunnel does not contain
|
||||||
|
* valid config to contact the router
|
||||||
|
*/
|
||||||
|
public I2PTunnelIRCServer(InetAddress host, int port, String privData, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
|
||||||
|
super(host, port, privData, l, notifyThis, tunnel);
|
||||||
|
initCloak(tunnel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public I2PTunnelIRCServer(InetAddress host, int port, File privkey, String privkeyname, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
|
||||||
|
super(host, port, privkey, privkeyname, l, notifyThis, tunnel);
|
||||||
|
initCloak(tunnel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public I2PTunnelIRCServer(InetAddress host, int port, InputStream privData, String privkeyname, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
|
||||||
|
super(host, port, privData, privkeyname, l, notifyThis, tunnel);
|
||||||
|
initCloak(tunnel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** generate a random 32 bytes, or the hash of the passphrase */
|
||||||
|
private void initCloak(I2PTunnel tunnel) {
|
||||||
|
Properties opts = tunnel.getClientOptions();
|
||||||
|
String passphrase = opts.getProperty(PROP_CLOAK);
|
||||||
|
_cloak = passphrase == null || !"none".equals(passphrase);
|
||||||
|
if (_cloak) {
|
||||||
|
if (passphrase == null) {
|
||||||
|
_cloakKey = new byte[Hash.HASH_LENGTH];
|
||||||
|
tunnel.getContext().random().nextBytes(_cloakKey);
|
||||||
|
} else {
|
||||||
|
_cloakKey = SHA256Generator.getInstance().calculateHash(passphrase.trim().getBytes()).getData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void blockingHandle(I2PSocket socket) {
|
||||||
|
try {
|
||||||
|
// give them 15 seconds to send in the request
|
||||||
|
socket.setReadTimeout(15*1000);
|
||||||
|
InputStream in = socket.getInputStream();
|
||||||
|
String modifiedRegistration = filterRegistration(in, cloakDest(socket.getPeerDestination()));
|
||||||
|
socket.setReadTimeout(readTimeout);
|
||||||
|
Socket s = new Socket(remoteHost, remotePort);
|
||||||
|
new I2PTunnelRunner(s, socket, slock, null, modifiedRegistration.getBytes(), null);
|
||||||
|
} catch (SocketException ex) {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
if (_log.shouldLog(Log.ERROR))
|
||||||
|
_log.error("Error while closing the received i2p con", ex);
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (IOException ioe) {}
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.warn("Error while receiving the new IRC Connection", ex);
|
||||||
|
} catch (OutOfMemoryError oom) {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (IOException ioe) {}
|
||||||
|
if (_log.shouldLog(Log.ERROR))
|
||||||
|
_log.error("OOM in IRC server", oom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (Optionally) append 32 bytes of crap to the destination then return
|
||||||
|
* the first few characters of the hash of the whole thing, + ".i2p".
|
||||||
|
* Or do we want the full hash if the ircd is going to use this for
|
||||||
|
* nickserv auto-login? Or even Base32 if it will be used in a
|
||||||
|
* case-insensitive manner?
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
String cloakDest(Destination d) {
|
||||||
|
Hash h;
|
||||||
|
if (_cloak) {
|
||||||
|
byte[] b = new byte[d.size() + _cloakKey.length];
|
||||||
|
System.arraycopy(b, 0, d.toByteArray(), 0, d.size());
|
||||||
|
System.arraycopy(b, d.size(), _cloakKey, 0, _cloakKey.length);
|
||||||
|
h = SHA256Generator.getInstance().calculateHash(b);
|
||||||
|
} else {
|
||||||
|
h = d.calculateHash();
|
||||||
|
}
|
||||||
|
return h.toBase64().substring(0, 8) + ".i2p";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** keep reading until we see USER or SERVER */
|
||||||
|
private String filterRegistration(InputStream in, String newHostname) throws IOException {
|
||||||
|
StringBuffer buf = new StringBuffer(128);
|
||||||
|
int lineCount = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
String s = DataHelper.readLine(in);
|
||||||
|
if (s == null)
|
||||||
|
throw new IOException("EOF reached before the end of the headers [" + buf.toString() + "]");
|
||||||
|
if (++lineCount > 10)
|
||||||
|
throw new IOException("Too many lines before USER or SERVER, giving up");
|
||||||
|
s = s.trim();
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("Got line: " + s);
|
||||||
|
|
||||||
|
String field[]=s.split(" ",5);
|
||||||
|
String command;
|
||||||
|
int idx=0;
|
||||||
|
|
||||||
|
if(field[0].charAt(0)==':')
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
try { command = field[idx++]; }
|
||||||
|
catch (IndexOutOfBoundsException ioobe) // wtf, server sent borked command?
|
||||||
|
{
|
||||||
|
throw new IOException("Dropping defective message: index out of bounds while extracting command.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("USER".equalsIgnoreCase(command)) {
|
||||||
|
if (field.length < idx + 4)
|
||||||
|
throw new IOException("Too few parameters in USER message: " + s);
|
||||||
|
// USER zzz1 hostname localhost :zzz
|
||||||
|
// =>
|
||||||
|
// USER zzz1 abcd1234.i2p localhost :zzz
|
||||||
|
// this whole class is for these two lines...
|
||||||
|
buf.append("USER ").append(field[idx]).append(' ').append(newHostname).append(".i2p ");
|
||||||
|
buf.append(field[idx+2]).append(' ').append(field[idx+3]).append("\r\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
buf.append(s).append("\r\n");
|
||||||
|
if ("SERVER".equalsIgnoreCase(command))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("All done, sending: " + buf.toString());
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -58,7 +58,7 @@ public class TunnelController implements Logging {
|
|||||||
setConfig(config, prefix);
|
setConfig(config, prefix);
|
||||||
_messages = new ArrayList(4);
|
_messages = new ArrayList(4);
|
||||||
_running = false;
|
_running = false;
|
||||||
if (createKey && ("server".equals(getType()) || "httpserver".equals(getType())) )
|
if (createKey && getType().endsWith("server"))
|
||||||
createPrivateKey();
|
createPrivateKey();
|
||||||
_starting = getStartOnLoad();
|
_starting = getStartOnLoad();
|
||||||
}
|
}
|
||||||
@ -148,6 +148,8 @@ public class TunnelController implements Logging {
|
|||||||
startServer();
|
startServer();
|
||||||
} else if ("httpserver".equals(type)) {
|
} else if ("httpserver".equals(type)) {
|
||||||
startHttpServer();
|
startHttpServer();
|
||||||
|
} else if ("ircserver".equals(type)) {
|
||||||
|
startIrcServer();
|
||||||
} else {
|
} else {
|
||||||
if (_log.shouldLog(Log.ERROR))
|
if (_log.shouldLog(Log.ERROR))
|
||||||
_log.error("Cannot start tunnel - unknown type [" + type + "]");
|
_log.error("Cannot start tunnel - unknown type [" + type + "]");
|
||||||
@ -274,6 +276,17 @@ public class TunnelController implements Logging {
|
|||||||
_running = true;
|
_running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void startIrcServer() {
|
||||||
|
setI2CPOptions();
|
||||||
|
setSessionOptions();
|
||||||
|
String targetHost = getTargetHost();
|
||||||
|
String targetPort = getTargetPort();
|
||||||
|
String privKeyFile = getPrivKeyFile();
|
||||||
|
_tunnel.runIrcServer(new String[] { targetHost, targetPort, privKeyFile }, this);
|
||||||
|
acquire();
|
||||||
|
_running = true;
|
||||||
|
}
|
||||||
|
|
||||||
private void setListenOn() {
|
private void setListenOn() {
|
||||||
String listenOn = getListenOnInterface();
|
String listenOn = getListenOnInterface();
|
||||||
if ( (listenOn != null) && (listenOn.length() > 0) ) {
|
if ( (listenOn != null) && (listenOn.length() > 0) ) {
|
||||||
|
@ -386,6 +386,7 @@ public class IndexBean {
|
|||||||
else if ("httpserver".equals(internalType)) return "HTTP server";
|
else if ("httpserver".equals(internalType)) return "HTTP server";
|
||||||
else if ("sockstunnel".equals(internalType)) return "SOCKS 5 proxy";
|
else if ("sockstunnel".equals(internalType)) return "SOCKS 5 proxy";
|
||||||
else if ("connectclient".equals(internalType)) return "CONNECT/SSL/HTTPS proxy";
|
else if ("connectclient".equals(internalType)) return "CONNECT/SSL/HTTPS proxy";
|
||||||
|
else if ("ircserver".equals(internalType)) return "IRC server";
|
||||||
else return internalType;
|
else return internalType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,10 +16,8 @@ String tun = request.getParameter("tunnel");
|
|||||||
int curTunnel = -1;
|
int curTunnel = -1;
|
||||||
if (EditBean.isClient(type)) {
|
if (EditBean.isClient(type)) {
|
||||||
%><jsp:include page="editClient.jsp" /><%
|
%><jsp:include page="editClient.jsp" /><%
|
||||||
} else if ("server".equals(type) || "httpserver".equals(type)) {
|
|
||||||
%><jsp:include page="editServer.jsp" /><%
|
|
||||||
} else {
|
} else {
|
||||||
%>Invalid tunnel type<%
|
%><jsp:include page="editServer.jsp" /><%
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
|
@ -260,6 +260,7 @@
|
|||||||
<select name="type">
|
<select name="type">
|
||||||
<option value="server">Standard</option>
|
<option value="server">Standard</option>
|
||||||
<option value="httpserver">HTTP</option>
|
<option value="httpserver">HTTP</option>
|
||||||
|
<option value="ircserver">IRC</option>
|
||||||
</select>
|
</select>
|
||||||
<input class="control" type="submit" value="Create" />
|
<input class="control" type="submit" value="Create" />
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user