Add DTLS timeout and retransmit tests.

This extends the packet adaptor protocol to send three commands:
  type command =
    | Packet of []byte
    | Timeout of time.Duration
    | TimeoutAck

When the shim processes a Timeout in BIO_read, it sends TimeoutAck, fails the
BIO_read, returns out of the SSL stack, advances the clock, calls
DTLSv1_handle_timeout, and continues.

If the Go side sends Timeout right between sending handshake flight N and
reading flight N+1, the shim won't read the Timeout until it has sent flight
N+1 (it only processes packet commands in BIO_read), so the TimeoutAck comes
after N+1. Go then drops all packets before the TimeoutAck, thus dropping one
transmit of flight N+1 without having to actually process the packets to
determine the end of the flight. The shim then sees the updated clock, calls
DTLSv1_handle_timeout, and re-sends flight N+1 for Go to process for real.

When dropping packets, Go checks the epoch and increments sequence numbers so
that we can continue to be strict here. This requires tracking the initial
sequence number of the next epoch.

The final Finished message takes an additional special-case to test. DTLS
triggers retransmits on either a timeout or seeing a stale flight. OpenSSL only
implements the former which should be sufficient (and is necessary) EXCEPT for
the final Finished message. If the peer's final Finished message is lost, it
won't be waiting for a message from us, so it won't time out anything. That
retransmit must be triggered on stale message, so we retransmit the Finished
message in Go.

Change-Id: I3ffbdb1de525beb2ee831b304670a3387877634c
Reviewed-on: https://boringssl-review.googlesource.com/3212
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index 33881fc..bfe6e36 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -41,6 +41,7 @@
 }
 
 static int g_ex_data_index = 0;
+static int g_ex_data_clock_index = 0;
 
 static bool SetConfigPtr(SSL *ssl, const TestConfig *config) {
   return SSL_set_ex_data(ssl, g_ex_data_index, (void *)config) == 1;
@@ -50,6 +51,14 @@
   return (const TestConfig *)SSL_get_ex_data(ssl, g_ex_data_index);
 }
 
