* I2PTunnel:

- Add new IRCServer tunnel type
      - Catch OOMs in HTTPServer
      - Name the IRCClient filter threads
This commit is contained in:
zzz
2009-02-22 00:35:24 +00:00
parent f3143d8b3d
commit f4c3607c4d
8 changed files with 270 additions and 6 deletions

View File

@ -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

View File

@ -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();

View File

@ -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))

View File

@ -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();
}
}

View File

@ -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) ) {

View File

@ -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;
} }

View File

@ -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" /><%
} }
} }
%> %>

View File

@ -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>