Skip to content

Commit 5643cc6

Browse files
committed
Add the DTLS 1.3 reliable handshake and pack handshake flights into datagrams
Adds the ACK-driven reliable handshake of RFC 9147 sections 5.8 and 7, and packs handshake flights into as few datagrams as the MTU allows. The packing applies to DTLS 1.2 as well as 1.3, since that is what github #1487 asks for: on the existing aggregated-handshake test a client flight went from 5 datagrams to 3 for the same 1279 bytes. Only handshake records are packed, and a non-handshake record flushes the buffer before it leaves, so write order is preserved; that matters for the implicit change_cipher_spec, which must not overtake the flight it follows. The reliable handshake registers each written fragment against the record number that carried it, retires fragments when an ACK arrives, retransmits only what is outstanding, and emits ACKs on the RFC 9147 7.1 triggers. Inbound ACKs are filtered by the epoch of the record carrying them, and both ACK emission and the accumulated record-number list are bounded by what fits in one datagram. DTLS 1.3 still cannot be negotiated, so none of the 1.3 paths are reachable yet. DTLSReassembler.contributeFragment changed from void to boolean and gained acceptsFragment and getNextExpectedOffset; the admission predicate is unchanged, and DTLS 1.2 behaviour is unaffected. Two test helpers, MinimalHandshakeAggregator and ServerHandshakeDropper, decided what to do by inspecting only the first record of a datagram, which was exact only while each datagram carried one record. Both now walk every record. relates to github #1468. closes github #1487.
1 parent 405626e commit 5643cc6

23 files changed