+static bool SetClockPtr(SSL *ssl, OPENSSL_timeval *clock) {
+  return SSL_set_ex_data(ssl, g_ex_data_clock_index, (void *)clock) == 1;
+}
+
+static OPENSSL_timeval *GetClockPtr(SSL *ssl) {
+  return (OPENSSL_timeval *)SSL_get_ex_data(ssl, g_ex_data_clock_index);
+}
+
 static EVP_PKEY *LoadPrivateKey(const std::string &file) {
   BIO *bio = BIO_new(BIO_s_file());
   if (bio == NULL) {
@@ -229,7 +238,7 @@
 }
 
 static void current_time_cb(SSL *ssl, OPENSSL_timeval *out_clock) {
-  memset(out_clock, 0, sizeof(*out_clock));
+  *out_clock = *GetClockPtr(ssl);
 }
 
 static SSL_CTX *setup_ctx(const TestConfig *config) {
@@ -297,11 +306,29 @@
   return NULL;
 }
 
-static int retry_async(SSL *ssl, int ret, BIO *bio) {
+static int retry_async(SSL *ssl, int ret, BIO *bio,
+                       OPENSSL_timeval *clock_delta) {
   // No error; don't retry.
   if (ret >= 0) {
     return 0;
   }
+
+  if (clock_delta->tv_usec != 0 || clock_delta->tv_sec != 0) {
+    // Process the timeout and retry.
+    OPENSSL_timeval *clock = GetClockPtr(ssl);
+    clock->tv_usec += clock_delta->tv_usec;
+    clock->tv_sec += clock->tv_usec / 1000000;
+    clock->tv_usec %= 1000000;
+    clock->tv_sec += clock_delta->tv_sec;
+    memset(clock_delta, 0, sizeof(*clock_delta));
+
+    if (DTLSv1_handle_timeout(ssl) < 0) {
+      printf("Error retransmitting.\n");
+      return 0;
+    }
+    return 1;
+  }
+
   // See if we needed to read or write more. If so, allow one byte through on
   // the appropriate end to maximally stress the state machine.
   int err = SSL_get_error(ssl, ret);
@@ -323,13 +350,15 @@
                        SSL_SESSION *session) {
   early_callback_called = 0;
 
+  OPENSSL_timeval clock = {0}, clock_delta = {0};
   SSL *ssl = SSL_new(ssl_ctx);
   if (ssl == NULL) {
     BIO_print_errors_fp(stdout);
     return 1;
   }
 
-  if (!SetConfigPtr(ssl, config)) {
+  if (!SetConfigPtr(ssl, config) ||
+      !SetClockPtr(ssl, &clock)) {
     BIO_print_errors_fp(stdout);
     return 1;
   }
@@ -454,7 +483,7 @@
     return 1;
   }
   if (config->is_dtls) {
-    BIO *packeted = packeted_bio_create();
+    BIO *packeted = packeted_bio_create(&clock_delta);
     BIO_push(packeted, bio);
     bio = packeted;
   }
@@ -480,7 +509,7 @@
     } else {
       ret = SSL_connect(ssl);
     }
-  } while (config->async && retry_async(ssl, ret, bio));
+  } while (config->async && retry_async(ssl, ret, bio, &clock_delta));
   if (ret != 1) {
     SSL_free(ssl);
     BIO_print_errors_fp(stdout);
@@ -641,7 +670,7 @@
         if (w > 0) {
           off += (size_t) w;
         }
-      } while ((config->async && retry_async(ssl, w, bio)) ||
+      } while ((config->async && retry_async(ssl, w, bio, &clock_delta)) ||
                (w > 0 && off < len));
 
       if (w < 0 || off != len) {
@@ -655,14 +684,14 @@
       int w;
       do {
         w = SSL_write(ssl, "hello", 5);
-      } while (config->async && retry_async(ssl, w, bio));
+      } while (config->async && retry_async(ssl, w, bio, &clock_delta));
     }
     for (;;) {
       uint8_t buf[512];
       int n;
       do {
         n = SSL_read(ssl, buf, sizeof(buf));
-      } while (config->async && retry_async(ssl, n, bio));
+      } while (config->async && retry_async(ssl, n, bio, &clock_delta));
       int err = SSL_get_error(ssl, n);
       if (err == SSL_ERROR_ZERO_RETURN ||
           (n == 0 && err == SSL_ERROR_SYSCALL)) {
@@ -693,7 +722,7 @@
       int w;
       do {
         w = SSL_write(ssl, buf, n);
-      } while (config->async && retry_async(ssl, w, bio));
+      } while (config->async && retry_async(ssl, w, bio, &clock_delta));
       if (w != n) {
         SSL_free(ssl);
         BIO_print_errors_fp(stdout);
@@ -720,7 +749,8 @@
     return 1;
   }
   g_ex_data_index = SSL_get_ex_new_index(0, NULL, NULL, NULL, NULL);
