diff --git a/router/java/src/net/i2p/router/transport/udp/SSU2Payload.java b/router/java/src/net/i2p/router/transport/udp/SSU2Payload.java new file mode 100644 index 0000000000000000000000000000000000000000..0439a5b3ad3be1155e398acb59231d1b386063c6 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/udp/SSU2Payload.java @@ -0,0 +1,669 @@ +package net.i2p.router.transport.udp; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.I2NPMessageException; +import net.i2p.data.i2np.I2NPMessageImpl; +import net.i2p.data.router.RouterInfo; + +/** + * + * SSU2 Payload generation and parsing + * + * @since 0.9.54 + */ +class SSU2Payload { + + public static final int BLOCK_HEADER_SIZE = 3; + + private static final int BLOCK_DATETIME = 0; + private static final int BLOCK_OPTIONS = 1; + private static final int BLOCK_ROUTERINFO = 2; + private static final int BLOCK_I2NP = 3; + private static final int BLOCK_FIRSTFRAG = 4; + private static final int BLOCK_FOLLOWONFRAG = 5; + private static final int BLOCK_TERMINATION = 6; + private static final int BLOCK_RELAYREQ = 7; + private static final int BLOCK_RELAYRESP = 8; + private static final int BLOCK_RELAYINTRO = 9; + private static final int BLOCK_PEERTEST = 10; + private static final int BLOCK_NEXTNONCE = 11; + private static final int BLOCK_ACK = 12; + private static final int BLOCK_ADDRESS = 13; + private static final int BLOCK_INTROKEY= 14; + private static final int BLOCK_RELAYTAGREQ = 15; + private static final int BLOCK_RELAYTAG = 16; + private static final int BLOCK_NEWTOKEN = 17; + private static final int BLOCK_PATHCHALLENGE = 18; + private static final int BLOCK_PATHRESP = 19; + private static final int BLOCK_PADDING = 254; + + /** + * For all callbacks, recommend throwing exceptions only from the handshake. + * Exceptions will get thrown out of processPayload() and prevent + * processing of succeeding blocks. + */ + public interface PayloadCallback { + public void gotDateTime(long time) throws DataFormatException; + + public void gotI2NP(I2NPMessage msg) throws I2NPMessageException; + + /** + * @param expires 0 for frag greater than 1 + * @param type 0 for frag greater than 1 + */ + public void gotFragment(byte[] data, long messageID, int type, long expires, int frag, boolean isLast) throws DataFormatException; + + /** + * @param ranges null if none + */ + public void gotACK(long ackThru, int acks, byte[] ranges); + + /** + * @param isHandshake true only for message 3 part 2 + */ + public void gotOptions(byte[] options, boolean isHandshake) throws DataFormatException; + + /** + * @param ri will already be validated + * @param isHandshake true only for message 3 part 2 + */ + public void gotRI(RouterInfo ri, boolean isHandshake, boolean flood) throws DataFormatException; + + /** + * @param data is first gzipped and then fragmented + * @param isHandshake true only for message 3 part 2 + */ + public void gotRIFragment(byte[] data, boolean isHandshake, boolean flood, boolean isGzipped, int frag, int totalFrags); + + public void gotAddress(byte[] ip, int port); + + public void gotIntroKey(byte[] key); + + public void gotRelayTagRequest(); + + public void gotRelayTag(long tag); + + public void gotToken(long token, long expires); + + /** + * @param lastReceived in theory could wrap around to negative, but very unlikely + */ + public void gotTermination(int reason, long lastReceived); + + /** + * For stats. + * @param paddingLength the number of padding bytes, not including the 3-byte block header + * @param frameLength the total size of the frame, including all blocks and block headers + */ + public void gotPadding(int paddingLength, int frameLength); + + public void gotUnknown(int type, int len); + } + + /** + * Incoming payload. Calls the callback for each received block. + * + * @return number of blocks processed + * @throws IOException on major errors + * @throws DataFormatException on parsing of individual blocks + * @throws I2NPMessageException on parsing of I2NP block + */ + public static int processPayload(I2PAppContext ctx, PayloadCallback cb, + byte[] payload, int off, int length, boolean isHandshake) + throws IOException, DataFormatException, I2NPMessageException { + int blocks = 0; + boolean gotPadding = false; + boolean gotTermination = false; + int i = off; + final int end = off + length; + while (i < end) { + int type = payload[i++] & 0xff; + if (gotPadding) + throw new IOException("Illegal block after padding: " + type); + if (gotTermination && type != BLOCK_PADDING) + throw new IOException("Illegal block after termination: " + type); + if (isHandshake && blocks == 0 && type != BLOCK_DATETIME) + throw new IOException("Illegal first block in handshake: " + type); + int len = (int) DataHelper.fromLong(payload, i, 2); + i += 2; + if (i + len > end) { + throw new IOException("Block " + blocks + " type " + type + " length " + len + + " at offset " + (i - 3 - off) + " runs over frame of size " + length + + '\n' + net.i2p.util.HexDump.dump(payload, off, length)); + } + switch (type) { + // don't modify i inside switch + + case BLOCK_DATETIME: + if (len != 4) + throw new IOException("Bad length for DATETIME: " + len); + long time = DataHelper.fromLong(payload, i, 4) * 1000; + cb.gotDateTime(time); + break; + + case BLOCK_OPTIONS: + byte[] options = new byte[len]; + System.arraycopy(payload, i, options, 0, len); + cb.gotOptions(options, isHandshake); + break; + + case BLOCK_ROUTERINFO: { + int flag = payload[i] & 0xff; + boolean flood = (flag & 0x01) != 0; + boolean gz = (flag & 0x02) != 0; + int frag = payload[i + 1] & 0xff; + int fnum = frag >> 4; + int ftot = frag & 0x0f; + if (ftot == 0) + throw new IOException("Bad fragment count for ROUTERINFO: " + ftot); + if (fnum == 0 && ftot == 1) { + RouterInfo alice = new RouterInfo(); + ByteArrayInputStream bais; + if (gz) { + byte decompressed[] = DataHelper.decompress(payload, i + 2, len - 2); + bais = new ByteArrayInputStream(decompressed); + } else { + bais = new ByteArrayInputStream(payload, i + 2, len - 2); + } + alice.readBytes(bais, true); + cb.gotRI(alice, isHandshake, flood); + } else { + byte[] data = new byte[len - 2]; + System.arraycopy(payload, i + 2, data, 0, len - 2); + cb.gotRIFragment(data, isHandshake, flood, gz, fnum, ftot); + } + break; + } + + case BLOCK_I2NP: + if (isHandshake) + throw new IOException("Illegal block in handshake: " + type); + I2NPMessage msg = I2NPMessageImpl.fromRawByteArrayNTCP2(ctx, payload, i, len, null); + cb.gotI2NP(msg); + break; + + case BLOCK_FIRSTFRAG: { + if (isHandshake) + throw new IOException("Illegal block in handshake: " + type); + if (len < 9) + throw new IOException("Bad length for FIRSTFRAG: " + len); + int mtype = payload[i] & 0xff; + long id = DataHelper.fromLong(payload, i + 1, 4); + long exp = DataHelper.fromLong(payload, i + 5, 4) * 1000; + byte[] data = new byte[len - 9]; + System.arraycopy(payload, i + 9, data, 0, len - 9); + cb.gotFragment(data, id, mtype, exp, 0, false); + break; + } + + case BLOCK_FOLLOWONFRAG: { + if (isHandshake) + throw new IOException("Illegal block in handshake: " + type); + if (len < 5) + throw new IOException("Bad length for FOLLOWON: " + len); + int frag = (payload[i] & 0xff) >> 1; + boolean isLast = (payload[i] & 0x01) != 0; + long id = DataHelper.fromLong(payload, i + 1, 4); + byte[] data = new byte[len - 5]; + System.arraycopy(payload, i + 5, data, 0, len - 5); + cb.gotFragment(data, id, 0, 0, frag, isLast); + break; + } + + case BLOCK_ACK: { + if (isHandshake) + throw new IOException("Illegal block in handshake: " + type); + if (len < 5 || (len % 2) != 1) + throw new IOException("Bad length for ACK: " + len); + long ack = DataHelper.fromLong(payload, i, 4); + int acnt = payload[i + 4] & 0xff; + int rcnt = len - 5; + byte[] ranges; + if (rcnt > 0) { + ranges = new byte[rcnt]; + System.arraycopy(payload, i + 5, ranges, 0, rcnt); + } else { + ranges = null; + } + cb.gotACK(ack, acnt, ranges); + break; + } + + case BLOCK_ADDRESS: + if (len != 6 && len != 18) + throw new IOException("Bad length for Address: " + len); + int port = (int) DataHelper.fromLong(payload, i, 2); + byte[] ip = new byte[len - 2]; + System.arraycopy(payload, i + 2, ip, 0, len - 2); + cb.gotAddress(ip, port); + break; + + case BLOCK_RELAYTAGREQ: + cb.gotRelayTagRequest(); + break; + + case BLOCK_RELAYTAG: + if (len < 4) + throw new IOException("Bad length for RELAYTAG: " + len); + long tag = DataHelper.fromLong(payload, i, 4); + cb.gotRelayTag(tag); + break; + + case BLOCK_NEWTOKEN: + if (len < 12) + throw new IOException("Bad length for NEWTOKEN: " + len); + long exp = DataHelper.fromLong(payload, i, 4) * 1000; + long token = DataHelper.fromLong8(payload, i + 4); + cb.gotToken(token, exp); + break; + + case BLOCK_TERMINATION: + if (isHandshake) + throw new IOException("Illegal block in handshake: " + type); + if (len < 9) + throw new IOException("Bad length for TERMINATION: " + len); + long last = DataHelper.fromLong8(payload, i); + int rsn = payload[i + 8] & 0xff; + cb.gotTermination(rsn, last); + gotTermination = true; + break; + + case BLOCK_PADDING: + gotPadding = true; + cb.gotPadding(len, length); + break; + + default: + if (isHandshake) + throw new IOException("Illegal block in handshake: " + type); + cb.gotUnknown(type, len); + break; + + } + i += len; + blocks++; + } + if (isHandshake && blocks == 0) + throw new IOException("No blocks in handshake"); + return blocks; + } + + /** + * @param payload writes to it starting at off + * @return the new offset + */ + public static int writePayload(byte[] payload, int off, List<Block> blocks) { + for (Block block : blocks) { + off = block.write(payload, off); + } + return off; + } + + /** + * Base class for blocks to be transmitted. + * Not used for receive; we use callbacks instead. + */ + public static abstract class Block { + private final int type; + + public Block(int ttype) { + type = ttype; + } + + /** @return new offset */ + public int write(byte[] tgt, int off) { + tgt[off++] = (byte) type; + // we do it this way so we don't call getDataLength(), + // which may be inefficient + // off is where the length goes + int rv = writeData(tgt, off + 2); + DataHelper.toLong(tgt, off, 2, rv - (off + 2)); + return rv; + } + + /** + * @return the size of the block, including the 3 byte header (type and size) + */ + public int getTotalLength() { + return BLOCK_HEADER_SIZE + getDataLength(); + } + + /** + * @return the size of the block, NOT including the 3 byte header (type and size) + */ + public abstract int getDataLength(); + + /** @return new offset */ + public abstract int writeData(byte[] tgt, int off); + + @Override + public String toString() { + return "Payload block type " + type + " length " + getDataLength(); + } + } + + public static class RIBlock extends Block { + private final byte[] data; + private final int doff, dlen; + private final boolean f, gz; + private final int fr, frt; + + /** + * Whole thing + */ + public RIBlock(byte[] ridata, boolean flood, boolean gzipped) { + this(ridata, 0, ridata.length, flood, gzipped, 0, 1); + } + + /** + * Fragment + */ + public RIBlock(byte[] ridata, int off, int len, boolean flood, boolean gzipped, int frag, int total) { + super(BLOCK_ROUTERINFO); + data = ridata; + doff = off; + dlen = len; + f = flood; + gz = gzipped; + fr = frag; + frt = total; + } + + public int getDataLength() { + return 2 + data.length; + } + + public int writeData(byte[] tgt, int off) { + byte b = (byte) (f ? 1 : 0); + if (gz) + b |= 0x02; + tgt[off++] = b; // flag + b = (byte) ((fr << 4) | frt); + tgt[off++] = b; // frag + System.arraycopy(data, doff, tgt, off, dlen); + return off + dlen; + } + } + +/* + public static class I2NPBlock extends Block { + private final OutboundMessageState2 m; + + public I2NPBlock(OutboundMessageState2 msg) { + super(BLOCK_I2NP); + m = msg; + } + + public int getDataLength() { + // 9 byte header vs. 16 + return m.getMessageSize(); + } + + public int writeData(byte[] tgt, int off) { + // fixme NTCP2 flavor + return off + m.writeFragment(tgt, off, 0); + } + } +*/ + + /** + * Same format as I2NPBlock + */ +/* + public static class FirstFragBlock extends Block { + private final OutboundMessageState2 m; + + public FirstFragBlock(OutboundMessageState2 msg) { + super(BLOCK_FIRSTFRAG); + m = msg; + } + + public int getDataLength() { + // 9 byte header vs. 5 + return m.fragmentSize(0); // + 4; + } + + public int writeData(byte[] tgt, int off) { + // fixme NTCP2 flavor + return off + m.writeFragment(tgt, off, 0); + } + } +*/ + + /** + * + */ +/* + public static class FollowFragBlock extends Block { + private final OutboundMessageState2 m; + private final int f; + + public FollowFragBlock(OutboundMessageState2 msg, int frag) { + super(BLOCK_FOLLOWONFRAG); + if (frag <= 0) + throw new IllegalArgumentException(); + m = msg; + f = frag; + } + + public int getDataLength() { + return m.fragmentSize(f) + 5; + } + + public int writeData(byte[] tgt, int off) { + byte b = (byte) (f << 1); + if (f == m.getFragmentCount() - 1) + b |= (byte) 0x01; + tgt[off++] = b; + DataHelper.toLong(tgt, off, 4, m.getMessageId()); + off += 4; + return off + m.writeFragment(tgt, off, 0); + } + } +*/ + + public static class PaddingBlock extends Block { + private final int sz; + private final I2PAppContext ctx; + + /** with zero-filled data */ + public PaddingBlock(int size) { + this(null, size); + } + + /** with random data */ + public PaddingBlock(I2PAppContext context, int size) { + super(BLOCK_PADDING); + sz = size; + ctx = context; + } + + public int getDataLength() { + return sz; + } + + public int writeData(byte[] tgt, int off) { + if (ctx != null) + ctx.random().nextBytes(tgt, off, sz); + else + Arrays.fill(tgt, off, off + sz, (byte) 0); + return off + sz; + } + } + + public static class DateTimeBlock extends Block { + private final long now; + + public DateTimeBlock(I2PAppContext ctx) { + super(BLOCK_DATETIME); + now = ctx.clock().now(); + } + + public int getDataLength() { + return 4; + } + + public int writeData(byte[] tgt, int off) { + DataHelper.toLong(tgt, off, 4, now / 1000); + return off + 4; + } + } + + public static class OptionsBlock extends Block { + private final byte[] opts; + + public OptionsBlock(byte[] options) { + super(BLOCK_OPTIONS); + opts = options; + } + + public int getDataLength() { + return opts.length; + } + + public int writeData(byte[] tgt, int off) { + System.arraycopy(opts, 0, tgt, off, opts.length); + return off + opts.length; + } + } + + public static class TerminationBlock extends Block { + private final byte rsn; + private final long rcvd; + + public TerminationBlock(int reason, long lastReceived) { + super(BLOCK_TERMINATION); + rsn = (byte) reason; + rcvd = lastReceived; + } + + public int getDataLength() { + return 9; + } + + public int writeData(byte[] tgt, int off) { + DataHelper.toLong8(tgt, off, rcvd); + tgt[off + 8] = rsn; + return off + 9; + } + } + + public static class AckBlock extends Block { + private final long t; + private final int a; + private final byte[] r; + private final int rc; + + /* + * @param ranges nack/ack/nack/ack + * @param rangeCount ranges length / 2 + */ + public AckBlock(long thru, int acnt, byte[] ranges, int rangeCount) { + super(BLOCK_ACK); + if (rangeCount > 255) + throw new IllegalArgumentException(); + if (acnt > 255) + throw new IllegalArgumentException(); + t = thru; + a = acnt; + r = ranges; + rc = rangeCount; + } + + public int getDataLength() { + return 5 + (rc * 2); + } + + public int writeData(byte[] tgt, int off) { + DataHelper.toLong(tgt, off, 4, t); + off += 4; + tgt[off++] = (byte) a; + System.arraycopy(r, 0, tgt, off, rc * 2); + return off + (rc * 2); + } + } + + public static class AddressBlock extends Block { + private final byte[] i; + private final int p; + + public AddressBlock(byte[] ip, int port) { + super(BLOCK_ADDRESS); + i = ip; + p = port; + } + + public int getDataLength() { + return 2 + i.length; + } + + public int writeData(byte[] tgt, int off) { + DataHelper.toLong(tgt, off, 2, p); + off += 2; + System.arraycopy(i, 0, tgt, off, i.length); + return off + i.length; + } + } + + public static class RelayTagRequestBlock extends Block { + + public RelayTagRequestBlock() { + super(BLOCK_RELAYTAGREQ); + } + + public int getDataLength() { + return 0; + } + + public int writeData(byte[] tgt, int off) { + return off; + } + } + + public static class RelayTagBlock extends Block { + private final long t; + + public RelayTagBlock(long tag) { + super(BLOCK_RELAYTAG); + t = tag; + } + + public int getDataLength() { + return 4; + } + + public int writeData(byte[] tgt, int off) { + DataHelper.toLong(tgt, off, 4, t); + return off + 4; + } + } + + public static class NewTokenBlock extends Block { + private final long t, e; + + public NewTokenBlock(long token, long expires) { + super(BLOCK_NEWTOKEN); + t = token; + e = expires / 1000; + } + + public int getDataLength() { + return 12; + } + + public int writeData(byte[] tgt, int off) { + DataHelper.toLong(tgt, off, 4, e); + off += 4; + DataHelper.toLong8(tgt, off, t); + return off + 8; + } + } +} diff --git a/router/java/src/net/i2p/router/transport/udp/SSU2Util.java b/router/java/src/net/i2p/router/transport/udp/SSU2Util.java index 4d8295b70f8b5e058de267d52e4f4b9deb113bc6..aa4370815196845d1e90c60ce71cde1b00106365 100644 --- a/router/java/src/net/i2p/router/transport/udp/SSU2Util.java +++ b/router/java/src/net/i2p/router/transport/udp/SSU2Util.java @@ -56,16 +56,16 @@ final class SSU2Util { /** 3 byte block header + 9 byte I2NP header = 12 */ - // public static final int FULL_I2NP_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 9; + public static final int FULL_I2NP_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 9; /** 3 byte block header + 9 byte I2NP header = 12 */ - // public static final int FIRST_FRAGMENT_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 9; + public static final int FIRST_FRAGMENT_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 9; /** 3 byte block header + 4 byte msg ID + 1 byte fragment info = 8 */ - // public static final int FOLLOWON_FRAGMENT_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 5; + public static final int FOLLOWON_FRAGMENT_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 5; /** 16 byte block header + 2 + 12 = 30 */ - // public static final int DATA_HEADER_SIZE = SHORT_HEADER_SIZE + 2 + FULL_I2NP_HEADER_SIZE; + public static final int DATA_HEADER_SIZE = SHORT_HEADER_SIZE + 2 + FULL_I2NP_HEADER_SIZE; /** * The message types, 0-10, as bytes