Lines changed: 3079 additions & 197 deletions
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package org.bouncycastle.tls;
2+
3+
import java.util.Enumeration;
4+
import java.util.Hashtable;
5+
import java.util.Vector;
6+
7+
/**
8+
* RFC 9147 7.2. Tracks which record carried which handshake fragment of the current outbound flight, so
9+
* that an ACK retires exactly the fragments it covers and a retransmission resends only what is left.
10+
* <p>
11+
* A fragment may be registered more than once, under a different record number each time it is sent. It
12+
* is acknowledged as soon as any one of those records is acknowledged.
13+
* </p>
14+
*/
15+
class DTLS13FlightTracker
16+
{
17+
/** One handshake fragment of the current outbound flight. */
18+
static final class Fragment
19+
{
20+
private final int messageSeq;
21+
private final int fragmentOffset;
22+
private final int fragmentLength;
23+
24+
boolean acknowledged = false;
25+
26+
Fragment(int messageSeq, int fragmentOffset, int fragmentLength)
27+
{
28+
this.messageSeq = messageSeq;
29+
this.fragmentOffset = fragmentOffset;
30+
this.fragmentLength = fragmentLength;
31+
}
32+
33+
int getMessageSeq()
34+
{
35+
return messageSeq;
36+
}
37+
38+
int getFragmentOffset()
39+
{
40+
return fragmentOffset;
41+
}
42+
43+
int getFragmentLength()
44+
{
45+
return fragmentLength;
46+
}
47+
48+
private String key()
49+
{
50+
return messageSeq + ":" + fragmentOffset + ":" + fragmentLength;
51+
}
52+
}
53+
54+
// record number -> Fragment
55+
private Hashtable carriers = new Hashtable();
56+
// fragment key -> Fragment, so the same fragment sent twice is one entry
57+
private Hashtable fragments = new Hashtable();
58+
// fragments in registration order, for deterministic retransmission
59+
private Vector order = new Vector();
60+
61+
void reset()
62+
{
63+
carriers = new Hashtable();
64+
fragments = new Hashtable();
65+
order = new Vector();
66+
}
67+
68+
void register(DTLSRecordNumber recordNumber, int messageSeq, int fragmentOffset, int fragmentLength)
69+
{
70+
Fragment fragment = new Fragment(messageSeq, fragmentOffset, fragmentLength);
71+
String key = fragment.key();
72+
73+
Fragment existing = (Fragment)fragments.get(key);
74+
if (null == existing)
75+
{
76+
fragments.put(key, fragment);
77+
order.addElement(fragment);
78+
existing = fragment;
79+
}
80+
81+
if (null != recordNumber)
82+
{
83+
carriers.put(recordNumber, existing);
84+
}
85+
}
86+
87+
void acknowledge(Vector recordNumbers)
88+
{
89+
for (int i = 0; i < recordNumbers.size(); ++i)
90+
{
91+
Fragment fragment = (Fragment)carriers.get(recordNumbers.elementAt(i));
92+
if (null != fragment)
93+
{
94+
fragment.acknowledged = true;
95+
}
96+
}
97+
}
98+
99+
boolean isEmpty()
100+
{
101+
return order.isEmpty();
102+
}
103+
104+
/**
105+
* @return true if fragments were registered and every one of them has been acknowledged.
106+
*/
107+
boolean isComplete()
108+
{
109+
if (order.isEmpty())
110+
{
111+
return false;
112+
}
113+
114+
Enumeration e = order.elements();
115+
while (e.hasMoreElements())
116+
{
117+
if (!((Fragment)e.nextElement()).acknowledged)
118+
{
119+
return false;
120+
}
121+
}
122+
return true;
123+
}
124+
125+
/**
126+
* @return the fragments not yet acknowledged, in the order they were first registered.
127+
*/
128+
Vector getOutstanding()
129+
{
130+
Vector outstanding = new Vector();
131+
132+
Enumeration e = order.elements();
133+
while (e.hasMoreElements())
134+
{
135+
Fragment fragment = (Fragment)e.nextElement();
136+
if (!fragment.acknowledged)
137+
{
138+
outstanding.addElement(fragment);
139+
}
140+
}
141+
142+
return outstanding;
143+
}
144+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package org.bouncycastle.tls;
2+
3+
import java.io.IOException;
4+
import java.util.Vector;
5+
6+
/**
7+
* RFC 9147 7. The ACK message, carried in its own content type rather than as a handshake message so
8+
* that it is not added to the handshake transcript.
9+
* <pre>
10+
* struct {
11+
* RecordNumber record_numbers&lt;0..2^16-1&gt;;
12+
* } ACK;
13+
* </pre>
14+
*/
15+
class DTLSAck
16+
{
17+
/** The wire size of one RecordNumber: two uint64. */
18+
static final int RECORD_NUMBER_LENGTH = 16;
19+
20+
/**
21+
* Encode a list of {@link DTLSRecordNumber} as an ACK body. An empty list is valid (RFC 9147 7.1).
22+
*/
23+
static byte[] encode(Vector recordNumbers) throws IOException
24+
{
25+
int count = recordNumbers.size();
26+
int bodyLength = count * RECORD_NUMBER_LENGTH;
27+
TlsUtils.checkUint16(bodyLength);
28+
29+
byte[] buf = new byte[2 + bodyLength];
30+
TlsUtils.writeUint16(bodyLength, buf, 0);
31+
32+
int pos = 2;
33+
for (int i = 0; i < count; ++i)
34+
{
35+
DTLSRecordNumber recordNumber = (DTLSRecordNumber)recordNumbers.elementAt(i);
36+
TlsUtils.writeUint64(recordNumber.getEpoch(), buf, pos);
37+
TlsUtils.writeUint64(recordNumber.getSequenceNumber(), buf, pos + 8);
38+
pos += RECORD_NUMBER_LENGTH;
39+
}
40+
41+
return buf;
42+
}
43+
44+
/**
45+
* Decode an ACK body.
46+
*
47+
* @return the record numbers in wire order, or null if the body is malformed. A malformed ACK is
48+
* discarded rather than failing the connection (RFC 9147 4.5.2).
49+
*/
50+
static Vector decode(byte[] buf, int off, int len) throws IOException
51+
{
52+
if (len < 2)
53+
{
54+
return null;
55+
}
56+
57+
int bodyLength = TlsUtils.readUint16(buf, off);
58+
if (bodyLength != len - 2 || (bodyLength % RECORD_NUMBER_LENGTH) != 0)
59+
{
60+
return null;
61+
}
62+
63+
Vector recordNumbers = new Vector();
64+
65+
int pos = off + 2;
66+
int count = bodyLength / RECORD_NUMBER_LENGTH;
67+
for (int i = 0; i < count; ++i)
68+
{
69+
long epoch = TlsUtils.readUint64(buf, pos);
70+
long sequenceNumber = TlsUtils.readUint64(buf, pos + 8);
71+
recordNumbers.addElement(new DTLSRecordNumber(epoch, sequenceNumber));
72+
pos += RECORD_NUMBER_LENGTH;
73+
}
74+
75+
return recordNumbers;
76+
}
77+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.bouncycastle.tls;
2+
3+
import java.io.IOException;
4+
import java.util.Vector;
5+
6+
/**
7+
* RFC 9147 7. Receives ACK records from the record layer. ACK is a content type rather than a handshake
8+
* message, so it does not reach the handshake through the normal message path.
9+
*/
10+
interface DTLSAckListener
11+
{
12+
/**
13+
* @param recordNumbers the acknowledged {@link DTLSRecordNumber}s, in the order they appeared.
14+
*/
15+
void receivedAck(Vector recordNumbers) throws IOException;
16+
}

tls/src/main/java/org/bouncycastle/tls/DTLSReassembler.java

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,43 @@ byte[] getBodyIfComplete()
3939
return missing.isEmpty() ? body : null;
4040
}
4141

42-
void contributeFragment(short msg_type, int length, byte[] buf, int off, int fragment_offset,
42+
/**
43+
* @return the offset of the first byte of this message that has not been received yet, or -1 once the
44+
* message is complete. Supports the RFC 9147 7.1 out-of-order ACK trigger: a fragment that does
45+
* not begin here is not the next piece of this message.
46+
*/
47+
int getNextExpectedOffset()
48+
{
49+
return missing.isEmpty() ? -1 : ((Range)missing.firstElement()).start;
50+
}
51+
52+
/**
53+
* Whether a fragment with these parameters is one this reassembler could hold, which is the difference
54+
* between a fragment that is rejected outright and one that is merely already held. RFC 9147 7.1
55+
* acknowledges a record whose message was discarded because a previous copy had been received, but not
56+
* one whose message was rejected, and only the reassembler knows which of the two happened.
57+
*
58+
* @return true if the fragment belongs to this message.
59+
*/
60+
boolean acceptsFragment(short msg_type, int length, int fragment_offset, int fragment_length)
61+
{
62+
return this.msg_type == msg_type && this.body.length == length
63+
&& fragment_offset + fragment_length <= length;
64+
}
65+
66+
/**
67+
* @return true if the fragment contributed at least one byte (or completed an empty message), false if
68+
* it was ignored, whether because it was rejected or because every byte of it was already held.
69+
* {@link #acceptsFragment(short, int, int, int)} separates those two cases.
70+
*/
71+
boolean contributeFragment(short msg_type, int length, byte[] buf, int off, int fragment_offset,
4372
int fragment_length)
4473
{
4574
int fragment_end = fragment_offset + fragment_length;
4675

47-
if (this.msg_type != msg_type || this.body.length != length || fragment_end > length)
76+
if (!acceptsFragment(msg_type, length, fragment_offset, fragment_length))
4877
{
49-
return;
78+
return false;
5079
}
5180

5281
// NOTE: Empty messages still require an empty fragment to complete it
@@ -55,10 +84,13 @@ void contributeFragment(short msg_type, int length, byte[] buf, int off, int fra
5584
if (fragment_offset == 0 && !missing.isEmpty() && ((Range)missing.firstElement()).end == 0)
5685
{
5786
missing.removeElementAt(0);
87+
return true;
5888
}
59-
return;
89+
return false;
6090
}
6191

92+
boolean contributed = false;
93+
6294
for (int i = findStartIndex(fragment_offset); i < missing.size(); ++i)
6395
{
6496
Range range = (Range)missing.elementAt(i);
@@ -103,7 +135,10 @@ void contributeFragment(short msg_type, int length, byte[] buf, int off, int fra
103135
}
104136

105137
System.arraycopy(buf, off + copyStart - fragment_offset, body, copyStart, copyLength);
138+
contributed = true;
106139
}
140+
141+
return contributed;
107142
}
108143

109144
/**

0 commit comments

Comments
 (0)