package net.i2p.client.streaming.impl; import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileOutputStream; import java.io.Flushable; import java.io.IOException; import java.io.OutputStream; import net.i2p.I2PAppContext; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; /** * Write a standard pcap file with a "TCP" packet that can be analyzed with * standard tools such as wireshark. * * The source and dest "IP" and "port" are fake but are generated from the * hashes of the Destinations and stream ID's, so they will be consistent. * The local "IP" will always be of the form 127.0.x.y * Initial IP for a conn will be 127.0.0.0 for the local and 0.0.0.0 for the remote. * * Reference: http://wiki.wireshark.org/Development/LibpcapFileFormat * * The Jpcap library http://netresearch.ics.uci.edu/kfujii/jpcap/doc/ * was close to what I want, but it requires you to instantiate a "captor" * before you can write a file, and it requires a native lib to do so, * and even then, it only wants to read the file, not write it. * * We even calculate a correct TCP header checksum to keep the tools happy. * We don't, however, convert I2P-style sequence numbers, which count packets, * to TCP-style byte counts. We don't track a lowest-acked-thru byte count atm, really. * * We do represent the window size in bytes though, so that's real confusing. * * This is designed to debug the streaming lib, but there are not log calls for every * single packet - pings and pongs, and various odd cases where received packets * are dropped, are not logged. * * Yes we could dump it natively and write a wireshark dissector. That sounds hard. * And we wouldn't get the TCP stream analysis built into the tools. * * @since 0.9.4 */ public class PcapWriter implements Closeable, Flushable { /** big-endian, see file format ref - 24 bytes */ private static final byte[] FILE_HEADER = { (byte) 0xa1, (byte) 0xb2, (byte) 0xc3, (byte) 0xd4, 0, 2, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xff, (byte) 0xff, 0, 0, 0, 1 }; /** dummy macs, IPv4 ethertype */ private static final byte[] MAC_HEADER = { 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, (byte) 0x08, 0 }; private static final byte[] IP_HEADER_1 = { 0x45, 0 }; // the length goes after this private static final byte[] IP_HEADER_2 = { 0x12, 0x34, 0x40, 0, 64, 6 }; // ID, flags, TTL and TCP private static final byte[] UNK_IP = { (byte) 0xff, 0, 0, 0}; private static final byte[] MY_UNK_IP = {127, 0, 0, 0}; /** max # of streaming lib payload bytes to dump */ private static final int MAX_PAYLOAD_BYTES = 10; /** options - give our custom ones some mnemonics */ private static final int MAX_OPTION_LEN = 40; private static final byte OPTION_END = 0; private static final byte OPTION_MSS = 2; private static final byte OPTION_PING = 6; private static final byte OPTION_PONG = 7; private static final byte OPTION_SIGREQ = 0x55; private static final byte OPTION_SIG = 0x56; private static final byte OPTION_RDELAY = (byte) 0xde; private static final byte OPTION_ODELAY = (byte) 0xd0; private static final byte OPTION_FROM = (byte) 0xf0; private static final byte OPTION_NACK = (byte) 0xac; private final OutputStream _fos; private final I2PAppContext _context; public PcapWriter(I2PAppContext ctx, String file) throws IOException { _context = ctx; File f = new File(ctx.getLogDir(), file); //if (f.exists()) { // _fos = new FileOutputStream(f, true); //} else { _fos = new BufferedOutputStream(new FileOutputStream(f), 64*1024); _fos.write(FILE_HEADER); //} } public void close() { try { _fos.close(); } catch (IOException ioe) {} } public void flush() { try { _fos.flush(); } catch (IOException ioe) {} } /** * For outbound packets */ public void write(PacketLocal pkt) throws IOException { try { wrt(pkt, pkt.getConnection(), false); } catch (DataFormatException dfe) { dfe.printStackTrace(); throw new IOException(dfe.toString()); } } /** * For inbound packets * @param con may be null */ public void write(Packet pkt, Connection con) throws IOException { try { wrt(pkt, con, true); } catch (DataFormatException dfe) { dfe.printStackTrace(); throw new IOException(dfe.toString()); } } /** * @param con may be null */ private synchronized void wrt(Packet pkt, Connection con, boolean isInbound) throws IOException, DataFormatException { int includeLen = Math.min(MAX_PAYLOAD_BYTES, pkt.getPayloadSize()); // option block Options opts = new Options(); if (pkt.isFlagSet(Packet.FLAG_MAX_PACKET_SIZE_INCLUDED)) opts.add(OPTION_MSS, 2, pkt.getOptionalMaxSize()); if (pkt.isFlagSet(Packet.FLAG_DELAY_REQUESTED)) opts.add(OPTION_ODELAY, 2, pkt.getOptionalDelay()); if (pkt.getResendDelay() > 0) opts.add(OPTION_RDELAY, 1, pkt.getResendDelay()); if (pkt.isFlagSet(Packet.FLAG_SIGNATURE_REQUESTED)) opts.add(OPTION_SIGREQ); if (pkt.isFlagSet(Packet.FLAG_SIGNATURE_INCLUDED)) opts.add(OPTION_SIG); if (pkt.isFlagSet(Packet.FLAG_FROM_INCLUDED)) opts.add(OPTION_FROM); if (pkt.isFlagSet(Packet.FLAG_ECHO)) { if (pkt.getSendStreamId() > 0) opts.add(OPTION_PING); else opts.add(OPTION_PONG); } if (pkt.getNacks() != null) opts.add(OPTION_NACK, 1, pkt.getNacks().length); int optLen = opts.size(); byte options[] = opts.getData(); // PCAP Header long now; if (isInbound) now = _context.clock().now(); else now = ((PacketLocal)pkt).getLastSend(); DataHelper.writeLong(_fos, 4, now / 1000); DataHelper.writeLong(_fos, 4, 1000 * (now % 1000)); DataHelper.writeLong(_fos, 4, 54 + optLen + includeLen); // 14 MAC + 20 IP + 20 TCP DataHelper.writeLong(_fos, 4, 58 + optLen + pkt.getPayloadSize()); // 54 + MAC checksum // MAC Header 14 bytes _fos.write(MAC_HEADER); // IP 20 bytes total // IP Header 12 bytes int length = 20 + 20 + optLen + pkt.getPayloadSize(); _fos.write(IP_HEADER_1); DataHelper.writeLong(_fos, 2, length); // total IP length _fos.write(IP_HEADER_2); // src and dst IP 8 bytes // make our side always start with 127.0.x.x byte[] srcAddr, dstAddr; if (isInbound) { if (con != null) { dstAddr = new byte[4]; dstAddr[0] = 127; dstAddr[1] = 0; System.arraycopy(con.getSession().getMyDestination().calculateHash().getData(), 0, dstAddr, 2, 2); } else dstAddr = MY_UNK_IP; if (con != null && con.getRemotePeer() != null) srcAddr = con.getRemotePeer().calculateHash().getData(); else if (pkt.getOptionalFrom() != null) srcAddr = pkt.getOptionalFrom().calculateHash().getData(); else srcAddr = UNK_IP; } else { if (con != null) { srcAddr = new byte[4]; srcAddr[0] = 127; srcAddr[1] = 0; System.arraycopy(con.getSession().getMyDestination().calculateHash().getData(), 0, srcAddr, 2, 2); } else srcAddr = MY_UNK_IP; if (con != null && con.getRemotePeer() != null) dstAddr = con.getRemotePeer().calculateHash().getData(); else dstAddr = UNK_IP; } // calculate and output the correct IP header checksum to keep the analyzers happy int checksum = length; checksum = update(checksum, IP_HEADER_1); checksum = update(checksum, IP_HEADER_2); checksum = update(checksum, srcAddr, 4); checksum = update(checksum, dstAddr, 4); DataHelper.writeLong(_fos, 2, checksum ^ 0xffff); // IPs _fos.write(srcAddr, 0, 4); _fos.write(dstAddr, 0, 4); // TCP header 20 bytes total // src and dst port 4 bytes // the rcv ID is the source, and the send ID is the dest. DataHelper.writeLong(_fos, 2, pkt.getReceiveStreamId() & 0xffff); DataHelper.writeLong(_fos, 2, pkt.getSendStreamId() & 0xffff); // seq and acks 8 bytes long seq; // wireshark wants the seq # in a SYN packet to be one less than the first data packet, // so let's set it to 0. ??????????? if (pkt.isFlagSet(Packet.FLAG_SYNCHRONIZE)) seq = 0xffffffffL; else seq = pkt.getSequenceNum(); DataHelper.writeLong(_fos, 4, seq); long acked = 0; if (con != null) { acked = getLowestAckedThrough(pkt, con); } DataHelper.writeLong(_fos, 4, acked); // offset and flags 2 bytes int flags = 0; if (pkt.isFlagSet(Packet.FLAG_CLOSE)) flags |= 0x01; if (pkt.isFlagSet(Packet.FLAG_SYNCHRONIZE)) flags |= 0x02; if (pkt.isFlagSet(Packet.FLAG_RESET)) flags |= 0x04; if (!pkt.isFlagSet(Packet.FLAG_NO_ACK)) flags |= 0x10; // offset byte int osb = (5 + (optLen / 4)) << 4; DataHelper.writeLong(_fos, 1, osb); // 5 + optLen/4 32-byte words DataHelper.writeLong(_fos, 1, flags); // window size 2 bytes long window = ConnectionOptions.INITIAL_WINDOW_SIZE; long msgSize = ConnectionOptions.DEFAULT_MAX_MESSAGE_SIZE; if (con != null) { // calculate the receive window, which doesn't have an exact streaming equivalent if (isInbound) { // Inbound pkt: his rcv buffer ~= our outbound window // try to represent what he thinks the window is, we don't really know // this isn't really right, the lastsendid can get way ahead window = con.getLastSendId() + con.getOptions().getWindowSize() - acked; } else { // Ourbound pkt: our rcv buffer ~= his outbound window // TODO just use a recent high unackedIn count? // following is from ConnectionPacketHandler // this is not interesting, we have lots of buffers long ready = con.getInputStream().getHighestReadyBlockId(); int available = con.getOptions().getInboundBufferSize() - con.getInputStream().getTotalReadySize(); int allowedBlocks = available/con.getOptions().getMaxMessageSize(); window = (ready + allowedBlocks) - pkt.getSequenceNum(); } if (window <= 1) window = 2; // TCP min msgSize = con.getOptions().getMaxMessageSize(); } // messages -> bytes window *= msgSize; // for now we don't spoof window scaling if (window > 65535) window = 65535; DataHelper.writeLong(_fos, 2, window); // checksum and urgent pointer 4 bytes DataHelper.writeLong(_fos, 4, 0); // TCP option block if (optLen > 0) _fos.write(options, 0, optLen); // some data if (includeLen > 0) _fos.write(pkt.getPayload().getData(), 0, includeLen); if (pkt.isFlagSet(Packet.FLAG_CLOSE)) _fos.flush(); } /** * copied from Connection.ackPackets() * * This is really nasty, but if the packet has an ACK, then we * find the lowest NACK, and we are acked thru the lowest - 1. * * If there is no ACK, then we could use the conn's highest acked through, * for an inbound packet (containing acks for outbound packets) * But it appears that all packets have ACKs, as FLAG_NO_ACK is never set. * * To do: Add the SACK option to the TCP header. */ private static long getLowestAckedThrough(Packet pkt, Connection con) { long nacks[] = pkt.getNacks(); long lowest = pkt.getAckThrough(); // can return -1 but we increment below if (nacks != null) { for (int i = 0; i < nacks.length; i++) { if (nacks[i] - 1 < lowest) lowest = nacks[i] - 1; } } // I2P ack is of current seq number; TCP is next expected seq number // should be >= 0 now lowest++; // just in case return Math.max(0, lowest); } private static class Options { private final byte[] _b; private int _len; public Options() { _b = new byte[MAX_OPTION_LEN]; } /** 40 bytes long, caller must use size() to get actual size */ public byte[] getData() { return _b; } /** rounded to next 4 bytes */ public int size() { return ((_len + 3) / 4) * 4; } public void add(byte type) { add(type, 0, 0); } public void add(byte type, int datalen, int data) { // no room? drop silently if (_len + datalen + 2 > MAX_OPTION_LEN) return; _b[_len++] = type; _b[_len++] = (byte) (datalen + 2); if (datalen > 0) { for (int i = datalen - 1; i >= 0; i--) _b[_len++] = (byte) ((data >> (i * 8)) & 0xff); } // end-of-options mark if (_len < MAX_OPTION_LEN) _b[_len] = OPTION_END; } } /** one's complement 2-byte checksum update */ private static int update(int checksum, byte[] b) { return update(checksum, b, b.length); } private static int update(int checksum, byte[] b, int len) { int rv = checksum; for (int i = 0; i < len; i += 2) { rv += ((b[i] << 8) & 0xff00) | (b[i+1] & 0xff); if (rv > 0xffff) { rv &= 0xffff; rv++; } } return rv; } }