-  if (g_ex_data_index < 0) {
+  g_ex_data_clock_index = SSL_get_ex_new_index(0, NULL, NULL, NULL, NULL);
+  if (g_ex_data_index < 0 || g_ex_data_clock_index < 0) {
     return 1;
   }
 
diff --git a/ssl/test/packeted_bio.cc b/ssl/test/packeted_bio.cc
index 93b2164..8f8b911 100644
--- a/ssl/test/packeted_bio.cc
+++ b/ssl/test/packeted_bio.cc
@@ -25,6 +25,10 @@
 
 extern const BIO_METHOD packeted_bio_method;
 
+const uint8_t kOpcodePacket = 'P';
+const uint8_t kOpcodeTimeout = 'T';
+const uint8_t kOpcodeTimeoutAck = 't';
+
 static int packeted_write(BIO *bio, const char *in, int inl) {
   if (bio->next_bio == NULL) {
     return 0;
@@ -32,13 +36,14 @@
 
   BIO_clear_retry_flags(bio);
 
-  // Write the length prefix.
-  uint8_t len_bytes[4];
-  len_bytes[0] = (inl >> 24) & 0xff;
-  len_bytes[1] = (inl >> 16) & 0xff;
-  len_bytes[2] = (inl >> 8) & 0xff;
-  len_bytes[3] = inl & 0xff;
-  int ret = BIO_write(bio->next_bio, len_bytes, sizeof(len_bytes));
+  // Write the header.
+  uint8_t header[5];
+  header[0] = kOpcodePacket;
+  header[1] = (inl >> 24) & 0xff;
+  header[2] = (inl >> 16) & 0xff;
+  header[3] = (inl >> 8) & 0xff;
+  header[4] = inl & 0xff;
+  int ret = BIO_write(bio->next_bio, header, sizeof(header));
   if (ret <= 0) {
     BIO_copy_next_retry(bio);
     return ret;
@@ -57,15 +62,64 @@
 
   BIO_clear_retry_flags(bio);
 
+  // Read the opcode.
+  uint8_t opcode;
+  int ret = BIO_read(bio->next_bio, &opcode, sizeof(opcode));
+  if (ret <= 0) {
+    BIO_copy_next_retry(bio);
+    return ret;
+  }
+  assert(static_cast<size_t>(ret) == sizeof(opcode));
+
+  if (opcode == kOpcodeTimeout) {
+    // Process the timeout.
+    uint8_t buf[8];
+    ret = BIO_read(bio->next_bio, &buf, sizeof(buf));
+    if (ret <= 0) {
+      BIO_copy_next_retry(bio);
+      return ret;
+    }
+    assert(static_cast<size_t>(ret) == sizeof(buf));
+    uint64_t timeout = (static_cast<uint64_t>(buf[0]) << 56) |
+        (static_cast<uint64_t>(buf[1]) << 48) |
+        (static_cast<uint64_t>(buf[2]) << 40) |
+        (static_cast<uint64_t>(buf[3]) << 32) |
+        (static_cast<uint64_t>(buf[4]) << 24) |
+        (static_cast<uint64_t>(buf[5]) << 16) |
+        (static_cast<uint64_t>(buf[6]) << 8) |
+        static_cast<uint64_t>(buf[7]);
+    timeout /= 1000;  // Convert nanoseconds to microseconds.
+    OPENSSL_timeval *out_timeout =
+        reinterpret_cast<OPENSSL_timeval *>(bio->ptr);
+    assert(out_timeout->tv_usec == 0);
+    assert(out_timeout->tv_sec == 0);
+    out_timeout->tv_usec = timeout % 1000000;
+    out_timeout->tv_sec = timeout / 1000000;
+
+    // Send an ACK to the peer.
+    ret = BIO_write(bio->next_bio, &kOpcodeTimeoutAck, 1);
+    assert(ret == 1);
+
+    // Signal to the caller to retry the read, after processing the
+    // new clock.
+    BIO_set_retry_read(bio);
+    return -1;
+  }
+
+  if (opcode != kOpcodePacket) {
+    fprintf(stderr, "Unknown opcode, %u\n", opcode);
+    return -1;
+  }
+
   // Read the length prefix.
   uint8_t len_bytes[4];
-  int ret = BIO_read(bio->next_bio, &len_bytes, sizeof(len_bytes));
+  ret = BIO_read(bio->next_bio, &len_bytes, sizeof(len_bytes));
   if (ret <= 0) {
     BIO_copy_next_retry(bio);
     return ret;
   }
   // BIOs for which a partial length comes back are not supported.
-  assert(ret == 4);
+  assert(static_cast<size_t>(ret) == sizeof(len_bytes));
 
   uint32_t len = (len_bytes[0] << 24) | (len_bytes[1] << 16) |
       (len_bytes[2] << 8) | len_bytes[3];
@@ -130,6 +184,8 @@
 
 }  // namespace
 
-BIO *packeted_bio_create() {
-  return BIO_new(&packeted_bio_method);
+BIO *packeted_bio_create(OPENSSL_timeval *out_timeout) {
+  BIO *bio = BIO_new(&packeted_bio_method);
+  bio->ptr = out_timeout;
+  return bio;
 }
diff --git a/ssl/test/packeted_bio.h b/ssl/test/packeted_bio.h
index 384bd64..4d75a7c 100644
--- a/ssl/test/packeted_bio.h
+++ b/ssl/test/packeted_bio.h
@@ -16,17 +16,19 @@
 #define HEADER_PACKETED_BIO
 
 #include <openssl/bio.h>
+#include <openssl/ssl.h>
 
 
-// packeted_bio_create creates a filter BIO for testing protocols which expect
-// datagram BIOs. It implements a reliable datagram socket and reads and writes
-// packets by prefixing each packet with a big-endian 32-bit length. It must be
-// layered over a reliable blocking stream BIO.
+// packeted_bio_create creates a filter BIO which implements a reliable in-order
+// blocking datagram socket. The resulting BIO, on |BIO_read|, may simulate a
+// timeout which sets |*out_timeout| to the timeout and fails the read.
+// |*out_timeout| must be zero on entry to |BIO_read|; it is an error to not
+// apply the timeout before the next |BIO_read|.
 //
-// Note: packeted_bio_create exists because a SOCK_DGRAM socketpair on OS X is
-// does not block the caller, unlike on Linux. Writes simply fail with
-// ENOBUFS. POSIX also does not guarantee that such sockets are reliable.
-BIO *packeted_bio_create();
+// Note: The read timeout simulation is intended to be used with the async BIO
+// wrapper. It doesn't simulate BIO_CTRL_DGRAM_SET_NEXT_TIMEOUT, used in DTLS's
+// blocking mode.
+BIO *packeted_bio_create(OPENSSL_timeval *out_timeout);
 
 
 #endif  // HEADER_PACKETED_BIO
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 7aaf9a2..99b0c04 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -603,6 +603,13 @@
 	// AppDataAfterChangeCipherSpec, if not null, causes application data to
 	// be sent immediately after ChangeCipherSpec.
 	AppDataAfterChangeCipherSpec []byte
+
+	// TimeoutSchedule is the schedule of packet drops and simulated
+	// timeouts for before each handshake leg from the peer.
+	TimeoutSchedule []time.Duration
+
+	// PacketAdaptor is the packetAdaptor to use to simulate timeouts.
+	PacketAdaptor *packetAdaptor
 }
 
 func (c *Config) serverInit() {
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index 1c64c6a..307f61d 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -131,6 +131,7 @@
 
 	nextCipher interface{} // next encryption state
 	nextMac    macFunction // next MAC algorithm
+	nextSeq    [6]byte     // next epoch's starting sequence number in DTLS
 
 	// used to save allocating a new buffer for each MAC.
 	inDigestBuf, outDigestBuf []byte
@@ -200,10 +201,20 @@
 	}
 }
 
-// incEpoch resets the sequence number. In DTLS, it increments the
-// epoch half of the sequence number.
+// incNextSeq increments the starting sequence number for the next epoch.
+func (hc *halfConn) incNextSeq() {
+	for i := len(hc.nextSeq) - 1; i >= 0; i-- {
+		hc.nextSeq[i]++
+		if hc.nextSeq[i] != 0 {
+			return
+		}
+	}
+	panic("TLS: sequence number wraparound")
+}
+
+// incEpoch resets the sequence number. In DTLS, it also increments the epoch
+// half of the sequence number.
 func (hc *halfConn) incEpoch() {
-	limit := 0
 	if hc.isDTLS {
 		for i := 1; i >= 0; i-- {
 			hc.seq[i]++
@@ -214,11 +225,14 @@
 				panic("TLS: epoch number wraparound")
 			}
 		}
-		limit = 2
-	}
-	seq := hc.seq[limit:]
-	for i := range seq {
-		seq[i] = 0
+		copy(hc.seq[2:], hc.nextSeq[:])
+		for i := range hc.nextSeq {
+			hc.nextSeq[i] = 0
+		}
+	} else {
+		for i := range hc.seq {
+			hc.seq[i] = 0
+		}
 	}
 }
 
@@ -1000,6 +1014,67 @@
 	return m, nil
 }
 
