From b1852c127bc50c8a53c9ab9f9e63b68814b12d4f Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 27 Mar 2022 12:36:12 -0400 Subject: [PATCH] SSU2: Prep for fragmented RI (WIP) Fix IES2 not being removed from pending after complete Don't send DSM after handshake Validate conn ID before pkt type for data pkts More log tweaks SSU: Increase min pending establish states limit (unless slow) Reduce IB establish timeout to 15s Log pending inbound establish states when limit exceeded Log tweaks to add establish state lifetime Bump -11 --- history.txt | 23 +++++++- .../src/net/i2p/router/RouterVersion.java | 2 +- .../transport/udp/EstablishmentManager.java | 58 +++++++++++++------ .../transport/udp/InboundEstablishState.java | 1 + .../transport/udp/InboundEstablishState2.java | 7 ++- .../transport/udp/OutboundEstablishState.java | 4 +- .../udp/OutboundEstablishState2.java | 10 +++- .../router/transport/udp/PacketBuilder2.java | 30 +++++----- .../router/transport/udp/PacketHandler.java | 2 +- .../i2p/router/transport/udp/PeerState.java | 4 +- .../i2p/router/transport/udp/PeerState2.java | 49 +++++++++++----- 11 files changed, 134 insertions(+), 56 deletions(-) diff --git a/history.txt b/history.txt index dc909b39a..afe73c94c 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,24 @@ +2022-03-27 zzz + * Crypto: Fix CertUtil loading EdDSA certs, check sigs + * Router: Validate family sig at startup + * SSU: Increase min pending establish states limit + * SSU2: Misc. fixes, prep for fragmented RI + +2022-03-26 zzz + * Crypto: Add official EdDSA OIDs to provider + * SSU: Reduce ack delay to minimize addition to measured RTT + * SSU2: Hook in IMF Bloom filter to detect dups + +2022-03-25 zzz + * Console: Prevent creating a family that another router is using + +2022-03-23 zzz + * I2CP: Synch fixes + * SSU2: Refactor tokens + +2022-03-22 zzz + * Router family fixes + 2022-03-20 zzz * NetDB: - Refactor family validation @@ -66,7 +87,7 @@ * SSU: SSU2 classes and keys (WIP) 2022-02-23 zzz - * i2psnark: Load sytem mime types if available + * i2psnark: Load system mime types if available * SSU: More SSU2 prep and support (WIP) 2022-02-22 zzz diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 837dbdd0f..3c8f141dc 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Git"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 10; + public final static long BUILD = 11; /** for example "-test" */ public final static String EXTRA = ""; diff --git a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java index bc6b57660..afadcd948 100644 --- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java +++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java @@ -33,6 +33,7 @@ import net.i2p.util.Addresses; import net.i2p.util.I2PThread; import net.i2p.util.LHMCache; import net.i2p.util.Log; +import net.i2p.util.SystemVersion; import net.i2p.util.VersionComparator; /** @@ -103,7 +104,7 @@ class EstablishmentManager { /** max outbound in progress - max inbound is half of this */ private final int DEFAULT_MAX_CONCURRENT_ESTABLISH; - private static final int DEFAULT_LOW_MAX_CONCURRENT_ESTABLISH = 20; + private static final int DEFAULT_LOW_MAX_CONCURRENT_ESTABLISH = SystemVersion.isSlow() ? 20 : 40; private static final int DEFAULT_HIGH_MAX_CONCURRENT_ESTABLISH = 150; private static final String PROP_MAX_CONCURRENT_ESTABLISH = "i2np.udp.maxConcurrentEstablish"; @@ -130,7 +131,7 @@ class EstablishmentManager { * Kill any inbound that takes more than this * One round trip (Created-Confirmed) */ - private static final int MAX_IB_ESTABLISH_TIME = 20*1000; + private static final int MAX_IB_ESTABLISH_TIME = 15*1000; /** max before receiving a response to a single message during outbound establishment */ public static final int OB_MESSAGE_TIMEOUT = 15*1000; @@ -513,8 +514,17 @@ class EstablishmentManager { // TODO this is insufficient to prevent DoSing, especially if // IP spoofing is used. For further study. if (!shouldAllowInboundEstablishment()) { - if (_log.shouldLog(Log.WARN)) + if (_log.shouldLog(Log.WARN)) { _log.warn("Dropping inbound establish, increase " + PROP_MAX_CONCURRENT_ESTABLISH); + if (_log.shouldDebug()) { + StringBuilder buf = new StringBuilder(4096); + buf.append("Active: ").append(_inboundStates.size()).append('\n'); + for (InboundEstablishState ies : _inboundStates.values()) { + buf.append(ies.toString()).append('\n'); + } + _log.debug(buf.toString()); + } + } _context.statManager().addRateData("udp.establishDropped", 1); return; // drop the packet } @@ -697,8 +707,14 @@ class EstablishmentManager { state.fail(); return; } - // we are done, go right to ps2 - handleCompletelyEstablished(state); + InboundEstablishState.InboundState istate = state.getState(); + if (istate == IB_STATE_CONFIRMED_COMPLETELY || + istate == IB_STATE_COMPLETE) { + // we are done, go right to ps2 + handleCompletelyEstablished(state); + } else { + // More RI blocks to come, TODO + } notifyActivity(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Receive session confirmed from: " + state); @@ -997,13 +1013,19 @@ class EstablishmentManager { // SimpleTimer.getInstance().addEvent(new PublishToNewInbound(peer), 10*1000); if (_log.shouldDebug()) _log.debug("IB confirm: " + peer); - DeliveryStatusMessage dsm = new DeliveryStatusMessage(_context); - dsm.setArrival(_networkID); // overloaded, sure, but future versions can check this + DeliveryStatusMessage dsm; + if (peer.getVersion() == 1) { + dsm = new DeliveryStatusMessage(_context); + dsm.setArrival(_networkID); // overloaded, sure, but future versions can check this // This causes huge values in the inNetPool.droppedDeliveryStatusDelay stat // so it needs to be caught in InNetMessagePool. - dsm.setMessageExpiration(_context.clock().now() + DATA_MESSAGE_TIMEOUT); - dsm.setMessageId(_context.random().nextLong(I2NPMessage.MAX_ID_VALUE)); - // sent below + dsm.setMessageExpiration(_context.clock().now() + DATA_MESSAGE_TIMEOUT); + dsm.setMessageId(_context.random().nextLong(I2NPMessage.MAX_ID_VALUE)); + // sent below + } else { + // SSU 2 uses an ACK of packet 0 + dsm = null; + } // just do this inline //_context.simpleTimer2().addEvent(new PublishToNewInbound(peer), 0); @@ -1016,10 +1038,11 @@ class EstablishmentManager { // bundle the two messages together for efficiency DatabaseStoreMessage dbsm = getOurInfo(); List msgs = new ArrayList(2); - msgs.add(dsm); + if (dsm != null) + msgs.add(dsm); msgs.add(dbsm); _transport.send(msgs, peer); - } else { + } else if (dsm != null) { _transport.send(dsm, peer); // nuh uh. if (_log.shouldLog(Log.WARN)) @@ -1465,6 +1488,7 @@ class EstablishmentManager { private void sendDestroy(InboundEstablishState state) { if (state.getVersion() > 1) return; + // TODO ban the IP for a while, like we do in NTCP? UDPPacket packet = _builder.buildSessionDestroyPacket(state); if (packet != null) { if (_log.shouldLog(Log.DEBUG)) @@ -1486,7 +1510,8 @@ class EstablishmentManager { for (Iterator iter = _inboundStates.values().iterator(); iter.hasNext(); ) { InboundEstablishState cur = iter.next(); - if (cur.getState() == IB_STATE_CONFIRMED_COMPLETELY) { + InboundEstablishState.InboundState istate = cur.getState(); + if (istate == IB_STATE_CONFIRMED_COMPLETELY) { // completely received (though the signature may be invalid) iter.remove(); inboundState = cur; @@ -1498,13 +1523,12 @@ class EstablishmentManager { iter.remove(); inboundState = cur; //_context.statManager().addRateData("udp.inboundEstablishFailedState", cur.getState(), cur.getLifetime()); - //if (_log.shouldLog(Log.DEBUG)) - // _log.debug("Removing expired inbound state"); + if (_log.shouldDebug()) + _log.debug("Expired: " + cur); expired = true; break; - } else if (cur.getState() == IB_STATE_FAILED) { + } else if (istate == IB_STATE_FAILED || istate == IB_STATE_COMPLETE) { iter.remove(); - //_context.statManager().addRateData("udp.inboundEstablishFailedState", cur.getState(), cur.getLifetime()); } else { // this will always be > 0 long next = cur.getNextSendTime(); diff --git a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java index 805476e77..5981fb6d5 100644 --- a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java +++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java @@ -550,6 +550,7 @@ class InboundEstablishState { //if (_sentY != null) // buf.append(" SentY: ").append(Base64.encode(_sentY, 0, 4)); //buf.append(" Bob: ").append(Addresses.toString(_bobIP, _bobPort)); + buf.append(" lifetime: ").append(DataHelper.formatDuration(getLifetime())); buf.append(" RelayTag: ").append(_sentRelayTag); //buf.append(" SignedOn: ").append(_sentSignedOnTime); buf.append(' ').append(_currentState); diff --git a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java index 2830e0197..8ec6029ea 100644 --- a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java +++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java @@ -163,8 +163,10 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa if (_timeReceived == 0) throw new GeneralSecurityException("No DateTime block in Session/Token Request"); _skew = _establishBegin - _timeReceived; - if (_skew > MAX_SKEW || _skew < 0 - MAX_SKEW) + if (_skew > MAX_SKEW || _skew < 0 - MAX_SKEW) { + // TODO send retry with termination throw new GeneralSecurityException("Skew exceeded in Session/Token Request: " + _skew); + } packetReceived(); } @@ -593,6 +595,7 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa sender.initializeKey(d_ba, 0); ChaChaPolyCipherState rcvr = new ChaChaPolyCipherState(); rcvr.initializeKey(d_ab, 0); + /**** if (_log.shouldDebug()) _log.debug("split()\nGenerated Chain key: " + Base64.encode(ckd) + "\nGenerated split key for A->B: " + Base64.encode(k_ab) + @@ -603,6 +606,7 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa "\nIntro key for Bob: " + Base64.encode(_rcvHeaderEncryptKey1) + "\nGenerated header key 2 for A->B: " + Base64.encode(h_ab) + "\nGenerated header key 2 for B->A: " + Base64.encode(h_ba)); + ****/ _handshakeState.destroy(); if (_createdSentCount == 1) _rtt = (int) ( _context.clock().now() - _lastSend ); @@ -706,6 +710,7 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa StringBuilder buf = new StringBuilder(128); buf.append("IES2 "); buf.append(Addresses.toString(_aliceIP, _alicePort)); + buf.append(" lifetime: ").append(DataHelper.formatDuration(getLifetime())); buf.append(" Rcv ID: ").append(_rcvConnID); buf.append(" Send ID: ").append(_sendConnID); buf.append(" RelayTag: ").append(_sentRelayTag); diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java index 3bd8011cb..b50f60da0 100644 --- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java +++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java @@ -768,6 +768,8 @@ class OutboundEstablishState { /** @since 0.8.9 */ @Override public String toString() { - return "OES " + _remoteHostId + ' ' + _currentState; + return "OES " + _remoteHostId + + " lifetime: " + DataHelper.formatDuration(getLifetime()) + + ' ' + _currentState; } } diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java index 4b53cff3e..770dbc3d8 100644 --- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java +++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java @@ -6,6 +6,7 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.UnknownHostException; import java.security.GeneralSecurityException; +import java.util.List; import com.southernstorm.noise.protocol.ChaChaPolyCipherState; import com.southernstorm.noise.protocol.CipherState; @@ -461,13 +462,15 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl * note that we just sent the SessionConfirmed packets * and save them for retransmission * + * @param riFrags if non-null, the RI was fragmented, and these are the + * remaining fragments to be sent in the PeerState. * @return the new PeerState2, may also be retrieved from getPeerState() */ - public synchronized PeerState2 confirmedPacketsSent(UDPPacket[] packets) { + public synchronized PeerState2 confirmedPacketSent(UDPPacket packet, List riFrags) { if (_sessConfForReTX == null) { // store pkt for retx // only one supported right now - DatagramPacket pkt = packets[0].getPacket(); + DatagramPacket pkt = packet.getPacket(); byte data[] = pkt.getData(); int off = pkt.getOffset(); int len = pkt.getLength(); @@ -515,7 +518,7 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl _sendConnID, _rcvConnID, _sendHeaderEncryptKey1, h_ab, h_ba); _currentState = OutboundState.OB_STATE_CONFIRMED_COMPLETELY; - _pstate.confirmedPacketsSent(_sessConfForReTX); + _pstate.confirmedPacketSent(_sessConfForReTX, riFrags); // PS2.super adds CLOCK_SKEW_FUDGE that doesn't apply here _pstate.adjustClockSkew(_skew - (_rtt / 2) - PeerState.CLOCK_SKEW_FUDGE); _pstate.setHisMTU(_mtu); @@ -554,6 +557,7 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl @Override public String toString() { return "OES2 " + _remoteHostId + + " lifetime: " + DataHelper.formatDuration(getLifetime()) + " Rcv ID: " + _rcvConnID + " Send ID: " + _sendConnID + ' ' + _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 index 662548b53..0b18cf412 100644 --- a/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java +++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java @@ -443,15 +443,12 @@ class PacketBuilder2 { * 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. + * If the RI is large enough that it is fragmented, this will still only return + * a single Session Confirmed message. The remaining RI blocks will be passed to + * the establish state via confirmedPacketsSent(), and the state will + * transmit them via the new PeerState2. * * @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; @@ -493,17 +490,24 @@ class PacketBuilder2 { len = info.length; } - UDPPacket packets[] = new UDPPacket[numFragments]; + UDPPacket packets[] = new UDPPacket[1]; packets[0] = buildSessionConfirmedPacket(state, numFragments, info, len, gzip); + List riFrags; if (numFragments > 1) { - // get PeerState from OES + riFrags = new ArrayList(numFragments - 1); + int off = len; for (int i = 1; i < numFragments; i++) { - //packets[i] = buildSessionConfirmedPacket(state, i, numFragments, info, gzip); + if (i == numFragments - 1) + len = info.length - off; + SSU2Payload.RIBlock block = new SSU2Payload.RIBlock(info, off, len, + false, gzip, i, numFragments); + riFrags.add(block); + off += len; } - // TODO numFragments > 1 requires shift to data phase - throw new IllegalArgumentException("TODO"); + } else { + riFrags = null; } - state.confirmedPacketsSent(packets); + state.confirmedPacketSent(packets[0], riFrags); return packets; } diff --git a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java index be1c7ffc8..143f9b35e 100644 --- a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java +++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java @@ -881,7 +881,7 @@ class PacketHandler { if (header.getPacketNumber() != 0 || header.getType() != SSU2Util.SESSION_CONFIRMED_FLAG_BYTE) { if (_log.shouldWarn()) - _log.warn("Queue possible data packet on: " + state); + _log.warn("Queue possible data packet with header " + header + " on: " + state); // TODO either attempt to decrypt as a retransmitted // Session Request or Token Request, // or just tell establisher so it can retransmit Session Created or Retry diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState.java b/router/java/src/net/i2p/router/transport/udp/PeerState.java index 3292ee04f..b5748f266 100644 --- a/router/java/src/net/i2p/router/transport/udp/PeerState.java +++ b/router/java/src/net/i2p/router/transport/udp/PeerState.java @@ -1570,8 +1570,8 @@ public class PeerState { // no need to nudge(), this is called from OMF loop before allocateSend() } if (rv <= 0) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug(_remotePeer + " nothing pending, cancelling timer"); + //if (_log.shouldLog(Log.DEBUG)) + // _log.debug(_remotePeer + " nothing pending, cancelling timer"); synchronized(this) { _retransmitTimer = 0; exitFastRetransmit(); diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState2.java b/router/java/src/net/i2p/router/transport/udp/PeerState2.java index 08093c360..1ab54c1a2 100644 --- a/router/java/src/net/i2p/router/transport/udp/PeerState2.java +++ b/router/java/src/net/i2p/router/transport/udp/PeerState2.java @@ -5,6 +5,7 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.security.GeneralSecurityException; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -60,6 +61,7 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback // Session Confirmed retransmit private byte[] _sessConfForReTX; + private List _riFragsForReTX; private long _sessConfSentTime; private int _sessConfSentCount; @@ -292,20 +294,20 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback //if (_log.shouldDebug()) // _log.debug("Packet before header decryption:\n" + HexDump.dump(data, off, len)); try { - if (len < MIN_DATA_LEN) { + SSU2Header.Header header = SSU2Header.trialDecryptShortHeader(packet, _rcvHeaderEncryptKey1, _rcvHeaderEncryptKey2); + if (header == null) { if (_log.shouldWarn()) _log.warn("Inbound packet too short " + len + " on " + this); return; } - SSU2Header.Header header = SSU2Header.trialDecryptShortHeader(packet, _rcvHeaderEncryptKey1, _rcvHeaderEncryptKey2); - if (header == null) { + if (header.getDestConnID() != _rcvConnID) { if (_log.shouldWarn()) - _log.warn("bad data header on " + this); + _log.warn("bad Dest Conn id " + header.getDestConnID() + " size " + len + " on " + this); return; } if (header.getType() != DATA_FLAG_BYTE) { if (_log.shouldWarn()) - _log.warn("bad data pkt type " + (header.getType() & 0xff) + " on " + this); + _log.warn("bad data pkt type " + (header.getType() & 0xff) + " size " + len + " on " + this); // TODO if it's early: // If inbound, could be a retransmitted Session Confirmed, // ack it again. @@ -316,11 +318,6 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback // we didn't know the session has disconnected yet. return; } - if (header.getDestConnID() != _rcvConnID) { - if (_log.shouldWarn()) - _log.warn("bad Dest Conn id " + header.getDestConnID() + " on " + this); - return; - } long n = header.getPacketNumber(); SSU2Header.acceptTrialDecrypt(packet, header); //if (_log.shouldDebug()) @@ -341,8 +338,8 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback return; } int payloadLen = len - (SHORT_HEADER_SIZE + MAC_LEN); - if (_log.shouldInfo()) - _log.info("New " + len + " byte pkt " + n + " rcvd on " + this); + if (_log.shouldDebug()) + _log.debug("New " + len + " byte pkt " + n + " rcvd on " + this); processPayload(data, off + SHORT_HEADER_SIZE, payloadLen); packetReceived(payloadLen); } catch (GeneralSecurityException gse) { @@ -642,12 +639,23 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback } /** - * note that we just sent the SessionConfirmed packets - * and save them for retransmission + * note that we just sent the SessionConfirmed packet + * and save it for retransmission. + * + * @param riFrags if non-null, the RI was fragmented, and these are the + * remaining fragments to be sent and saved for retransmission. */ - public synchronized void confirmedPacketsSent(byte[] data) { + public synchronized void confirmedPacketSent(byte[] data, List riFrags) { if (_sessConfForReTX == null) _sessConfForReTX = data; + if (riFrags != null) { + if (_riFragsForReTX == null) + _riFragsForReTX = riFrags; + for (SSU2Payload.RIBlock block : riFrags) { + UDPPacket pkt = _transport.getBuilder2().buildPacket(Collections.emptyList(), Collections.singletonList(block), this); + _transport.send(pkt); + } + } _sessConfSentTime = _context.clock().now(); _sessConfSentCount++; } @@ -659,7 +667,10 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback if (_sessConfForReTX == null) return null; UDPPacket packet = UDPPacket.acquire(_context, false); - UDPPacket[] rv = new UDPPacket[1]; + int count = 1; + if (_riFragsForReTX != null) + count += _riFragsForReTX.size(); + UDPPacket[] rv = new UDPPacket[count]; rv[0] = packet; DatagramPacket pkt = packet.getPacket(); byte data[] = pkt.getData(); @@ -670,6 +681,12 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback pkt.setPort(_remotePort); packet.setMessageType(PacketBuilder2.TYPE_CONF); packet.setPriority(PacketBuilder2.PRIORITY_HIGH); + if (_riFragsForReTX != null) { + int i = 1; + for (SSU2Payload.RIBlock block : _riFragsForReTX) { + rv[i++] = _transport.getBuilder2().buildPacket(Collections.emptyList(), Collections.singletonList(block), this); + } + } return rv; }