package net.i2p.router; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.Properties; import net.i2p.I2PAppContext; import net.i2p.data.Base64; import net.i2p.data.DataFormatException; import net.i2p.data.Hash; import net.i2p.data.router.RouterIdentity; import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.I2NPMessage; import net.i2p.data.i2np.I2NPMessageException; import net.i2p.data.i2np.I2NPMessageImpl; /** * Demo of a stripped down router - no tunnels, no netDb, no i2cp, no peer profiling, * just the SSU comm layer, crypto, and associated infrastructure, extended to handle * a new type of message ("FooMessage"). * */ public class SSUDemo { RouterContext _us; public static void main(String args[]) { boolean testNTCP = args.length > 0 && args[0].equals("ntcp"); SSUDemo demo = new SSUDemo(); demo.run(testNTCP); } public SSUDemo() {} public void run(boolean testNTCP) { String cfgFile = "router.config"; Properties envProps = getEnv(testNTCP); Router r = new Router(cfgFile, envProps); r.runRouter(); _us = r.getContext(); setupHandlers(); // wait for it to warm up a bit try { Thread.sleep(30*1000); } catch (InterruptedException ie) {} // now write out our ident and info RouterInfo myInfo = _us.router().getRouterInfo(); storeMyInfo(myInfo); // look for any other peers written to the same directory, and send each // a single Foo message (0x0123), unless they've already contacted us first. // this call never returns loadPeers(); } private static Properties getEnv(boolean testNTCP) { Properties envProps = new Properties(); // disable one of the transports and UPnP if (testNTCP) envProps.setProperty("i2np.udp.enable", "false"); else envProps.setProperty("i2np.ntcp.enable", "false"); envProps.setProperty("i2np.upnp.enable", "false"); // we want SNTP synchronization for replay prevention envProps.setProperty("time.disabled", "false"); // allow 127.0.0.1/10.0.0.1/etc (useful for testing). If this is false, // peers who say they're on an invalid IP are banlisted envProps.setProperty("i2np.allowLocal", "true"); // IPv6 envProps.setProperty("i2np.udp.ipv6", "enable"); envProps.setProperty("i2np.ntcp.ipv6", "enable"); // explicit IP+port. at least one router on the net has to have their IP+port // set, since there has to be someone to detect one's IP off. most don't need // to set these though //envProps.setProperty("i2np.udp.host", "127.0.0.1"); envProps.setProperty("i2np.udp.host", "::1"); envProps.setProperty("i2np.ntcp.autoip", "false"); envProps.setProperty("i2np.ntcp.hostname", "::1"); // we don't have a context yet to use its random String port = Integer.toString(44000 + (((int) System.currentTimeMillis()) & (16384 - 1))); envProps.setProperty("i2np.udp.internalPort", port); envProps.setProperty("i2np.udp.port", port); envProps.setProperty("i2np.ntcp.autoport", "false"); envProps.setProperty("i2np.ntcp.port", port); // disable I2CP, the netDb, peer testing/profile persistence, and tunnel // creation/management envProps.setProperty("i2p.dummyClientFacade", "true"); envProps.setProperty("i2p.dummyNetDb", "true"); envProps.setProperty("i2p.dummyPeerManager", "true"); envProps.setProperty("i2p.dummyTunnelManager", "true"); // set to false if you want to use HMAC-SHA256-128 instead of HMAC-MD5-128 as // the SSU MAC //envProps.setProperty("i2p.HMACMD5", "true"); // if you're using the HMAC MD5, by default it will use a 32 byte MAC field, // which is a bug, as it doesn't generate the same values as a 16 byte MAC field. // set this to false if you don't want the bug //envProps.setProperty("i2p.HMACBrokenSize", "false"); // no need to include any stats in the routerInfo we send to people on SSU // session establishment envProps.setProperty("router.publishPeerRankings", "false"); // write the logs to ./logs/log-router-*.txt (logger configured with the file // ./logger.config, or another config file specified as // -Dlogger.configLocation=blah) // avoid conflicts over log envProps.setProperty("loggerFilenameOverride", "logs/log-router-" + port + "-@.txt"); System.setProperty("wrapper.logfile", "wrapper-" + port + ".log"); // avoid conflicts over key backup etc. so we don't all use the same keys envProps.setProperty("router.keyBackupDir", "keyBackup/router-" + port); envProps.setProperty("router.info.location", "router-" + port + ".info"); envProps.setProperty("router.keys.location", "router-" + port + ".keys"); envProps.setProperty("router.configLocation", "router-" + port + ".config"); envProps.setProperty("router.pingFile", "router-" + port + ".ping"); // avoid conflicts over blockfile envProps.setProperty("i2p.naming.impl", "net.i2p.client.naming.HostsTxtNamingService"); return envProps; } private void setupHandlers() { // netDb store is sent on connection establishment, which includes contact info // for the peer. the DBStoreJobBuilder builds a new asynchronous Job to process // each one received (storing it in our in-memory, passive netDb) _us.inNetMessagePool().registerHandlerJobBuilder(DatabaseStoreMessage.MESSAGE_TYPE, new DBStoreJobBuilder()); // handle any Foo messages by displaying them on stdout _us.inNetMessagePool().registerHandlerJobBuilder(FooMessage.MESSAGE_TYPE, new FooJobBuilder()); } /** random place for storing router info files - written as $dir/base64(SHA256(info.getIdentity)) */ private static File getInfoDir() { return new File("/tmp/ssuDemoInfo/"); } private static void storeMyInfo(RouterInfo info) { File infoDir = getInfoDir(); if (!infoDir.exists()) infoDir.mkdirs(); FileOutputStream fos = null; File infoFile = new File(infoDir, info.getIdentity().calculateHash().toBase64()); infoFile.deleteOnExit(); try { fos = new FileOutputStream(infoFile); info.writeBytes(fos); } catch (IOException ioe) { ioe.printStackTrace(); } catch (DataFormatException dfe) { dfe.printStackTrace(); } finally { if (fos != null) try { fos.close(); } catch (IOException ioe) {} } System.out.println("Our info stored at: " + infoFile.getAbsolutePath()); } private void loadPeers() { File infoDir = getInfoDir(); if (!infoDir.exists()) infoDir.mkdirs(); while (true) { File peerFiles[] = infoDir.listFiles(); if ( (peerFiles != null) && (peerFiles.length > 0) ) { for (int i = 0; i < peerFiles.length; i++) { if (peerFiles[i].isFile() && !peerFiles[i].isHidden()) { if (!_us.routerHash().toBase64().equals(peerFiles[i].getName())) { System.out.println("Reading info: " + peerFiles[i].getAbsolutePath()); try { FileInputStream in = new FileInputStream(peerFiles[i]); RouterInfo ri = new RouterInfo(); ri.readBytes(in); peerRead(ri); } catch (IOException ioe) { System.err.println("Error reading " + peerFiles[i].getAbsolutePath()); ioe.printStackTrace(); } catch (DataFormatException dfe) { System.err.println("Corrupt " + peerFiles[i].getAbsolutePath()); dfe.printStackTrace(); } } } } } try { Thread.sleep(30*1000); } catch (InterruptedException ie) {} } } private void peerRead(RouterInfo ri) { RouterInfo old = _us.netDb().store(ri.getIdentity().calculateHash(), ri); if (old == null) newPeerRead(ri); } private void newPeerRead(RouterInfo ri) { FooMessage data = new FooMessage(_us, new byte[] { 0x0, 0x1, 0x2, 0x3 }); // _us.clock() is an ntp synchronized clock. give up on sending this message // if it doesn't get ACKed within the next 10 seconds OutNetMessage out = new OutNetMessage(_us, data, _us.clock().now() + 10*1000, 100, ri); System.out.println("SEND: " + Base64.encode(data.getData()) + " to " + ri.getIdentity().calculateHash()); // job fired if we can't contact them, or if it takes too long to get an ACK out.setOnFailedSendJob(null); // job fired once the transport gets a full ACK of the message out.setOnSendJob(new AfterACK()); // queue up the message, establishing a new SSU session if necessary, using // their direct SSU address if they have one, or their indirect SSU addresses // if they don't. If we cannot contact them, we will 'banlist' their address, // during which time we will not even attempt to send messages to them. We also // drop their netDb info when we banlist them, in case their info is no longer // correct. Since the netDb is disabled for all meaningful purposes, the SSUDemo // will be responsible for fetching such information. _us.outNetMessagePool().add(out); } /** fired if and only if the FooMessage is ACKed before we time out */ private class AfterACK extends JobImpl { public AfterACK() { super(_us); } public void runJob() { System.out.println("Foo message sent completely"); } public String getName() { return "After Foo message send"; } } //// // Foo and netDb store handling below /** * Deal with an Foo message received */ private class FooJobBuilder implements HandlerJobBuilder { public FooJobBuilder() { I2NPMessageImpl.registerBuilder(new FooBuilder(), FooMessage.MESSAGE_TYPE); } public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash) { return new FooHandleJob(_us, receivedMessage, from, fromHash); } } private static class FooHandleJob extends JobImpl { private final I2NPMessage _msg; private final Hash _from; public FooHandleJob(RouterContext ctx, I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash) { super(ctx); _msg = receivedMessage; _from = fromHash; } public void runJob() { // we know its a FooMessage, since thats the type of message that the handler // is registered as FooMessage m = (FooMessage)_msg; System.out.println("RECV FooMessage: " + Base64.encode(m.getData()) + " from " + _from); } public String getName() { return "Handle Foo message"; } } private static class FooBuilder implements I2NPMessageImpl.Builder { public I2NPMessage build(I2PAppContext ctx) { return new FooMessage(ctx, null); } } /** * Just carry some data... */ private static class FooMessage extends I2NPMessageImpl { private byte[] _data; public static final int MESSAGE_TYPE = 17; public FooMessage(I2PAppContext ctx, byte data[]) { super(ctx); _data = data; } /** pull the read data off */ public byte[] getData() { return _data; } /** specify the payload to be sent */ public void setData(byte data[]) { _data = data; } public int getType() { return MESSAGE_TYPE; } protected int calculateWrittenLength() { return _data.length; } public void readMessage(byte[] data, int offset, int dataSize, int type) throws I2NPMessageException { _data = new byte[dataSize]; System.arraycopy(data, offset, _data, 0, dataSize); } protected int writeMessageBody(byte[] out, int curIndex) throws I2NPMessageException { System.arraycopy(_data, 0, out, curIndex, _data.length); return curIndex + _data.length; } } //// // netDb store handling below /** * Handle any netDb stores from the peer - they send us their netDb as part of * their SSU establishment (and we send them ours). */ private class DBStoreJobBuilder implements HandlerJobBuilder { public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash) { return new HandleJob(_us, receivedMessage, from, fromHash); } } private class HandleJob extends JobImpl { private final I2NPMessage _msg; public HandleJob(RouterContext ctx, I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash) { super(ctx); _msg = receivedMessage; } public void runJob() { // we know its a DatabaseStoreMessage, since thats the type of message that the handler // is registered as DatabaseStoreMessage m = (DatabaseStoreMessage)_msg; System.out.println("RECV: " + m); try { _us.netDb().store(m.getKey(), (RouterInfo) m.getEntry()); } catch (IllegalArgumentException iae) { iae.printStackTrace(); } } public String getName() { return "Handle netDb store"; } } }