+// skipPacket processes all the DTLS records in packet. It updates
+// sequence number expectations but otherwise ignores them.
+func (c *Conn) skipPacket(packet []byte) error {
+	for len(packet) > 0 {
+		// Dropped packets are completely ignored save to update
+		// expected sequence numbers for this and the next epoch. (We
+		// don't assert on the contents of the packets both for
+		// simplicity and because a previous test with one shorter
+		// timeout schedule would have done so.)
+		epoch := packet[3:5]
+		seq := packet[5:11]
+		length := uint16(packet[11])<<8 | uint16(packet[12])
+		if bytes.Equal(c.in.seq[:2], epoch) {
+			if !bytes.Equal(c.in.seq[2:], seq) {
+				return errors.New("tls: sequence mismatch")
+			}
+			c.in.incSeq(false)
+		} else {
+			if !bytes.Equal(c.in.nextSeq[:], seq) {
+				return errors.New("tls: sequence mismatch")
+			}
+			c.in.incNextSeq()
+		}
+		packet = packet[13+length:]
+	}
+	return nil
+}
+
+// simulatePacketLoss simulates the loss of a handshake leg from the
+// peer based on the schedule in c.config.Bugs. If resendFunc is
+// non-nil, it is called after each simulated timeout to retransmit
+// handshake messages from the local end. This is used in cases where
+// the peer retransmits on a stale Finished rather than a timeout.
+func (c *Conn) simulatePacketLoss(resendFunc func()) error {
+	if len(c.config.Bugs.TimeoutSchedule) == 0 {
+		return nil
+	}
+	if !c.isDTLS {
+		return errors.New("tls: TimeoutSchedule may only be set in DTLS")
+	}
+	if c.config.Bugs.PacketAdaptor == nil {
+		return errors.New("tls: TimeoutSchedule set without PacketAdapter")
+	}
+	for _, timeout := range c.config.Bugs.TimeoutSchedule {
+		// Simulate a timeout.
+		packets, err := c.config.Bugs.PacketAdaptor.SendReadTimeout(timeout)
+		if err != nil {
+			return err
+		}
+		for _, packet := range packets {
+			if err := c.skipPacket(packet); err != nil {
+				return err
+			}
+		}
+		if resendFunc != nil {
+			resendFunc()
+		}
+	}
+	return nil
+}
+
 // Write writes data to the connection.
 func (c *Conn) Write(b []byte) (int, error) {
 	if err := c.Handshake(); err != nil {
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index f297fc1..72a82f8 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -22,13 +22,14 @@
 )
 
 type clientHandshakeState struct {
-	c            *Conn
-	serverHello  *serverHelloMsg
-	hello        *clientHelloMsg
-	suite        *cipherSuite
-	finishedHash finishedHash
-	masterSecret []byte
-	session      *ClientSessionState
+	c             *Conn
+	serverHello   *serverHelloMsg
+	hello         *clientHelloMsg
+	suite         *cipherSuite
+	finishedHash  finishedHash
+	masterSecret  []byte
+	session       *ClientSessionState
+	finishedBytes []byte
 }
 
 func (c *Conn) clientHandshake() error {
@@ -214,6 +215,9 @@
 		c.writeRecord(recordTypeHandshake, helloBytes)
 	}
 
+	if err := c.simulatePacketLoss(nil); err != nil {
+		return err
+	}
 	msg, err := c.readHandshake()
 	if err != nil {
 		return err
@@ -234,6 +238,9 @@
 			helloBytes = hello.marshal()
 			c.writeRecord(recordTypeHandshake, helloBytes)
 
+			if err := c.simulatePacketLoss(nil); err != nil {
+				return err
+			}
 			msg, err = c.readHandshake()
 			if err != nil {
 				return err
@@ -317,6 +324,12 @@
 		if err := hs.sendFinished(isResume); err != nil {
 			return err
 		}
+		// Most retransmits are triggered by a timeout, but the final
+		// leg of the handshake is retransmited upon re-receiving a
+		// Finished.
+		if err := c.simulatePacketLoss(func() { c.writeRecord(recordTypeHandshake, hs.finishedBytes) }); err != nil {
+			return err
+		}
 		if err := hs.readSessionTicket(); err != nil {
 			return err
 		}
@@ -826,9 +839,9 @@
 		finished.verifyData = hs.finishedHash.clientSum(hs.masterSecret)
 	}
 	c.clientVerify = append(c.clientVerify[:0], finished.verifyData...)
-	finishedBytes := finished.marshal()
-	hs.writeHash(finishedBytes, seqno)
-	postCCSBytes = append(postCCSBytes, finishedBytes...)
+	hs.finishedBytes = finished.marshal()
+	hs.writeHash(hs.finishedBytes, seqno)
+	postCCSBytes = append(postCCSBytes, hs.finishedBytes...)
 
 	if c.config.Bugs.FragmentAcrossChangeCipherSpec {
 		c.writeRecord(recordTypeHandshake, postCCSBytes[:5])
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index 1234a57..af521fc 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -33,6 +33,7 @@
 	masterSecret    []byte
 	certsFromClient [][]byte
 	cert            *Certificate
+	finishedBytes   []byte
 }
 
 // serverHandshake performs a TLS handshake as a server.
@@ -71,6 +72,12 @@
 		if err := hs.sendFinished(); err != nil {
 			return err
 		}
+		// Most retransmits are triggered by a timeout, but the final
+		// leg of the handshake is retransmited upon re-receiving a
+		// Finished.
+		if err := c.simulatePacketLoss(func() { c.writeRecord(recordTypeHandshake, hs.finishedBytes) }); err != nil {
+			return err
+		}
 		if err := hs.readFinished(isResume); err != nil {
 			return err
 		}
@@ -110,6 +117,9 @@
 	config := hs.c.config
 	c := hs.c
 
+	if err := c.simulatePacketLoss(nil); err != nil {
+		return false, err
+	}
 	msg, err := c.readHandshake()
 	if err != nil {
 		return false, err
@@ -137,6 +147,9 @@
 		}
 		c.writeRecord(recordTypeHandshake, helloVerifyRequest.marshal())
 
+		if err := c.simulatePacketLoss(nil); err != nil {
+			return false, err
+		}
 		msg, err := c.readHandshake()
 		if err != nil {
 			return false, err
@@ -533,6 +546,9 @@
 
 	var pub crypto.PublicKey // public key for client auth, if any
 
+	if err := c.simulatePacketLoss(nil); err != nil {
+		return err
+	}
 	msg, err := c.readHandshake()
 	if err != nil {
 		return err
@@ -812,8 +828,9 @@
 	finished := new(finishedMsg)
 	finished.verifyData = hs.finishedHash.serverSum(hs.masterSecret)
 	c.serverVerify = append(c.serverVerify[:0], finished.verifyData...)
-	postCCSBytes := finished.marshal()
-	hs.writeServerHash(postCCSBytes)
+	hs.finishedBytes = finished.marshal()
+	hs.writeServerHash(hs.finishedBytes)
+	postCCSBytes := hs.finishedBytes
 
 	if c.config.Bugs.FragmentAcrossChangeCipherSpec {
 		c.writeRecord(recordTypeHandshake, postCCSBytes[:5])
diff --git a/ssl/test/runner/packet_adapter.go b/ssl/test/runner/packet_adapter.go
index 671b413..6a0ed53 100644
--- a/ssl/test/runner/packet_adapter.go
+++ b/ssl/test/runner/packet_adapter.go
@@ -6,52 +6,117 @@
 
 import (
 	"encoding/binary"
-	"errors"
+	"fmt"
+	"io"
 	"net"
+	"time"
 )
 
+// opcodePacket signals a packet, encoded with a 32-bit length prefix, followed
+// by the payload.
+const opcodePacket = byte('P')
+
+// opcodeTimeout signals a read timeout, encoded by a 64-bit number of
+// nanoseconds. On receipt, the peer should reply with
+// opcodeTimeoutAck. opcodeTimeout may only be sent by the Go side.
+const opcodeTimeout = byte('T')
+
+// opcodeTimeoutAck acknowledges a read timeout. This opcode has no payload and
+// may only be sent by the C side. Timeout ACKs act as a synchronization point
+// at the timeout, to bracket one flight of messages from C.
+const opcodeTimeoutAck = byte('t')
+
 type packetAdaptor struct {
 	net.Conn
 }
 
-// newPacketAdaptor wraps a reliable streaming net.Conn into a
-// reliable packet-based net.Conn. Every packet is encoded with a
-// 32-bit length prefix as a framing layer.
-func newPacketAdaptor(conn net.Conn) net.Conn {
+// newPacketAdaptor wraps a reliable streaming net.Conn into a reliable
+// packet-based net.Conn. The stream contains packets and control commands,
+// distinguished by a one byte opcode.
+func newPacketAdaptor(conn net.Conn) *packetAdaptor {
 	return &packetAdaptor{conn}
 }
 
-func (p *packetAdaptor) Read(b []byte) (int, error) {
-	var length uint32
-	if err := binary.Read(p.Conn, binary.BigEndian, &length); err != nil {
+func (p *packetAdaptor) readOpcode() (byte, error) {
+	out := make([]byte, 1)
+	if _, err := io.ReadFull(p.Conn, out); err != nil {
 		return 0, err
 	}
+	return out[0], nil
+}
+
+func (p *packetAdaptor) readPacketBody() ([]byte, error) {
+	var length uint32
+	if err := binary.Read(p.Conn, binary.BigEndian, &length); err != nil {
+		return nil, err
+	}
 	out := make([]byte, length)
-	n, err := p.Conn.Read(out)
+	if _, err := io.ReadFull(p.Conn, out); err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (p *packetAdaptor) Read(b []byte) (int, error) {
+	opcode, err := p.readOpcode()
 	if err != nil {
 		return 0, err
 	}
-	if n != int(length) {
-		return 0, errors.New("internal error: length mismatch!")
+	if opcode != opcodePacket {
+		return 0, fmt.Errorf("unexpected opcode '%s'", opcode)
+	}
+	out, err := p.readPacketBody()
+	if err != nil {
+		return 0, err
 	}
 	return copy(b, out), nil
 }
 
 func (p *packetAdaptor) Write(b []byte) (int, error) {
-	length := uint32(len(b))
-	if err := binary.Write(p.Conn, binary.BigEndian, length); err != nil {
+	payload := make([]byte, 1+4+len(b))
+	payload[0] = opcodePacket
+	binary.BigEndian.PutUint32(payload[1:5], uint32(len(b)))
+	copy(payload[5:], b)
+	if _, err := p.Conn.Write(payload); err != nil {
 		return 0, err
 	}
-	n, err := p.Conn.Write(b)
-	if err != nil {
-		return 0, err
-	}
-	if n != len(b) {
-		return 0, errors.New("internal error: length mismatch!")
-	}
 	return len(b), nil
 }
 
+// SendReadTimeout instructs the peer to simulate a read timeout. It then waits
+// for acknowledgement of the timeout, buffering any packets received since
+// then. The packets are then returned.
+func (p *packetAdaptor) SendReadTimeout(d time.Duration) ([][]byte, error) {
+	payload := make([]byte, 1+8)
+	payload[0] = opcodeTimeout
+	binary.BigEndian.PutUint64(payload[1:], uint64(d.Nanoseconds()))
+	if _, err := p.Conn.Write(payload); err != nil {
+		return nil, err
+	}
+
+	packets := make([][]byte, 0)
+	for {
+		opcode, err := p.readOpcode()
+		if err != nil {
+			return nil, err
+		}
+		switch opcode {
+		case opcodeTimeoutAck:
+			// Done! Return the packets buffered and continue.
+			return packets, nil
+		case opcodePacket:
+			// Buffer the packet for the caller to process.
+			packet, err := p.readPacketBody()
+			if err != nil {
+				return nil, err
+			}
+			packets = append(packets, packet)
+		default:
+			return nil, fmt.Errorf("unexpected opcode '%s'", opcode)
+		}
+	}
+}
+
 type replayAdaptor struct {
 	net.Conn
 	prevWrite []byte
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 137a87c..7fda082 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -20,6 +20,7 @@
 	"strings"
 	"sync"
 	"syscall"
+	"time"
 )
 
 var (
@@ -665,7 +666,8 @@
 	}
 
 	if test.protocol == dtls {
-		conn = newPacketAdaptor(conn)
+		config.Bugs.PacketAdaptor = newPacketAdaptor(conn)
+		conn = config.Bugs.PacketAdaptor
 		if test.replayWrites {
 			conn = newReplayAdaptor(conn)
 		}
@@ -2536,6 +2538,87 @@
 	})
 }
 
+// timeouts is the retransmit schedule for BoringSSL. It doubles and
+// caps at 60 seconds. On the 13th timeout, it gives up.
+var timeouts = []time.Duration{
+	1 * time.Second,
+	2 * time.Second,
+	4 * time.Second,
+	8 * time.Second,
+	16 * time.Second,
+	32 * time.Second,
+	60 * time.Second,
+	60 * time.Second,
+	60 * time.Second,
+	60 * time.Second,
+	60 * time.Second,
+	60 * time.Second,
+	60 * time.Second,
+}
+
+func addDTLSRetransmitTests() {
+	// Test that this is indeed the timeout schedule. Stress all
+	// four patterns of handshake.
+	for i := 1; i < len(timeouts); i++ {
+		number := strconv.Itoa(i)
+		testCases = append(testCases, testCase{
+			protocol: dtls,
+			name:     "DTLS-Retransmit-Client-" + number,
+			config: Config{
+				Bugs: ProtocolBugs{
+					TimeoutSchedule: timeouts[:i],
+				},
+			},
+			resumeSession: true,
+			flags:         []string{"-async"},
+		})
+		testCases = append(testCases, testCase{
+			protocol: dtls,
+			testType: serverTest,
+			name:     "DTLS-Retransmit-Server-" + number,
+			config: Config{
+				Bugs: ProtocolBugs{
+					TimeoutSchedule: timeouts[:i],
+				},
+			},
+			resumeSession: true,
+			flags:         []string{"-async"},
+		})
+	}
+
+	// Test that exceeding the timeout schedule hits a read
+	// timeout.
+	testCases = append(testCases, testCase{
+		protocol: dtls,
+		name:     "DTLS-Retransmit-Timeout",
+		config: Config{
+			Bugs: ProtocolBugs{
+				TimeoutSchedule: timeouts,
+			},
+		},
+		resumeSession: true,
+		flags:         []string{"-async"},
+		shouldFail:    true,
+		expectedError: ":READ_TIMEOUT_EXPIRED:",
+	})
+
+	// Test that timeout handling has a fudge factor, due to API
+	// problems.
+	testCases = append(testCases, testCase{
+		protocol: dtls,
+		name:     "DTLS-Retransmit-Fudge",
+		config: Config{
+			Bugs: ProtocolBugs{
+				TimeoutSchedule: []time.Duration{
+					timeouts[0] - 10*time.Millisecond,
+				},
+			},
+		},
+		resumeSession: true,
+		flags:         []string{"-async"},
+	})
+}
+
 func worker(statusChan chan statusMsg, c chan *testCase, buildDir string, wg *sync.WaitGroup) {
 	defer wg.Done()
 
@@ -2611,6 +2694,7 @@
 	addDTLSReplayTests()
 	addSigningHashTests()
 	addFastRadioPaddingTests()
+	addDTLSRetransmitTests()
 	for _, async := range []bool{false, true} {
 		for _, splitHandshake := range []bool{false, true} {
 			for _, protocol := range []protocol{tls, dtls} {