From f4be99ecd0326493d4d23e0666627ee7a5e8caac Mon Sep 17 00:00:00 2001 From: zzz <zzz@i2pmail.org> Date: Thu, 24 Feb 2022 06:13:28 -0500 Subject: [PATCH] SSU: Add SSU2 class extensions and packet builder Pass XDH key builder to UDPTransport Add SSU2 static keygen when enabled WIP, not hooked in --- .../router/transport/TransportManager.java | 7 +- .../transport/udp/InboundEstablishState2.java | 398 ++++++ .../udp/OutboundEstablishState2.java | 288 ++++ .../router/transport/udp/PacketBuilder2.java | 1173 +++++++++++++++++ .../i2p/router/transport/udp/PeerState2.java | 68 + .../router/transport/udp/UDPTransport.java | 131 +- 6 files changed, 2063 insertions(+), 2 deletions(-) create mode 100644 router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java create mode 100644 router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java create mode 100644 router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java create mode 100644 router/java/src/net/i2p/router/transport/udp/PeerState2.java diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java index 4c63f1ea5f..6e887c4eb5 100644 --- a/router/java/src/net/i2p/router/transport/TransportManager.java +++ b/router/java/src/net/i2p/router/transport/TransportManager.java @@ -83,6 +83,10 @@ public class TransportManager implements TransportEventListener { /** default true */ public final static String PROP_ENABLE_UDP = "i2np.udp.enable"; + /** + * @since 0.9.54 + */ + public final static String PROP_ENABLE_SSU2 = "i2np.ssu2.enable"; /** default true */ public final static String PROP_ENABLE_NTCP = "i2np.ntcp.enable"; /** default true */ @@ -255,7 +259,8 @@ public class TransportManager implements TransportEventListener { private void configTransports() { Transport udp = null; if (_enableUDP) { - udp = new UDPTransport(_context, _dhThread); + X25519KeyFactory xdh = _context.getBooleanProperty(PROP_ENABLE_SSU2) ? _xdhThread : null; + udp = new UDPTransport(_context, _dhThread, _xdhThread); addTransport(udp); initializeAddress(udp); } diff --git a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java new file mode 100644 index 0000000000..06f015fcd1 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java @@ -0,0 +1,398 @@ +package net.i2p.router.transport.udp; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.GeneralSecurityException; +import java.util.List; + +import com.southernstorm.noise.protocol.CipherState; +import com.southernstorm.noise.protocol.CipherStatePair; +import com.southernstorm.noise.protocol.HandshakeState; + +import net.i2p.data.Base64; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.SessionKey; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterInfo; +import net.i2p.router.RouterContext; +import net.i2p.util.Addresses; +import net.i2p.util.Log; + +/** + * Data for a new connection being established, where the remote peer has + * initiated the connection with us. In other words, they are Alice and + * we are Bob. + * + * SSU2 only. + * + * @since 0.9.54 + */ +class InboundEstablishState2 extends InboundEstablishState implements SSU2Payload.PayloadCallback { + private final InetSocketAddress _aliceSocketAddress; + private final long _rcvConnID; + private final long _sendConnID; + private final long _token; + private final long _nextToken; + private final HandshakeState _handshakeState; + private byte[] _sendHeaderEncryptKey1; + private final byte[] _rcvHeaderEncryptKey1; + private byte[] _sendHeaderEncryptKey2; + private byte[] _rcvHeaderEncryptKey2; + + /** + * @param localPort Must be our external port, otherwise the signature of the + * SessionCreated message will be bad if the external port != the internal port. + * @param packet with all header encryption removed + */ + public InboundEstablishState2(RouterContext ctx, UDPTransport transport, + UDPPacket packet) throws GeneralSecurityException { + super(ctx, (InetSocketAddress) packet.getPacket().getSocketAddress()); + DatagramPacket pkt = packet.getPacket(); + _aliceSocketAddress = (InetSocketAddress) pkt.getSocketAddress(); + _handshakeState = new HandshakeState(HandshakeState.PATTERN_ID_XK_SSU2, HandshakeState.RESPONDER, transport.getXDHFactory()); + _handshakeState.getLocalKeyPair().setKeys(transport.getSSU2StaticPrivKey(), 0, + transport.getSSU2StaticPubKey(), 0); + byte[] introKey = transport.getSSU2StaticIntroKey(); + _sendHeaderEncryptKey1 = introKey; + _rcvHeaderEncryptKey1 = introKey; + //_sendHeaderEncryptKey2 set below + //_rcvHeaderEncryptKey2 set below + _introductionRequested = false; // todo + //_bobIP = TODO + //if (_log.shouldLog(Log.DEBUG)) + // _log.debug("Receive sessionRequest, BobIP = " + Addresses.toString(_bobIP)); + int off = pkt.getOffset(); + int len = pkt.getLength(); + byte data[] = pkt.getData(); + // fast MSB check for key < 2^255 + if ((data[off + 32 + 32 - 1] & 0x80) != 0) + throw new GeneralSecurityException("Bad PK msg 1"); + _rcvConnID = DataHelper.fromLong8(data, off); + _sendConnID = DataHelper.fromLong8(data, off + 16); + if (_rcvConnID == _sendConnID) + throw new GeneralSecurityException("Identical Conn IDs"); + int type = data[off + 12] & 0xff; + long token = DataHelper.fromLong8(data, off + 24); + if (type == 10) { + _currentState = InboundState.IB_STATE_TOKEN_REQUEST_RECEIVED; + // TODO decrypt chacha? + _sendHeaderEncryptKey2 = introKey; + do { + token = ctx.random().nextLong(); + } while (token == 0); + _token = token; + } else if (type == 0 && token == 0) { // || token not valid + _currentState = InboundState.IB_STATE_REQUEST_BAD_TOKEN_RECEIVED; + _sendHeaderEncryptKey2 = introKey; + do { + token = ctx.random().nextLong(); + } while (token == 0); + _token = token; + } else { + // probably don't need again + _token = token; + _handshakeState.start(); + if (_log.shouldDebug()) + _log.debug("State after start: " + _handshakeState); + _handshakeState.mixHash(data, off, 32); + if (_log.shouldDebug()) + _log.debug("State after mixHash 1: " + _handshakeState); + + byte[] payload = new byte[len - 80]; // 32 hdr, 32 eph. key, 16 MAC + try { + _handshakeState.readMessage(data, off + 32, len - 32, payload, 0); + } catch (GeneralSecurityException gse) { + if (_log.shouldDebug()) + _log.debug("Session request error, State at failure: " + _handshakeState + '\n' + net.i2p.util.HexDump.dump(data, off, len), gse); + throw gse; + } + if (_log.shouldDebug()) + _log.debug("State after sess req: " + _handshakeState); + processPayload(payload, payload.length, true); + _sendHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessCreateHeader"); + _currentState = InboundState.IB_STATE_REQUEST_RECEIVED; + } + _nextToken = ctx.random().nextLong(); + packetReceived(); + } + + @Override + public int getVersion() { return 2; } + + private void processPayload(byte[] payload, int length, boolean isHandshake) throws GeneralSecurityException { + try { + int blocks = SSU2Payload.processPayload(_context, this, payload, 0, length, isHandshake); + System.out.println("Processed " + blocks + " blocks"); + } catch (Exception e) { + _log.error("IES2 payload error\n" + net.i2p.util.HexDump.dump(payload, 0, length)); + throw new GeneralSecurityException("IES2 payload error", e); + } + } + + ///////////////////////////////////////////////////////// + // begin payload callbacks + ///////////////////////////////////////////////////////// + + public void gotDateTime(long time) { + System.out.println("Got DATE block: " + DataHelper.formatTime(time)); + } + + public void gotOptions(byte[] options, boolean isHandshake) { + System.out.println("Got OPTIONS block"); + } + + public void gotRI(RouterInfo ri, boolean isHandshake, boolean flood) throws DataFormatException { + System.out.println("Got RI block: " + ri); + if (isHandshake) + throw new DataFormatException("RI in Sess Req"); + List<RouterAddress> addrs = ri.getTargetAddresses("SSU", "SSU2"); + RouterAddress ra = null; + for (RouterAddress addr : addrs) { + // skip NTCP w/o "s" + if (addrs.size() > 1 && addr.getTransportStyle().equals("SSU") && addr.getOption("s") == null) + continue; + ra = addr; + break; + } + if (ra == null) + throw new DataFormatException("no SSU2 addr"); + String siv = ra.getOption("i"); + if (siv == null) + throw new DataFormatException("no SSU2 IKey"); + byte[] ik = Base64.decode(siv); + if (ik == null) + throw new DataFormatException("bad SSU2 IKey"); + if (ik.length != 32) + throw new DataFormatException("bad SSU2 IKey len"); + String ss = ra.getOption("s"); + if (ss == null) + throw new DataFormatException("no SSU2 S"); + byte[] s = Base64.decode(ss); + if (s == null) + throw new DataFormatException("bad SSU2 S"); + if (s.length != 32) + throw new DataFormatException("bad SSU2 S len"); + if (!"2".equals(ra.getOption("v"))) + throw new DataFormatException("bad SSU2 v"); + + _sendHeaderEncryptKey1 = ik; + //_sendHeaderEncryptKey2 calculated below + + } + + public void gotRIFragment(byte[] data, boolean isHandshake, boolean flood, boolean isGzipped, int frag, int totalFrags) { + System.out.println("Got RI fragment " + frag + " of " + totalFrags); + if (isHandshake) + throw new IllegalStateException("RI in Sess Req"); + } + + public void gotAddress(byte[] ip, int port) { + System.out.println("Got ADDRESS block: " + Addresses.toString(ip, port)); + throw new IllegalStateException("Address in Handshake"); + } + + public void gotIntroKey(byte[] key) { + System.out.println("Got Intro key: " + Base64.encode(key)); + } + + public void gotRelayTagRequest() { + System.out.println("Got relay tag request"); + } + + public void gotRelayTag(long tag) { + System.out.println("Got relay tag " + tag); + throw new IllegalStateException("Relay tag in Handshake"); + } + + public void gotToken(long token, long expires) { + System.out.println("Got NEW TOKEN block " + token + " expires " + DataHelper.formatTime(expires)); + } + + public void gotI2NP(I2NPMessage msg) { + System.out.println("Got I2NP block: " + msg); + if (getState() != InboundState.IB_STATE_CREATED_SENT) + throw new IllegalStateException("I2NP in Sess Req"); + } + + public void gotFragment(byte[] data, long messageID, int type, long expires, int frag, boolean isLast) throws DataFormatException { + System.out.println("Got FRAGMENT block: " + messageID); + if (getState() != InboundState.IB_STATE_CREATED_SENT) + throw new IllegalStateException("I2NP in Sess Req"); + } + + public void gotACK(long ackThru, int acks, byte[] ranges) { + System.out.println("Got ACK block: " + ackThru); + throw new IllegalStateException("ACK in Handshake"); + } + + public void gotTermination(int reason, long count) { + System.out.println("Got TERMINATION block, reason: " + reason + " count: " + count); + throw new IllegalStateException("Termination in Handshake"); + } + + public void gotUnknown(int type, int len) { + System.out.println("Got UNKNOWN block, type: " + type + " len: " + len); + } + + public void gotPadding(int paddingLength, int frameLength) { + System.out.println("Got PADDING block, len: " + paddingLength + " in frame len: " + frameLength); + } + + ///////////////////////////////////////////////////////// + // end payload callbacks + ///////////////////////////////////////////////////////// + + public long getSendConnID() { return _sendConnID; } + public long getRcvConnID() { return _rcvConnID; } + public long getToken() { return _token; } + public long getNextToken() { return _nextToken; } + public HandshakeState getHandshakeState() { return _handshakeState; } + public byte[] getSendHeaderEncryptKey1() { return _sendHeaderEncryptKey1; } + public byte[] getRcvHeaderEncryptKey1() { return _rcvHeaderEncryptKey1; } + public byte[] getSendHeaderEncryptKey2() { return _sendHeaderEncryptKey2; } + public byte[] getRcvHeaderEncryptKey2() { return _rcvHeaderEncryptKey2; } + public InetSocketAddress getSentAddress() { return _aliceSocketAddress; } + + @Override + public synchronized void createdPacketSent() { + /// todo state check + if (_rcvHeaderEncryptKey2 == null) + _rcvHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessionConfirmed"); + _lastSend = _context.clock().now(); + long delay; + if (_createdSentCount == 0) { + delay = RETRANSMIT_DELAY; + } else { + delay = Math.min(RETRANSMIT_DELAY << _createdSentCount, MAX_DELAY); + } + _createdSentCount++; + _nextSend = _lastSend + delay; + if ( (_currentState == InboundState.IB_STATE_UNKNOWN) || (_currentState == InboundState.IB_STATE_REQUEST_RECEIVED) ) + _currentState = InboundState.IB_STATE_CREATED_SENT; + } + + + /** note that we just sent a Retry packet */ + public synchronized void retryPacketSent() { + if (_currentState != InboundState.IB_STATE_REQUEST_BAD_TOKEN_RECEIVED && + _currentState != InboundState.IB_STATE_TOKEN_REQUEST_RECEIVED) + throw new IllegalStateException("Bad state for Retry Sent: " + _currentState); + _currentState = InboundState.IB_STATE_RETRY_SENT; + } + + /** + * + */ + public synchronized void receiveSessionRequestAfterRetry(UDPPacket packet) throws GeneralSecurityException { + if (_currentState != InboundState.IB_STATE_RETRY_SENT) + throw new GeneralSecurityException("Bad state for Session Request after Retry: " + _currentState); + DatagramPacket pkt = packet.getPacket(); + SocketAddress from = pkt.getSocketAddress(); + if (!from.equals(_aliceSocketAddress)) + throw new GeneralSecurityException("Address mismatch: req: " + _aliceSocketAddress + " conf: " + from); + int off = pkt.getOffset(); + int len = pkt.getLength(); + byte data[] = pkt.getData(); + long rid = DataHelper.fromLong8(data, off); + if (rid != _rcvConnID) + throw new GeneralSecurityException("Conn ID mismatch: 1: " + _rcvConnID + " 2: " + rid); + long sid = DataHelper.fromLong8(data, off + 16); + if (sid != _sendConnID) + throw new GeneralSecurityException("Conn ID mismatch: 1: " + _sendConnID + " 2: " + sid); + long token = DataHelper.fromLong8(data, off + 24); + if (token != _token) + throw new GeneralSecurityException("Token mismatch: 1: " + _token + " 2: " + token); + _handshakeState.start(); + _handshakeState.mixHash(data, off, 32); + if (_log.shouldDebug()) + _log.debug("State after mixHash 1: " + _handshakeState); + + byte[] payload = new byte[len - 80]; // 16 hdr, 32 static key, 16 MAC, 16 MAC + try { + _handshakeState.readMessage(data, off + 32, len - 32, payload, 0); + } catch (GeneralSecurityException gse) { + if (_log.shouldDebug()) + _log.debug("Session Request error, State at failure: " + _handshakeState + '\n' + net.i2p.util.HexDump.dump(data, off, len), gse); + throw gse; + } + if (_log.shouldDebug()) + _log.debug("State after sess req: " + _handshakeState); + processPayload(payload, payload.length, true); + _sendHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessCreateHeader"); + _currentState = InboundState.IB_STATE_REQUEST_RECEIVED; + + if (_createdSentCount == 1) { + _rtt = (int) ( _context.clock().now() - _lastSend ); + } + + packetReceived(); + } + + /** + * + * + * + */ + public synchronized void receiveSessionConfirmed(UDPPacket packet) throws GeneralSecurityException { + if (_currentState != InboundState.IB_STATE_CREATED_SENT) + throw new GeneralSecurityException("Bad state for Session Confirmed: " + _currentState); + DatagramPacket pkt = packet.getPacket(); + SocketAddress from = pkt.getSocketAddress(); + if (!from.equals(_aliceSocketAddress)) + throw new GeneralSecurityException("Address mismatch: req: " + _aliceSocketAddress + " conf: " + from); + int off = pkt.getOffset(); + int len = pkt.getLength(); + byte data[] = pkt.getData(); + long rid = DataHelper.fromLong8(data, off); + if (rid != _rcvConnID) + throw new GeneralSecurityException("Conn ID mismatch: req: " + _rcvConnID + " conf: " + rid); + _handshakeState.mixHash(data, off, 16); + if (_log.shouldDebug()) + _log.debug("State after mixHash 3: " + _handshakeState); + + byte[] payload = new byte[len - 80]; // 16 hdr, 32 static key, 16 MAC, 16 MAC + try { + _handshakeState.readMessage(data, off + 16, len - 16, payload, 0); + } catch (GeneralSecurityException gse) { + if (_log.shouldDebug()) + _log.debug("Session Confirmed error, State at failure: " + _handshakeState + '\n' + net.i2p.util.HexDump.dump(data, off, len), gse); + throw gse; + } + if (_log.shouldDebug()) + _log.debug("State after sess conf: " + _handshakeState); + processPayload(payload, payload.length, false); + + // TODO split, calculate keys + + + // TODO fix state + if ( (_currentState == InboundState.IB_STATE_UNKNOWN) || + (_currentState == InboundState.IB_STATE_REQUEST_RECEIVED) || + (_currentState == InboundState.IB_STATE_CREATED_SENT) ) { + if (confirmedFullyReceived()) + _currentState = InboundState.IB_STATE_CONFIRMED_COMPLETELY; + else + _currentState = InboundState.IB_STATE_CONFIRMED_PARTIALLY; + } + + if (_createdSentCount == 1) { + _rtt = (int) ( _context.clock().now() - _lastSend ); + } + + packetReceived(); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(128); + buf.append("IES2 "); + buf.append(Addresses.toString(_aliceIP, _alicePort)); + buf.append(" RelayTag: ").append(_sentRelayTag); + buf.append(' ').append(_currentState); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java new file mode 100644 index 0000000000..86975bec28 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java @@ -0,0 +1,288 @@ +package net.i2p.router.transport.udp; + +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; + +import com.southernstorm.noise.protocol.CipherState; +import com.southernstorm.noise.protocol.CipherStatePair; +import com.southernstorm.noise.protocol.HandshakeState; + +import net.i2p.data.Base64; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.SessionKey; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.router.RouterIdentity; +import net.i2p.data.router.RouterInfo; +import net.i2p.router.RouterContext; +import net.i2p.util.Addresses; +import net.i2p.util.Log; + +/** + * Data for a new connection being established, where we initiated the + * connection with a remote peer. In other words, we are Alice and + * they are Bob. + * + * SSU2 only. + * + * @since 0.9.54 + */ +class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payload.PayloadCallback { + private InetSocketAddress _bobSocketAddress; + private final UDPTransport _transport; + private final long _sendConnID; + private final long _rcvConnID; + private long _token; + private final long _nextToken; + private HandshakeState _handshakeState; + private final byte[] _sendHeaderEncryptKey1; + private final byte[] _rcvHeaderEncryptKey1; + private byte[] _sendHeaderEncryptKey2; + private byte[] _rcvHeaderEncryptKey2; + private final byte[] _rcvRetryHeaderEncryptKey2; + private int _mtu; + private static final boolean SET_TOKEN = false; + + /** + * @param claimedAddress an IP/port based RemoteHostId, or null if unknown + * @param remoteHostId non-null, == claimedAddress if direct, or a hash-based one if indirect + * @param remotePeer must have supported sig type + * @param needIntroduction should we ask Bob to be an introducer for us? + ignored unless allowExtendedOptions is true + * @param introKey Bob's introduction key, as published in the netdb + * @param addr non-null + */ + public OutboundEstablishState2(RouterContext ctx, UDPTransport transport, RemoteHostId claimedAddress, + RemoteHostId remoteHostId, int mtu, + RouterIdentity remotePeer, byte[] publicKey, + boolean needIntroduction, + SessionKey introKey, UDPAddress addr) { + super(ctx, claimedAddress, remoteHostId, remotePeer, needIntroduction, introKey, addr); + _transport = transport; + if (claimedAddress != null) { + try { + _bobSocketAddress = new InetSocketAddress(InetAddress.getByAddress(_bobIP), _bobPort); + } catch (UnknownHostException uhe) { + throw new IllegalArgumentException("bad IP", uhe); + } + _mtu = mtu; + } else { + _mtu = PeerState.MIN_IPV6_MTU; + } + if (addr.getIntroducerCount() > 0) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("new outbound establish to " + remotePeer.calculateHash() + ", with address: " + addr); + _currentState = OutboundState.OB_STATE_PENDING_INTRO; + } else { + _currentState = OutboundState.OB_STATE_UNKNOWN; + } + + // SSU2 + createNewState(publicKey); + + _sendConnID = ctx.random().nextLong(); + // rcid == scid is not allowed + long rcid; + do { + rcid = ctx.random().nextLong(); + } while (_sendConnID == rcid); + if (SET_TOKEN) { + do { + _token = ctx.random().nextLong(); + } while (_token == 0); + } + _rcvConnID = rcid; + _nextToken = ctx.random().nextLong(); + byte[] ik = introKey.getData(); + _sendHeaderEncryptKey1 = ik; + _rcvHeaderEncryptKey1 = ik; + _sendHeaderEncryptKey2 = ik; + //_rcvHeaderEncryptKey2 will be set after the Session Request message is created + _rcvRetryHeaderEncryptKey2 = ik; + } + + private void createNewState(byte[] publicKey) { + try { + _handshakeState = new HandshakeState(HandshakeState.PATTERN_ID_XK_SSU2, HandshakeState.INITIATOR, _transport.getXDHFactory()); + } catch (GeneralSecurityException gse) { + throw new IllegalStateException("bad proto", gse); + } + _handshakeState.getRemotePublicKey().setPublicKey(publicKey, 0); + _handshakeState.getLocalKeyPair().setKeys(_transport.getSSU2StaticPrivKey(), 0, + _transport.getSSU2StaticPubKey(), 0); + } + + public synchronized void restart(long token) { + _token = token; + HandshakeState old = _handshakeState; + byte[] pub = new byte[32]; + old.getRemotePublicKey().getPublicKey(pub, 0); + createNewState(pub); + old.destroy(); + //_rcvHeaderEncryptKey2 will be set after the Session Request message is created + _rcvHeaderEncryptKey2 = null; + } + + private void processPayload(byte[] payload, int length, boolean isHandshake) throws GeneralSecurityException { + try { + int blocks = SSU2Payload.processPayload(_context, this, payload, 0, length, isHandshake); + System.out.println("Processed " + blocks + " blocks"); + } catch (Exception e) { + throw new GeneralSecurityException("Session Created payload error", e); + } + } + + ///////////////////////////////////////////////////////// + // begin payload callbacks + ///////////////////////////////////////////////////////// + + public void gotDateTime(long time) { + System.out.println("Got DATE block: " + DataHelper.formatTime(time)); + } + + public void gotOptions(byte[] options, boolean isHandshake) { + System.out.println("Got OPTIONS block"); + } + + public void gotRI(RouterInfo ri, boolean isHandshake, boolean flood) throws DataFormatException { + System.out.println("Got RI block: " + ri); + throw new DataFormatException("RI in Sess Created"); + } + + public void gotRIFragment(byte[] data, boolean isHandshake, boolean flood, boolean isGzipped, int frag, int totalFrags) { + System.out.println("Got RI fragment " + frag + " of " + totalFrags); + throw new IllegalStateException("RI in Sess Created"); + } + + public void gotAddress(byte[] ip, int port) { + System.out.println("Got ADDRESS block: " + Addresses.toString(ip, port)); + } + + public void gotIntroKey(byte[] key) { + System.out.println("Got Intro key: " + Base64.encode(key)); + } + + public void gotRelayTagRequest() { + System.out.println("Got relay tag request"); + throw new IllegalStateException("Relay tag req in Sess Created"); + } + + public void gotRelayTag(long tag) { + System.out.println("Got relay tag " + tag); + } + + public void gotToken(long token, long expires) { + System.out.println("Got NEW TOKEN block " + token + " expires " + DataHelper.formatTime(expires)); + } + + public void gotI2NP(I2NPMessage msg) { + System.out.println("Got I2NP block: " + msg); + throw new IllegalStateException("I2NP in Sess Created"); + } + + public void gotFragment(byte[] data, long messageID, int type, long expires, int frag, boolean isLast) throws DataFormatException { + System.out.println("Got FRAGMENT block: " + messageID); + throw new IllegalStateException("I2NP in Sess Created"); + } + + public void gotACK(long ackThru, int acks, byte[] ranges) { + System.out.println("Got ACK block: " + ackThru); + throw new IllegalStateException("ACK in Sess Created"); + } + + public void gotTermination(int reason, long count) { + System.out.println("Got TERMINATION block, reason: " + reason + " count: " + count); + throw new IllegalStateException("Termination in Sess Created"); + } + + public void gotUnknown(int type, int len) { + System.out.println("Got UNKNOWN block, type: " + type + " len: " + len); + } + + public void gotPadding(int paddingLength, int frameLength) { + System.out.println("Got PADDING block, len: " + paddingLength + " in frame len: " + frameLength); + } + + ///////////////////////////////////////////////////////// + // end payload callbacks + ///////////////////////////////////////////////////////// + + public long getSendConnID() { return _sendConnID; } + public long getRcvConnID() { return _rcvConnID; } + public long getToken() { return _token; } + public long getNextToken() { return _nextToken; } + public HandshakeState getHandshakeState() { return _handshakeState; } + public byte[] getSendHeaderEncryptKey1() { return _sendHeaderEncryptKey1; } + public byte[] getRcvHeaderEncryptKey1() { return _rcvHeaderEncryptKey1; } + public byte[] getSendHeaderEncryptKey2() { return _sendHeaderEncryptKey2; } + public byte[] getRcvHeaderEncryptKey2() { return _rcvHeaderEncryptKey2; } + public byte[] getRcvRetryHeaderEncryptKey2() { return _rcvRetryHeaderEncryptKey2; } + public InetSocketAddress getSentAddress() { return _bobSocketAddress; } + + /** what is the largest packet we can send to the peer? */ + public int getMTU() { return _mtu; } + + public synchronized void receiveSessionCreated(UDPPacket packet) throws GeneralSecurityException { + ////// todo fix state check + if (_currentState == OutboundState.OB_STATE_VALIDATION_FAILED) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Session created already failed"); + return; + } + + DatagramPacket pkt = packet.getPacket(); + SocketAddress from = pkt.getSocketAddress(); + if (!from.equals(_bobSocketAddress)) + throw new GeneralSecurityException("Address mismatch: req: " + _bobSocketAddress + " created: " + from); + int off = pkt.getOffset(); + int len = pkt.getLength(); + byte data[] = pkt.getData(); + _handshakeState.mixHash(data, off, 32); + if (_log.shouldDebug()) + _log.debug("State after mixHash 2: " + _handshakeState); + + byte[] payload = new byte[len - 80]; // 32 hdr, 32 eph. key, 16 MAC + try { + _handshakeState.readMessage(data, off + 32, len - 32, payload, 0); + } catch (GeneralSecurityException gse) { + if (_log.shouldDebug()) + _log.debug("Session create error, State at failure: " + _handshakeState + '\n' + net.i2p.util.HexDump.dump(data, off, len), gse); + throw gse; + } + if (_log.shouldDebug()) + _log.debug("State after sess cr: " + _handshakeState); + processPayload(payload, payload.length, true); + _sendHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessionConfirmed"); + + if (_currentState == OutboundState.OB_STATE_UNKNOWN || + _currentState == OutboundState.OB_STATE_REQUEST_SENT || + _currentState == OutboundState.OB_STATE_INTRODUCED || + _currentState == OutboundState.OB_STATE_PENDING_INTRO) + _currentState = OutboundState.OB_STATE_CREATED_RECEIVED; + + if (_requestSentCount == 1) { + _rtt = (int) (_context.clock().now() - _requestSentTime); + } + packetReceived(); + } + + /** + * note that we just sent the SessionRequest packet + */ + @Override + public synchronized void requestSent() { + /// TODO store pkt for retx + if (_rcvHeaderEncryptKey2 == null) + _rcvHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessCreateHeader"); + super.requestSent(); + } + + @Override + public String toString() { + return "OES2 " + _remoteHostId + ' ' + _currentState; + } +} diff --git a/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java b/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java new file mode 100644 index 0000000000..7c67268b5c --- /dev/null +++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java @@ -0,0 +1,1173 @@ +package net.i2p.router.transport.udp; + +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import com.southernstorm.noise.protocol.ChaChaPolyCipherState; +import com.southernstorm.noise.protocol.HandshakeState; + +import net.i2p.crypto.ChaCha20; +import net.i2p.data.Base64; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.router.RouterInfo; +import net.i2p.data.SessionKey; +import net.i2p.data.router.RouterAddress; +import net.i2p.router.OutNetMessage; +import net.i2p.router.RouterContext; +import net.i2p.router.transport.TransportUtil; +import net.i2p.router.transport.udp.PacketBuilder.Fragment; +import net.i2p.router.transport.udp.SSU2Payload.Block; +import static net.i2p.router.transport.udp.SSU2Util.*; +import net.i2p.util.Addresses; +import net.i2p.util.Log; + +/** + * SSU2 only + * + * @since 0.9.54 + */ +class PacketBuilder2 { + private final RouterContext _context; + private final Log _log; + private final UDPTransport _transport; + + /** + * For debugging and stats only - does not go out on the wire. + * These are chosen to be higher than the highest I2NP message type, + * as a data packet is set to the underlying I2NP message type. + */ + static final int TYPE_FIRST = 62; + static final int TYPE_ACK = TYPE_FIRST; + static final int TYPE_PUNCH = 63; + static final int TYPE_TCB = 67; + static final int TYPE_TBC = 68; + static final int TYPE_TTA = 69; + static final int TYPE_TFA = 70; + static final int TYPE_CONF = 71; + static final int TYPE_SREQ = 72; + static final int TYPE_CREAT = 73; + + /** IPv4 only */ + public static final int IP_HEADER_SIZE = 20; + /** Same for IPv4 and IPv6 */ + public static final int UDP_HEADER_SIZE = 8; + + /** 74 */ + public static final int MIN_DATA_PACKET_OVERHEAD = IP_HEADER_SIZE + UDP_HEADER_SIZE + DATA_HEADER_SIZE + MAC_LEN; + + public static final int IPV6_HEADER_SIZE = 40; + /** 94 */ + public static final int MIN_IPV6_DATA_PACKET_OVERHEAD = IPV6_HEADER_SIZE + UDP_HEADER_SIZE + DATA_HEADER_SIZE + MAC_LEN; + +/// FIXME + private static final int MAX_IDENTITY_FRAGMENT_SIZE = 1280 - (MIN_DATA_PACKET_OVERHEAD + KEY_LEN + MAC_LEN); + + /** one byte field */ + public static final int ABSOLUTE_MAX_ACKS = 255; + + /* Higher than all other OutNetMessage priorities, but still droppable, + * and will be shown in the codel.UDP-Sender.drop.500 stat. + */ + static final int PRIORITY_HIGH = 550; + private static final int PRIORITY_LOW = OutNetMessage.PRIORITY_LOWEST; + + /** + * No state, all methods are thread-safe. + * + * @param transport may be null for unit testing only + */ + public PacketBuilder2(RouterContext ctx, UDPTransport transport) { + _context = ctx; + _transport = transport; + _log = ctx.logManager().getLog(PacketBuilder2.class); + // all createRateStat in UDPTransport + } + + /** + * Will a packet to 'peer' that already has 'numFragments' fragments + * totalling 'curDataSize' bytes fit another fragment of size 'newFragSize' ?? + * + * This doesn't leave anything for acks. + * + * @param numFragments >= 1 + */ + public static int getMaxAdditionalFragmentSize(PeerState2 peer, int numFragments, int curDataSize) { + int available = peer.getMTU() - curDataSize; + if (peer.isIPv6()) + available -= MIN_IPV6_DATA_PACKET_OVERHEAD; + else + available -= MIN_DATA_PACKET_OVERHEAD; + // OVERHEAD above includes 1 * FRAGMENT+HEADER_SIZE; + // this adds for the others, plus the new one. + available -= numFragments * FIRST_FRAGMENT_HEADER_SIZE; + return available; + } + + /** + * This builds a data packet (PAYLOAD_TYPE_DATA). + * See the methods below for the other message types. + * + * Note that while the UDP message spec allows for more than one fragment in a message, + * this method writes exactly one fragment. + * For no fragments use buildAck(). + * Multiple fragments in a single packet is not supported. + * Rekeying and extended options are not supported. + * + * So ignoring the ack bitfields, and assuming we have explicit acks, + * it's (47 + 4*explict acks + padding) added to the + * fragment length. + * + * @param ackIdsRemaining list of messageIds (Long) that should be acked by this packet. + * The list itself is passed by reference, and if a messageId is + * transmitted it will be removed from the list. + * Not all message IDs will necessarily be sent, there may not be room. + * non-null. + * + * @param newAckCount the number of ackIdsRemaining entries that are new. These must be the first + * ones in the list + * + * @param partialACKsRemaining list of messageIds (ACKBitfield) that should be acked by this packet. + * The list itself is passed by reference, and if a messageId is + * included, it should be removed from the list. + * Full acks in this list are skipped, they are NOT transmitted. + * non-null. + * Not all acks will necessarily be sent, there may not be room. + * + * @return null on error + */ + public UDPPacket buildPacket(OutboundMessageState state, int fragment, PeerState2 peer, + Collection<Long> ackIdsRemaining, int newAckCount, + List<ACKBitfield> partialACKsRemaining) { + List<Fragment> frags = Collections.singletonList(new Fragment(state, fragment)); + return buildPacket(frags, peer, ackIdsRemaining, newAckCount, partialACKsRemaining); + } + + /* + * Multiple fragments + * + */ + public UDPPacket buildPacket(List<Fragment> fragments, PeerState2 peer, + Collection<Long> ackIdsRemaining, int newAckCount, + List<ACKBitfield> partialACKsRemaining) { + // calculate data size + int numFragments = fragments.size(); + int dataSize = 0; + int priority = 0; + for (int i = 0; i < numFragments; i++) { + Fragment frag = fragments.get(i); + OutboundMessageState state = frag.state; + int pri = state.getPriority(); + if (pri > priority) + priority = pri; + int fragment = frag.num; + int sz = state.fragmentSize(fragment); + dataSize += sz; + } + + if (dataSize < 0) + return null; + + // calculate size available for acks + int currentMTU = peer.getMTU(); + int availableForAcks = currentMTU - dataSize; + int ipHeaderSize; + if (peer.isIPv6()) { + availableForAcks -= MIN_IPV6_DATA_PACKET_OVERHEAD; + ipHeaderSize = IPV6_HEADER_SIZE; + } else { + availableForAcks -= MIN_DATA_PACKET_OVERHEAD; + ipHeaderSize = IP_HEADER_SIZE; + } + if (numFragments > 1) + availableForAcks -= (numFragments - 1) * FIRST_FRAGMENT_HEADER_SIZE; + int availableForExplicitAcks = availableForAcks; + + // make the packet + long pktNum = peer.getNextPacketNumber(); + UDPPacket packet = buildShortPacketHeader(peer.getSendConnID(), pktNum, DATA_FLAG_BYTE); + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = SHORT_HEADER_SIZE; + + // ok, now for the body... + // +2 for acks and padding + List<Block> blocks = new ArrayList<Block>(fragments.size() + 2); + +/* + // partial acks have priority but they are after explicit acks in the packet + // so we have to compute the space in advance + int partialAcksToSend = 0; + if (availableForExplicitAcks >= 6 && !partialACKsRemaining.isEmpty()) { + for (ACKBitfield bf : partialACKsRemaining) { + if (partialAcksToSend >= ABSOLUTE_MAX_ACKS) + break; // ack count + // only send what we have to + //int acksz = 4 + (bf.fragmentCount() / 7) + 1; + int bits = bf.highestReceived() + 1; + if (bits <= 0) + continue; + int acksz = bits / 7; + if (bits % 7 > 0) + acksz++; + acksz += 4; + if (partialAcksToSend == 0) + acksz++; // ack count + if (availableForExplicitAcks >= acksz) { + availableForExplicitAcks -= acksz; + partialAcksToSend++; + } else { + break; + } + } + } + + + // always send all the new acks if we have room + int explicitToSend = Math.min(ABSOLUTE_MAX_ACKS, + Math.min(newAckCount + (currentMTU > PeerState.MIN_MTU ? MAX_RESEND_ACKS_LARGE : MAX_RESEND_ACKS_SMALL), + Math.min((availableForExplicitAcks - 1) / 4, ackIdsRemaining.size()))); + if (explicitToSend > 0) { + data[off++] = (byte) explicitToSend; + Iterator<Long> iter = ackIdsRemaining.iterator(); + for (int i = 0; i < explicitToSend && iter.hasNext(); i++) { + Long ackId = iter.next(); + iter.remove(); + } + //acksIncluded = true; + } +*/ + +/* + if (partialAcksToSend > 0) { + if (msg != null) + msg.append(partialAcksToSend).append(" partial acks included:"); + int origNumRemaining = partialACKsRemaining.size(); + int numPartialOffset = off; + // leave it blank for now, since we could skip some + off++; + Iterator<ACKBitfield> iter = partialACKsRemaining.iterator(); + for (int i = 0; i < partialAcksToSend && iter.hasNext(); i++) { + ACKBitfield bitfield = iter.next(); + if (bitfield.receivedComplete()) continue; + // only send what we have to + //int bits = bitfield.fragmentCount(); + int bits = bitfield.highestReceived() + 1; + if (bits <= 0) + continue; + int size = bits / 7; + if (bits % 7 > 0) + size++; + DataHelper.toLong(data, off, 4, bitfield.getMessageId()); + off += 4; + for (int curByte = 0; curByte < size; curByte++) { + if (curByte + 1 < size) + data[off] = (byte)(1 << 7); + else + data[off] = 0; + + for (int curBit = 0; curBit < 7; curBit++) { + if (bitfield.received(curBit + 7*curByte)) + data[off] |= (byte)(1 << curBit); + } + off++; + } + iter.remove(); + } + DataHelper.toLong(data, numPartialOffset, 1, origNumRemaining - partialACKsRemaining.size()); + } +*/ + + // now write each fragment + int sizeWritten = 0; + for (int i = 0; i < numFragments; i++) { + Fragment frag = fragments.get(i); + OutboundMessageState state = frag.state; + int fragment = frag.num; + int count = state.getFragmentCount(); + Block block; + if (fragment == 0) { + if (count == 1) + block = new SSU2Payload.I2NPBlock(state); + else + block = new SSU2Payload.FirstFragBlock(state); + } else { + block = new SSU2Payload.FollowFragBlock(state, fragment); + } + blocks.add(block); + int sz = block.getTotalLength(); + off += sz; + sizeWritten += sz; + } + Block block = getPadding(sizeWritten, peer.getMTU()); + if (block != null) { + blocks.add(block); + int sz = block.getTotalLength(); + off += sz; + sizeWritten += sz; + } + SSU2Payload.writePayload(data, SHORT_HEADER_SIZE, blocks); + pkt.setLength(off); + + encryptDataPacket(packet, peer.getSendEncryptKey(), pktNum, peer.getSendHeaderEncryptKey1(), peer.getSendHeaderEncryptKey2()); + setTo(packet, peer.getRemoteIPAddress(), peer.getRemotePort()); + + // FIXME ticket #2675 + // the packet could have been built before the current mtu got lowered, so + // compare to LARGE_MTU + // Also happens on switch between IPv4 and IPv6 + if (_log.shouldWarn()) { + int maxMTU = peer.isIPv6() ? PeerState.MAX_IPV6_MTU : PeerState.LARGE_MTU; + if (off + (ipHeaderSize + UDP_HEADER_SIZE) > maxMTU) { + _log.warn("Size is " + off + " for " + packet + + " data size " + dataSize + + " pkt size " + (off + (ipHeaderSize + UDP_HEADER_SIZE)) + + " MTU " + currentMTU + +/* + ' ' + availableForAcks + " for all acks, " + + availableForExplicitAcks + " for full acks, " + + explicitToSend + " full acks included, " + + partialAcksToSend + " partial acks included, " + +*/ + " Fragments: " + DataHelper.toString(fragments), new Exception()); + } + } + + packet.setPriority(priority); + return packet; + } + + /** + * An ACK packet with no acks. + * We use this for keepalive purposes. + * It doesn't generate a reply, but that's ok. + */ + public UDPPacket buildPing(PeerState2 peer) { + return buildACK(peer, Collections.<ACKBitfield> emptyList()); + } + + /** + * Build the ack packet. The list need not be sorted into full and partial; + * this method will put all fulls before the partials in the outgoing packet. + * An ack packet is just a data packet with no data. + * See buildPacket() for format. + * + * TODO MTU not enforced. + * TODO handle huge number of acks better + * + * @param ackBitfields list of ACKBitfield instances to either fully or partially ACK + */ + public UDPPacket buildACK(PeerState2 peer, List<ACKBitfield> ackBitfields) { + long pktNum = peer.getNextPacketNumber(); + UDPPacket packet = buildShortPacketHeader(peer.getSendConnID(), pktNum, DATA_FLAG_BYTE); + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = SHORT_HEADER_SIZE; + + int fullACKCount = 0; + int partialACKCount = 0; +/* + for (int i = 0; i < ackBitfields.size(); i++) { + if (ackBitfields.get(i).receivedComplete()) + fullACKCount++; + else + partialACKCount++; + } + +// sort, create ranges + + // FIXME do better than this, we could still exceed MTU + if (fullACKCount > ABSOLUTE_MAX_ACKS || + partialACKCount > ABSOLUTE_MAX_ACKS) + throw new IllegalArgumentException("Too many acks full/partial " + fullACKCount + + '/' + partialACKCount); + + // ok, now for the body... + if (fullACKCount > 0) + data[off] |= UDPPacket.DATA_FLAG_EXPLICIT_ACK; + if (partialACKCount > 0) + data[off] |= UDPPacket.DATA_FLAG_ACK_BITFIELDS; + // add ECN if (peer.getSomethingOrOther()) + off++; + + if (fullACKCount > 0) { + data[off++] = (byte) fullACKCount; + for (int i = 0; i < ackBitfields.size(); i++) { + ACKBitfield bf = ackBitfields.get(i); + if (bf.receivedComplete()) { + DataHelper.toLong(data, off, 4, bf.getMessageId()); + off += 4; + } + } + } + + if (partialACKCount > 0) { + data[off++] = (byte) partialACKCount; + for (int i = 0; i < ackBitfields.size(); i++) { + ACKBitfield bitfield = ackBitfields.get(i); + if (bitfield.receivedComplete()) continue; + DataHelper.toLong(data, off, 4, bitfield.getMessageId()); + off += 4; + // only send what we have to + //int bits = bitfield.fragmentCount(); + int bits = bitfield.highestReceived() + 1; + int size = bits / 7; + if (bits == 0 || bits % 7 > 0) + size++; + for (int curByte = 0; curByte < size; curByte++) { + if (curByte + 1 < size) + data[off] = (byte)(1 << 7); + else + data[off] = 0; + + for (int curBit = 0; curBit < 7; curBit++) { + if (bitfield.received(curBit + 7*curByte)) + data[off] |= (byte)(1 << curBit); + } + off++; + } + } + } +*/ + + pkt.setLength(off); + encryptDataPacket(packet, peer.getSendEncryptKey(), pktNum, peer.getSendHeaderEncryptKey1(), peer.getSendHeaderEncryptKey2()); + setTo(packet, peer.getRemoteIPAddress(), peer.getRemotePort()); + packet.setMessageType(TYPE_ACK); + packet.setPriority((fullACKCount > 0 || partialACKCount > 0) ? PRIORITY_HIGH : PRIORITY_LOW); + return packet; + } + + /** + * Build a new SessionRequest packet for the given peer, encrypting it + * as necessary. + * + * @return ready to send packet, or null if there was a problem + */ + public UDPPacket buildTokenRequestPacket(OutboundEstablishState2 state) { + long n = _context.random().signedNextInt() & 0xFFFFFFFFL; + UDPPacket packet = buildLongPacketHeader(state.getSendConnID(), n, SESSION_REQUEST_FLAG_BYTE, + state.getRcvConnID(), 0); + DatagramPacket pkt = packet.getPacket(); + + byte toIP[] = state.getSentIP(); + if (!_transport.isValid(toIP)) { + packet.release(); + return null; + } + InetAddress to = null; + try { + to = InetAddress.getByAddress(toIP); + } catch (UnknownHostException uhe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("How did we think this was a valid IP? " + state.getRemoteHostId()); + packet.release(); + return null; + } + + pkt.setLength(LONG_HEADER_SIZE); + byte[] introKey = state.getSendHeaderEncryptKey1(); + encryptTokenRequest(packet, introKey, n, introKey, introKey); + state.requestSent(); + setTo(packet, to, state.getSentPort()); + packet.setMessageType(TYPE_SREQ); + packet.setPriority(PRIORITY_HIGH); + return packet; + } + + /** + * Build a new SessionRequest packet for the given peer, encrypting it + * as necessary. + * + * @return ready to send packet, or null if there was a problem + */ + public UDPPacket buildSessionRequestPacket(OutboundEstablishState2 state) { + UDPPacket packet = buildLongPacketHeader(state.getSendConnID(), 0, SESSION_REQUEST_FLAG_BYTE, + state.getRcvConnID(), state.getToken()); + DatagramPacket pkt = packet.getPacket(); + + byte toIP[] = state.getSentIP(); + if (!_transport.isValid(toIP)) { + packet.release(); + return null; + } + InetAddress to = null; + try { + to = InetAddress.getByAddress(toIP); + } catch (UnknownHostException uhe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("How did we think this was a valid IP? " + state.getRemoteHostId()); + packet.release(); + return null; + } + + pkt.setLength(LONG_HEADER_SIZE); + byte[] introKey = state.getSendHeaderEncryptKey1(); + encryptSessionRequest(packet, state.getHandshakeState(), introKey, introKey, state.needIntroduction()); + state.requestSent(); + setTo(packet, to, state.getSentPort()); + packet.setMessageType(TYPE_SREQ); + packet.setPriority(PRIORITY_HIGH); + return packet; + } + + /** + * Build a new SessionCreated packet for the given peer, encrypting it + * as necessary. + * + * @return ready to send packet, or null if there was a problem + */ + public UDPPacket buildSessionCreatedPacket(InboundEstablishState2 state) { + UDPPacket packet = buildLongPacketHeader(state.getSendConnID(), 0, SESSION_CREATED_FLAG_BYTE, + state.getRcvConnID(), state.getToken()); + DatagramPacket pkt = packet.getPacket(); + + byte sentIP[] = state.getSentIP(); + pkt.setLength(LONG_HEADER_SIZE); + int port = state.getSentPort(); + encryptSessionCreated(packet, state.getHandshakeState(), state.getSendHeaderEncryptKey1(), + state.getSendHeaderEncryptKey2(), state.getSentRelayTag(), state.getNextToken(), + sentIP, port); + state.createdPacketSent(); + pkt.setSocketAddress(state.getSentAddress()); + packet.setMessageType(TYPE_CREAT); + packet.setPriority(PRIORITY_HIGH); + return packet; + } + + /** + * Build a new Retry packet for the given peer, encrypting it + * as necessary. + * + * @return ready to send packet, or null if there was a problem + */ + public UDPPacket buildRetryPacket(InboundEstablishState2 state) { + long n = _context.random().signedNextInt() & 0xFFFFFFFFL; + UDPPacket packet = buildLongPacketHeader(state.getSendConnID(), n, RETRY_FLAG_BYTE, + state.getRcvConnID(), state.getToken()); + DatagramPacket pkt = packet.getPacket(); + + byte sentIP[] = state.getSentIP(); + pkt.setLength(LONG_HEADER_SIZE); + int port = state.getSentPort(); + encryptRetry(packet, state.getSendHeaderEncryptKey1(), n, state.getSendHeaderEncryptKey1(), + state.getSendHeaderEncryptKey2(), + sentIP, port); + state.retryPacketSent(); + pkt.setSocketAddress(state.getSentAddress()); + packet.setMessageType(TYPE_CREAT); + packet.setPriority(PRIORITY_HIGH); + return packet; + } + + /** + * Build a new series of SessionConfirmed packets for the given peer, + * encrypting it as necessary. + * + * Note that while a SessionConfirmed could in theory be fragmented, + * in practice a RouterIdentity is 387 bytes and a single fragment is 512 bytes max, + * so it will never be fragmented. + * + * @return ready to send packets, or null if there was a problem + * + * TODO: doesn't really return null, and caller doesn't handle null return + * (null SigningPrivateKey should cause this?) + * Should probably return null if buildSessionConfirmedPacket() returns null for any fragment + */ + public UDPPacket[] buildSessionConfirmedPackets(OutboundEstablishState2 state, RouterInfo ourInfo) { + boolean gzip = false; + byte info[] = ourInfo.toByteArray(); + int mtu = state.getMTU(); + byte toIP[] = state.getSentIP(); + // 20 + 8 + 16 + 32 + 16 + 16 + 3 + 2 = 113 + // 40 + 8 + 16 + 32 + 16 + 16 + 3 + 2 = 133 + int overhead = (toIP.length == 16 ? IPV6_HEADER_SIZE : IP_HEADER_SIZE) + + UDP_HEADER_SIZE + SHORT_HEADER_SIZE + KEY_LEN + MAC_LEN + MAC_LEN + + SSU2Payload.BLOCK_HEADER_SIZE + 2; // RIBlock flags + int max = mtu - overhead; + + int numFragments = info.length / max; + if (numFragments * max != info.length) + numFragments++; + + if (numFragments > 1) { + byte[] gzipped = DataHelper.compress(info, 0, info.length, DataHelper.MAX_COMPRESSION); + if (gzipped.length < info.length) { + if (_log.shouldWarn()) + _log.warn("Gzipping RI, max is " + max + " size was " + info.length + " size now " + gzipped.length); + gzip = true; + info = gzipped; + numFragments = info.length / max; + if (numFragments * max != info.length) + numFragments++; + } + } + + int len; + if (numFragments > 1) { + if (_log.shouldWarn()) + _log.warn("RI size " + info.length + " requires " + numFragments + " packets"); + len = max; + } else { + len = info.length; + } + + + UDPPacket packets[] = new UDPPacket[numFragments]; + packets[0] = buildSessionConfirmedPacket(state, numFragments, info, len, gzip); + if (numFragments > 1) { + // get PeerState from OES + for (int i = 1; i < numFragments; i++) { + //packets[i] = buildSessionConfirmedPacket(state, i, numFragments, info, gzip); + } + // TODO numFragments > 1 requires shift to data phase + throw new IllegalArgumentException("TODO"); + } + state.confirmedPacketsSent(); + return packets; + } + + /** + * Build a new SessionConfirmed packet for the given peer + * + * @return ready to send packet, or null if there was a problem + */ + private UDPPacket buildSessionConfirmedPacket(OutboundEstablishState2 state, int numFragments, byte ourInfo[], int len, boolean gzip) { + UDPPacket packet = buildShortPacketHeader(state.getSendConnID(), 1, SESSION_CONFIRMED_FLAG_BYTE); + DatagramPacket pkt = packet.getPacket(); + + InetAddress to = null; + try { + to = InetAddress.getByAddress(state.getSentIP()); + } catch (UnknownHostException uhe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("How did we think this was a valid IP? " + state.getRemoteHostId()); + packet.release(); + return null; + } + + pkt.setLength(SHORT_HEADER_SIZE); + SSU2Payload.RIBlock block = new SSU2Payload.RIBlock(ourInfo, 0, len, + false, gzip, 0, numFragments); + encryptSessionConfirmed(packet, state.getHandshakeState(), state.getMTU(), + state.getSendHeaderEncryptKey1(), state.getSendHeaderEncryptKey2(), block, state.getNextToken()); + setTo(packet, to, state.getSentPort()); + packet.setMessageType(TYPE_CONF); + packet.setPriority(PRIORITY_HIGH); + return packet; + } + + /** + * Build a packet as if we are Alice and we either want Bob to begin a + * peer test or Charlie to finish a peer test. + * + * @return ready to send packet, or null if there was a problem + */ +/* + public UDPPacket buildPeerTestFromAlice(InetAddress toIP, int toPort, SessionKey toIntroKey, long nonce, SessionKey aliceIntroKey) { + return buildPeerTestFromAlice(toIP, toPort, toIntroKey, toIntroKey, nonce, aliceIntroKey); + } +*/ + + /** + * Build a packet as if we are Alice and we either want Bob to begin a + * peer test or Charlie to finish a peer test. + * + * @return ready to send packet, or null if there was a problem + */ +/* + public UDPPacket buildPeerTestFromAlice(InetAddress toIP, int toPort, SessionKey toCipherKey, SessionKey toMACKey, + long nonce, SessionKey aliceIntroKey) { + UDPPacket packet = buildShortPacketHeader(PEER_TEST_FLAG_BYTE); + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = SHORT_HEADER_SIZE; + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Sending peer test " + nonce + " to Bob"); + + // now for the body + DataHelper.toLong(data, off, 4, nonce); + off += 4; + data[off++] = 0; // neither Bob nor Charlie need Alice's IP from her + DataHelper.toLong(data, off, 2, 0); // neither Bob nor Charlie need Alice's port from her + off += 2; + System.arraycopy(aliceIntroKey.getData(), 0, data, off, SessionKey.KEYSIZE_BYTES); + off += SessionKey.KEYSIZE_BYTES; + + pkt.setLength(off); + authenticate(packet, toCipherKey, toMACKey); + setTo(packet, toIP, toPort); + packet.setMessageType(TYPE_TFA); + packet.setPriority(PRIORITY_LOW); + return packet; + } +*/ + + /** + * Build a packet as if we are either Bob or Charlie and we are helping test Alice. + * Not for use as Bob, as of 0.9.52; use in-session cipher/mac keys instead. + * + * @return ready to send packet, or null if there was a problem + */ +/* + public UDPPacket buildPeerTestToAlice(InetAddress aliceIP, int alicePort, + SessionKey aliceIntroKey, SessionKey charlieIntroKey, long nonce) { + return buildPeerTestToAlice(aliceIP, alicePort, aliceIntroKey, aliceIntroKey, charlieIntroKey, nonce); + } +*/ + + /** + * Build a packet as if we are either Bob or Charlie and we are helping test Alice. + * + * @param aliceCipherKey the intro key if we are Charlie + * @param aliceMACKey the intro key if we are Charlie + * @return ready to send packet, or null if there was a problem + */ +/* + public UDPPacket buildPeerTestToAlice(InetAddress aliceIP, int alicePort, + SessionKey aliceCipherKey, SessionKey aliceMACKey, + SessionKey charlieIntroKey, long nonce) { + UDPPacket packet = buildShortPacketHeader(PEER_TEST_FLAG_BYTE); + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = SHORT_HEADER_SIZE; + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Sending peer test " + nonce + " to Alice"); + + // now for the body + DataHelper.toLong(data, off, 4, nonce); + off += 4; + byte ip[] = aliceIP.getAddress(); + data[off++] = (byte) ip.length; + System.arraycopy(ip, 0, data, off, ip.length); + off += ip.length; + DataHelper.toLong(data, off, 2, alicePort); + off += 2; + System.arraycopy(charlieIntroKey.getData(), 0, data, off, SessionKey.KEYSIZE_BYTES); + off += SessionKey.KEYSIZE_BYTES; + + pkt.setLength(off); + authenticate(packet, aliceCipherKey, aliceMACKey); + setTo(packet, aliceIP, alicePort); + packet.setMessageType(TYPE_TTA); + packet.setPriority(PRIORITY_LOW); + return packet; + } +*/ + + /** + * Build a packet as if we are Bob sending Charlie a packet to help test Alice. + * + * @return ready to send packet, or null if there was a problem + */ +/* + public UDPPacket buildPeerTestToCharlie(InetAddress aliceIP, int alicePort, SessionKey aliceIntroKey, long nonce, + InetAddress charlieIP, int charliePort, + SessionKey charlieCipherKey, SessionKey charlieMACKey) { + UDPPacket packet = buildShortPacketHeader(PEER_TEST_FLAG_BYTE); + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = SHORT_HEADER_SIZE; + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Sending peer test " + nonce + " to Charlie"); + + // now for the body + DataHelper.toLong(data, off, 4, nonce); + off += 4; + byte ip[] = aliceIP.getAddress(); + data[off++] = (byte) ip.length; + System.arraycopy(ip, 0, data, off, ip.length); + off += ip.length; + DataHelper.toLong(data, off, 2, alicePort); + off += 2; + System.arraycopy(aliceIntroKey.getData(), 0, data, off, SessionKey.KEYSIZE_BYTES); + off += SessionKey.KEYSIZE_BYTES; + + pkt.setLength(off); + authenticate(packet, charlieCipherKey, charlieMACKey); + setTo(packet, charlieIP, charliePort); + packet.setMessageType(TYPE_TBC); + packet.setPriority(PRIORITY_LOW); + return packet; + } +*/ + + /** + * Build a packet as if we are Charlie sending Bob a packet verifying that we will help test Alice. + * + * @return ready to send packet, or null if there was a problem + */ +/* + public UDPPacket buildPeerTestToBob(InetAddress bobIP, int bobPort, InetAddress aliceIP, int alicePort, + SessionKey aliceIntroKey, long nonce, + SessionKey bobCipherKey, SessionKey bobMACKey) { + UDPPacket packet = buildShortPacketHeader(PEER_TEST_FLAG_BYTE); + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = SHORT_HEADER_SIZE; + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Sending peer test " + nonce + " to Bob"); + + // now for the body + DataHelper.toLong(data, off, 4, nonce); + off += 4; + byte ip[] = aliceIP.getAddress(); + data[off++] = (byte) ip.length; + System.arraycopy(ip, 0, data, off, ip.length); + off += ip.length; + DataHelper.toLong(data, off, 2, alicePort); + off += 2; + System.arraycopy(aliceIntroKey.getData(), 0, data, off, SessionKey.KEYSIZE_BYTES); + off += SessionKey.KEYSIZE_BYTES; + + pkt.setLength(off); + authenticate(packet, bobCipherKey, bobMACKey); + setTo(packet, bobIP, bobPort); + packet.setMessageType(TYPE_TCB); + packet.setPriority(PRIORITY_LOW); + return packet; + } +*/ + + /** + * Creates an empty unauthenticated packet for hole punching. + * Parameters must be validated previously. + */ + public UDPPacket buildHolePunch(InetAddress to, int port) { + UDPPacket packet = UDPPacket.acquire(_context, false); + if (_log.shouldLog(Log.INFO)) + _log.info("Sending relay hole punch to " + to + ":" + port); + + // the packet is empty and does not need to be authenticated, since + // its just for hole punching + packet.getPacket().setLength(0); + setTo(packet, to, port); + + packet.setMessageType(TYPE_PUNCH); + packet.setPriority(PRIORITY_HIGH); + return packet; + } + + /** + * @param pktNum 0 - 0xFFFFFFFF + * @return a packet with the first 32 bytes filled in + */ + private UDPPacket buildLongPacketHeader(long destID, long pktNum, byte type, long srcID, long token) { + if (_log.shouldDebug()) + _log.debug("Building long header destID " + destID + " pkt num " + pktNum + " type " + type + " srcID " + srcID + " token " + token); + UDPPacket packet = buildShortPacketHeader(destID, pktNum, type); + byte data[] = packet.getPacket().getData(); + data[13] = PROTOCOL_VERSION; + data[14] = (byte) _context.router().getNetworkID(); + DataHelper.toLong8(data, 16, srcID); + DataHelper.toLong8(data, 24, token); + return packet; + } + + /** + * @param pktNum 0 - 0xFFFFFFFF + * @return a packet with the first 16 bytes filled in + */ + private UDPPacket buildShortPacketHeader(long destID, long pktNum, byte type) { + UDPPacket packet = UDPPacket.acquire(_context, false); + byte data[] = packet.getPacket().getData(); + Arrays.fill(data, 0, data.length, (byte) 0); + DataHelper.toLong8(data, 0, destID); + DataHelper.toLong(data, 8, 4, pktNum); + data[12] = type; + return packet; + } + + private static void setTo(UDPPacket packet, InetAddress ip, int port) { + DatagramPacket pkt = packet.getPacket(); + pkt.setAddress(ip); + pkt.setPort(port); + } + + /** + * @param packet containing only 32 byte header + */ + private void encryptSessionRequest(UDPPacket packet, HandshakeState state, + byte[] hdrKey1, byte[] hdrKey2, boolean needIntro) { + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = pkt.getOffset(); + try { + if (_log.shouldDebug()) + _log.debug("After start: " + state); + List<Block> blocks = new ArrayList<Block>(3); + Block block = new SSU2Payload.DateTimeBlock(_context); + int len = block.getTotalLength(); + blocks.add(block); + if (needIntro) { + block = new SSU2Payload.RelayTagRequestBlock(); + len += block.getTotalLength(); + blocks.add(block); + } + // plenty of room + block = getPadding(len, 1280); + len += block.getTotalLength(); + blocks.add(block); + + // If we skip past where the ephemeral key will be, we can + // use the packet for the plaintext and Noise will symmetric encrypt in-place + SSU2Payload.writePayload(data, off + LONG_HEADER_SIZE + KEY_LEN, blocks); + state.start(); + if (_log.shouldDebug()) + _log.debug("State after start: " + state); + state.mixHash(data, off, LONG_HEADER_SIZE); + if (_log.shouldDebug()) + _log.debug("State after mixHash 1: " + state); + state.writeMessage(data, off + LONG_HEADER_SIZE, data, off + LONG_HEADER_SIZE + KEY_LEN, len); + pkt.setLength(pkt.getLength() + KEY_LEN + len + MAC_LEN); + } catch (RuntimeException re) { + if (!_log.shouldWarn()) + _log.error("Bad msg 1 out", re); + throw re; + } catch (GeneralSecurityException gse) { + if (!_log.shouldWarn()) + _log.error("Bad msg 1 out", gse); + throw new RuntimeException("Bad msg 1 out", gse); + } + if (_log.shouldDebug()) + _log.debug("After msg 1: " + state + '\n' + net.i2p.util.HexDump.dump(data, off, pkt.getLength())); + SSU2Header.encryptHandshakeHeader(packet, hdrKey1, hdrKey2); + if (_log.shouldDebug()) + _log.debug("Hdr key 1: " + Base64.encode(hdrKey1) + " Hdr key 2: " + Base64.encode(hdrKey2)); + } + + /** + * @param packet containing only 32 byte header + */ + private void encryptSessionCreated(UDPPacket packet, HandshakeState state, + byte[] hdrKey1, byte[] hdrKey2, long relayTag, long token, byte[] ip, int port) { + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = pkt.getOffset(); + try { + List<Block> blocks = new ArrayList<Block>(4); + Block block = new SSU2Payload.DateTimeBlock(_context); + int len = block.getTotalLength(); + blocks.add(block); + block = new SSU2Payload.AddressBlock(ip, port); + len += block.getTotalLength(); + blocks.add(block); + if (relayTag > 0) { + block = new SSU2Payload.RelayTagBlock(relayTag); + len += block.getTotalLength(); + blocks.add(block); + } + if (token > 0) { + block = new SSU2Payload.NewTokenBlock(token, _context.clock().now() + 6*24*60*60*1000L); + len += block.getTotalLength(); + blocks.add(block); + } + // plenty of room + block = getPadding(len, 1280); + len += block.getTotalLength(); + blocks.add(block); + + // If we skip past where the ephemeral key will be, we can + // use the packet for the plaintext and Noise will symmetric encrypt in-place + SSU2Payload.writePayload(data, off + LONG_HEADER_SIZE + KEY_LEN, blocks); + + state.mixHash(data, off, LONG_HEADER_SIZE); + if (_log.shouldDebug()) + _log.debug("State after mixHash 2: " + state); + state.writeMessage(data, off + LONG_HEADER_SIZE, data, off + LONG_HEADER_SIZE + KEY_LEN, len); + pkt.setLength(pkt.getLength() + KEY_LEN + len + MAC_LEN); + } catch (RuntimeException re) { + if (!_log.shouldWarn()) + _log.error("Bad msg 2 out", re); + throw re; + } catch (GeneralSecurityException gse) { + if (!_log.shouldWarn()) + _log.error("Bad msg 2 out", gse); + throw new RuntimeException("Bad msg 2 out", gse); + } + if (_log.shouldDebug()) + _log.debug("After msg 2: " + state); + SSU2Header.encryptHandshakeHeader(packet, hdrKey1, hdrKey2); + } + + /** + * @param packet containing only 32 byte header + */ + private void encryptRetry(UDPPacket packet, byte[] chachaKey, long n, + byte[] hdrKey1, byte[] hdrKey2, byte[] ip, int port) { + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = pkt.getOffset(); + try { + List<Block> blocks = new ArrayList<Block>(4); + Block block = new SSU2Payload.DateTimeBlock(_context); + int len = block.getTotalLength(); + blocks.add(block); + block = new SSU2Payload.AddressBlock(ip, port); + len += block.getTotalLength(); + blocks.add(block); + // plenty of room + block = getPadding(len, 1280); + len += block.getTotalLength(); + blocks.add(block); + byte[] payload = new byte[len]; + SSU2Payload.writePayload(payload, 0, blocks); + + ChaChaPolyCipherState chacha = new ChaChaPolyCipherState(); + chacha.initializeKey(chachaKey, 0); + chacha.setNonce(n); + chacha.encryptWithAd(data, off, LONG_HEADER_SIZE, + data, off + LONG_HEADER_SIZE, data, off + LONG_HEADER_SIZE, len); + + pkt.setLength(pkt.getLength() + len + MAC_LEN); + } catch (RuntimeException re) { + if (!_log.shouldWarn()) + _log.error("Bad retry msg out", re); + throw re; + } catch (GeneralSecurityException gse) { + if (!_log.shouldWarn()) + _log.error("Bad retry msg out", gse); + throw new RuntimeException("Bad retry msg out", gse); + } + SSU2Header.encryptLongHeader(packet, hdrKey1, hdrKey2); + } + + /** + * @param packet containing only 32 byte header + */ + private void encryptTokenRequest(UDPPacket packet, byte[] chachaKey, long n, + byte[] hdrKey1, byte[] hdrKey2) { + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = pkt.getOffset(); + try { + List<Block> blocks = new ArrayList<Block>(4); + Block block = new SSU2Payload.DateTimeBlock(_context); + int len = block.getTotalLength(); + blocks.add(block); + // plenty of room + block = getPadding(len, 1280); + len += block.getTotalLength(); + blocks.add(block); + byte[] payload = new byte[len]; + SSU2Payload.writePayload(payload, 0, blocks); + + ChaChaPolyCipherState chacha = new ChaChaPolyCipherState(); + chacha.initializeKey(chachaKey, 0); + chacha.setNonce(n); + chacha.encryptWithAd(data, off, LONG_HEADER_SIZE, + data, off + LONG_HEADER_SIZE, data, off + LONG_HEADER_SIZE, len - LONG_HEADER_SIZE); + + pkt.setLength(pkt.getLength() + len + MAC_LEN); + } catch (RuntimeException re) { + if (!_log.shouldWarn()) + _log.error("Bad token req msg out", re); + throw re; + } catch (GeneralSecurityException gse) { + if (!_log.shouldWarn()) + _log.error("Bad token req msg out", gse); + throw new RuntimeException("Bad token req msg out", gse); + } + SSU2Header.encryptHandshakeHeader(packet, hdrKey1, hdrKey2); + } + + /** + * @param packet containing only 16 byte header + */ + private void encryptSessionConfirmed(UDPPacket packet, HandshakeState state, int mtu, + byte[] hdrKey1, byte[] hdrKey2, + SSU2Payload.RIBlock riblock, long token) { + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = pkt.getOffset(); + try { + List<Block> blocks = new ArrayList<Block>(3); + int len = riblock.getTotalLength(); + blocks.add(riblock); + if (token > 0) { + // TODO only if room + Block block = new SSU2Payload.NewTokenBlock(token, _context.clock().now() + 6*24*60*60*1000L); + len += block.getTotalLength(); + blocks.add(block); + } + Block block = getPadding(len, mtu - 80); + if (block != null) { + len += block.getTotalLength(); + blocks.add(block); + } + + // If we skip past where the static key and 1st MAC will be, we can + // use the packet for the plaintext and Noise will symmetric encrypt in-place + SSU2Payload.writePayload(data, off + SHORT_HEADER_SIZE + KEY_LEN + MAC_LEN, blocks); + state.mixHash(data, off, SHORT_HEADER_SIZE); + if (_log.shouldDebug()) + _log.debug("State after mixHash 3: " + state); + state.writeMessage(data, off + SHORT_HEADER_SIZE, data, off + SHORT_HEADER_SIZE + KEY_LEN + MAC_LEN, len); + pkt.setLength(pkt.getLength() + KEY_LEN + MAC_LEN + len + MAC_LEN); + } catch (RuntimeException re) { + if (!_log.shouldWarn()) + _log.error("Bad msg 3 out", re); + throw re; + } catch (GeneralSecurityException gse) { + if (!_log.shouldWarn()) + _log.error("Bad msg 3 out", gse); + throw new RuntimeException("Bad msg 1 out", gse); + } + if (_log.shouldDebug()) + _log.debug("After msg 3: " + state); + SSU2Header.encryptShortHeader(packet, hdrKey1, hdrKey2); + } + + /** + * @param packet containing 16 byte header and all data with + * length set to the end of the data. + * This will extend the length by 16 for the MAC. + */ + private void encryptDataPacket(UDPPacket packet, byte[] chachaKey, long n, + byte[] hdrKey1, byte[] hdrKey2) { + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = pkt.getOffset(); + int len = pkt.getLength(); + ChaChaPolyCipherState chacha = new ChaChaPolyCipherState(); + chacha.initializeKey(chachaKey, 0); + chacha.setNonce(n); + try { + chacha.encryptWithAd(data, off, SHORT_HEADER_SIZE, + data, off + SHORT_HEADER_SIZE, data, off + SHORT_HEADER_SIZE, len - SHORT_HEADER_SIZE); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Bad data msg", e); + } + pkt.setLength(len + MAC_LEN); + SSU2Header.encryptShortHeader(packet, hdrKey1, hdrKey2); + } + + /** + * @param len current length of the packet + * @param max max length of the packet + * @return null if no room + */ + private Block getPadding(int len, int max) { + int maxpadlen = Math.min(max - len, PADDING_MAX) - SSU2Payload.BLOCK_HEADER_SIZE; + if (maxpadlen < 0) + return null; + int padlen; + if (maxpadlen == 0) + padlen = 0; + else + padlen = _context.random().nextInt(maxpadlen + 1); + return new SSU2Payload.PaddingBlock(padlen); + } + + private void writePayload(List<Block> blocks, byte[] data, int off) { + SSU2Payload.writePayload(data, off, blocks); + } +} diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState2.java b/router/java/src/net/i2p/router/transport/udp/PeerState2.java new file mode 100644 index 0000000000..df31c12f8c --- /dev/null +++ b/router/java/src/net/i2p/router/transport/udp/PeerState2.java @@ -0,0 +1,68 @@ +package net.i2p.router.transport.udp; + +import java.net.InetSocketAddress; +import java.util.concurrent.atomic.AtomicInteger; + +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.SessionKey; +import net.i2p.router.RouterContext; +import net.i2p.util.Log; +import net.i2p.util.SimpleTimer2; + +/** + * Contain all of the state about a UDP connection to a peer. + * This is instantiated only after a connection is fully established. + * + * Public only for UI peers page. Not a public API, not for external use. + * + * SSU2 only. + * + * @since 0.9.54 + */ +public class PeerState2 extends PeerState { + private final long _sendConnID; + private final long _rcvConnID; + private final AtomicInteger _packetNumber = new AtomicInteger(); + private final byte[] _sendEncryptKey; + private final byte[] _rcvEncryptKey; + private final byte[] _sendHeaderEncryptKey1; + private final byte[] _rcvHeaderEncryptKey1; + private final byte[] _sendHeaderEncryptKey2; + private final byte[] _rcvHeaderEncryptKey2; + private final SSU2Bitfield _receivedMessages; + + public static final int MIN_MTU = 1280; + + /** + * @param rtt from the EstablishState, or 0 if not available + */ + public PeerState2(RouterContext ctx, UDPTransport transport, + InetSocketAddress remoteAddress, Hash remotePeer, boolean isInbound, int rtt, + byte[] sendKey, byte[] rcvKey, long sendID, long rcvID, + byte[] sendHdrKey1, byte[] sendHdrKey2, byte[] rcvHdrKey2) { + super(ctx, transport, remoteAddress, remotePeer, isInbound, rtt); + _sendConnID = sendID; + _rcvConnID = rcvID; + _sendEncryptKey = sendKey; + _rcvEncryptKey = rcvKey; + _sendHeaderEncryptKey1 = sendHdrKey1; + _rcvHeaderEncryptKey1 = transport.getSSU2StaticIntroKey(); + _sendHeaderEncryptKey2 = sendHdrKey2; + _rcvHeaderEncryptKey2 = rcvHdrKey2; + _receivedMessages = new SSU2Bitfield(256, 0); + } + + // SSU2 + long getNextPacketNumber() { return _packetNumber.incrementAndGet(); } + public long getSendConnID() { return _sendConnID; } + public long getRcvConnID() { return _rcvConnID; } + public byte[] getSendEncryptKey() { return _sendEncryptKey; } + public byte[] getRcvEncryptKey() { return _rcvEncryptKey; } + public byte[] getSendHeaderEncryptKey1() { return _sendHeaderEncryptKey1; } + public byte[] getRcvHeaderEncryptKey1() { return _rcvHeaderEncryptKey1; } + public byte[] getSendHeaderEncryptKey2() { return _sendHeaderEncryptKey2; } + public byte[] getRcvHeaderEncryptKey2() { return _rcvHeaderEncryptKey2; } + public SSU2Bitfield getReceivedMessages() { return _receivedMessages; } + +} diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java index c6a6c6da3b..53ea224b33 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java @@ -21,7 +21,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import net.i2p.CoreVersion; +import net.i2p.crypto.EncType; import net.i2p.crypto.HMACGenerator; +import net.i2p.crypto.KeyPair; import net.i2p.crypto.SigType; import net.i2p.data.Base64; import net.i2p.data.DatabaseEntry; @@ -30,6 +32,7 @@ import net.i2p.data.Hash; import net.i2p.data.router.RouterAddress; import net.i2p.data.router.RouterIdentity; import net.i2p.data.router.RouterInfo; +import net.i2p.data.PrivateKey; import net.i2p.data.SessionKey; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.I2NPMessage; @@ -46,6 +49,7 @@ import net.i2p.router.transport.TransportImpl; import net.i2p.router.transport.TransportUtil; import static net.i2p.router.transport.TransportUtil.IPv6Config.*; import net.i2p.router.transport.crypto.DHSessionKeyBuilder; +import net.i2p.router.transport.crypto.X25519KeyFactory; import static net.i2p.router.transport.udp.PeerTestState.Role.*; import net.i2p.router.util.EventLog; import net.i2p.router.util.RandomIterator; @@ -132,6 +136,22 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority private RouterAddress _currentOurV4Address; private RouterAddress _currentOurV6Address; + // SSU2 + private final boolean _enableSSU1; + private final boolean _enableSSU2; + private final PacketBuilder2 _packetBuilder2; + private final X25519KeyFactory _xdhFactory; + private final byte[] _ssu2StaticPubKey; + private final byte[] _ssu2StaticPrivKey; + private final byte[] _ssu2StaticIntroKey; + private final String _ssu2B64StaticPubKey; + private final String _ssu2B64StaticIntroKey; + /** b64 static private key */ + public static final String PROP_SSU2_SP = "i2np.ssu2.sp"; + /** b64 static IV */ + public static final String PROP_SSU2_IKEY = "i2np.ssu2.ikey"; + private static final long MIN_DOWNTIME_TO_REKEY_HIDDEN = 24*60*60*1000L; + private static final int DROPLIST_PERIOD = 10*60*1000; public static final String STYLE = "SSU"; public static final String PROP_INTERNAL_PORT = "i2np.udp.internalPort"; @@ -295,10 +315,14 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority Status.IPV4_DISABLED_IPV6_OK); - public UDPTransport(RouterContext ctx, DHSessionKeyBuilder.Factory dh) { + /** + * @param xdh non-null to enable SSU2 + */ + public UDPTransport(RouterContext ctx, DHSessionKeyBuilder.Factory dh, X25519KeyFactory xdh) { super(ctx); _networkID = ctx.router().getNetworkID(); _dhFactory = dh; + _xdhFactory = xdh; _log = ctx.logManager().getLog(UDPTransport.class); _peersByIdent = new ConcurrentHashMap<Hash, PeerState>(128); _peersByRemoteHost = new ConcurrentHashMap<RemoteHostId, PeerState>(128); @@ -322,6 +346,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority } _packetBuilder = new PacketBuilder(_context, this); + _packetBuilder2 = (xdh != null) ? new PacketBuilder2(_context, this) : null; _fragments = new OutboundMessageFragments(_context, this, _activeThrottle); _inboundFragments = new InboundMessageFragments(_context, _fragments, this); //if (SHOULD_FLOOD_PEERS) @@ -363,6 +388,63 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority //_context.statManager().createRateStat("udp.packetAuthTimeSlow", "How long it takes to encrypt and MAC a packet for sending (when its slow)", "udp", RATES); _context.simpleTimer2().addPeriodicEvent(new PingIntroducers(), MIN_EXPIRE_TIMEOUT * 3 / 4); + + // SSU2 key and IV generation if required + _enableSSU1 = dh != null; + _enableSSU2 = xdh != null; + byte[] ikey = null; + String b64Ikey = null; + if (_enableSSU2) { + byte[] priv = null; + boolean shouldSave = false; + String s = null; + // try to determine if we've been down for 30 days or more + long minDowntime = _context.router().isHidden() ? MIN_DOWNTIME_TO_REKEY_HIDDEN : MIN_DOWNTIME_TO_REKEY; + boolean shouldRekey = _context.getEstimatedDowntime() >= minDowntime; + if (!shouldRekey) { + s = ctx.getProperty(PROP_SSU2_SP); + if (s != null) { + priv = Base64.decode(s); + } + } + if (priv == null || priv.length != SSU2Util.KEY_LEN) { + KeyPair keys = xdh.getKeys(); + _ssu2StaticPrivKey = keys.getPrivate().getData(); + _ssu2StaticPubKey = keys.getPublic().getData(); + shouldSave = true; + } else { + _ssu2StaticPrivKey = priv; + _ssu2StaticPubKey = (new PrivateKey(EncType.ECIES_X25519, priv)).toPublic().getData(); + } + if (!shouldSave) { + s = ctx.getProperty(PROP_SSU2_IKEY); + if (s != null) { + ikey = Base64.decode(s); + b64Ikey = s; + } + } + if (ikey == null || ikey.length != SSU2Util.INTRO_KEY_LEN) { + ikey = new byte[SSU2Util.INTRO_KEY_LEN]; + do { + ctx.random().nextBytes(ikey); + } while (DataHelper.eq(ikey, 0, SSU2Util.ZEROKEY, 0, SSU2Util.INTRO_KEY_LEN)); + shouldSave = true; + } + if (shouldSave) { + Map<String, String> changes = new HashMap<String, String>(2); + String b64Priv = Base64.encode(_ssu2StaticPrivKey); + b64Ikey = Base64.encode(ikey); + changes.put(PROP_SSU2_SP, b64Priv); + changes.put(PROP_SSU2_IKEY, b64Ikey); + ctx.router().saveConfig(changes, null); + } + } else { + _ssu2StaticPrivKey = null; + _ssu2StaticPubKey = null; + } + _ssu2StaticIntroKey = ikey; + _ssu2B64StaticIntroKey = b64Ikey; + _ssu2B64StaticPubKey = (_ssu2StaticPubKey != null) ? Base64.encode(_ssu2StaticPubKey) : null; } /** @@ -372,6 +454,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority OutboundMessageFragments getOMF() { return _fragments; } + /** * Pick a port if not previously configured, so that TransportManager may @@ -779,6 +862,36 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority */ SessionKey getIntroKey() { return _introKey; } + /** + * The static Intro key + * + * @return null if not configured for SSU2 + * @since 0.9.54 + */ + byte[] getSSU2StaticIntroKey() { + return _ssu2StaticIntroKey; + } + + /** + * The static pub key + * + * @return null if not configured for SSU2 + * @since 0.9.54 + */ + byte[] getSSU2StaticPubKey() { + return _ssu2StaticPubKey; + } + + /** + * The static priv key + * + * @return null if not configured for SSU2 + * @since 0.9.54 + */ + byte[] getSSU2StaticPrivKey() { + return _ssu2StaticPrivKey; + } + /** * Published or requested port */ @@ -3205,6 +3318,14 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority DHSessionKeyBuilder.Factory getDHFactory() { return _dhFactory; } + + /** + * @return null if not configured for SSU2 + * @since 0.9.54 + */ + X25519KeyFactory getXDHFactory() { + return _xdhFactory; + } /** * @return the SSU HMAC @@ -3222,6 +3343,14 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority return _packetBuilder; } + /** + * @return null if not configured for SSU2 + * @since 0.9.54 + */ + PacketBuilder2 getBuilder2() { + return _packetBuilder2; + } + /** * Does nothing * @deprecated as of 0.9.31 -- GitLab