Add DTLS replay tests.

At the record layer, DTLS maintains a window of seen sequence numbers to detect
replays. Add tests to cover that case. Test both repeated sequence numbers
within the window and sequence numbers past the window's left edge. Also test
receiving sequence numbers far past the window's right edge.

Change-Id: If6a7a24869db37fdd8fb3c4b3521b730e31f8f86
Reviewed-on: https://boringssl-review.googlesource.com/2221
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 6f146af..8cdbaea 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -499,6 +499,12 @@
 	// BadRenegotiationInfo causes the renegotiation extension value in a
 	// renegotiation handshake to be incorrect.
 	BadRenegotiationInfo bool
+
+	// SequenceNumberIncrement, if non-zero, causes outgoing sequence
+	// numbers in DTLS to increment by that value rather by 1. This is to
+	// stress the replay bitmap window by simulating extreme packet loss and
+	// retransmit at the record layer.
+	SequenceNumberIncrement uint64
 }
 
 func (c *Config) serverInit() {
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index e76f9d1..e1ccbb7 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -71,6 +71,13 @@
 	tmp [16]byte
 }
 
+func (c *Conn) init() {
+	c.in.isDTLS = c.isDTLS
+	c.out.isDTLS = c.isDTLS
+	c.in.config = c.config
+	c.out.config = c.config
+}
+
 // Access to net.Conn methods.
 // Cannot just embed net.Conn because that would
 // export the struct field too.
@@ -164,23 +171,29 @@
 }
 
 // incSeq increments the sequence number.
-func (hc *halfConn) incSeq() {
+func (hc *halfConn) incSeq(isOutgoing bool) {
 	limit := 0
+	increment := uint64(1)
 	if hc.isDTLS {
 		// Increment up to the epoch in DTLS.
 		limit = 2
+
+		if isOutgoing && hc.config.Bugs.SequenceNumberIncrement != 0 {
+			increment = hc.config.Bugs.SequenceNumberIncrement
+		}
 	}
 	for i := 7; i >= limit; i-- {
-		hc.seq[i]++
-		if hc.seq[i] != 0 {
-			return
-		}
+		increment += uint64(hc.seq[i])
+		hc.seq[i] = byte(increment)
+		increment >>= 8
 	}
 
 	// Not allowed to let sequence number wrap.
 	// Instead, must renegotiate before it does.
 	// Not likely enough to bother.
-	panic("TLS: sequence number wraparound")
+	if increment != 0 {
+		panic("TLS: sequence number wraparound")
+	}
 }
 
 // incEpoch resets the sequence number. In DTLS, it increments the
@@ -380,7 +393,7 @@
 		}
 		hc.inDigestBuf = localMAC
 	}
-	hc.incSeq()
+	hc.incSeq(false)
 
 	return true, recordHeaderLen + explicitIVLen, 0
 }
@@ -467,7 +480,7 @@
 	n := len(b.data) - recordHeaderLen
 	b.data[recordHeaderLen-2] = byte(n >> 8)
 	b.data[recordHeaderLen-1] = byte(n)
-	hc.incSeq()
+	hc.incSeq(true)
 
 	return true, 0
 }
diff --git a/ssl/test/runner/dtls.go b/ssl/test/runner/dtls.go
index 3b3f5a2..9c48b19 100644
--- a/ssl/test/runner/dtls.go
+++ b/ssl/test/runner/dtls.go
@@ -301,13 +301,9 @@
 // The configuration config must be non-nil and must have
 // at least one certificate.
 func DTLSServer(conn net.Conn, config *Config) *Conn {
-	return &Conn{
-		config: config,
-		isDTLS: true,
-		conn:   conn,
-		in:     halfConn{isDTLS: true},
-		out:    halfConn{isDTLS: true},
-	}
+	c := &Conn{config: config, isDTLS: true, conn: conn}
+	c.init()
+	return c
 }
 
 // DTLSClient returns a new DTLS client side connection
@@ -315,12 +311,7 @@
 // The config cannot be nil: users must set either ServerHostname or
 // InsecureSkipVerify in the config.
 func DTLSClient(conn net.Conn, config *Config) *Conn {
-	return &Conn{
-		config:   config,
-		isClient: true,
-		isDTLS:   true,
-		conn:     conn,
-		in:       halfConn{isDTLS: true},
-		out:      halfConn{isDTLS: true},
-	}
+	c := &Conn{config: config, isClient: true, isDTLS: true, conn: conn}
+	c.init()
+	return c
 }
diff --git a/ssl/test/runner/packet_adapter.go b/ssl/test/runner/packet_adapter.go
index b2f2765..4f36eb1 100644
--- a/ssl/test/runner/packet_adapter.go
+++ b/ssl/test/runner/packet_adapter.go
@@ -14,6 +14,9 @@
 	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 {
 	return &packetAdaptor{conn}
 }
@@ -48,3 +51,28 @@
 	}
 	return len(b), nil
 }
+
+type replayAdaptor struct {
+	net.Conn
+	prevWrite []byte
+}
+
+// newReplayAdaptor wraps a packeted net.Conn. It transforms it into
+// one which, after writing a packet, always replays the previous
+// write.
+func newReplayAdaptor(conn net.Conn) net.Conn {
+	return &replayAdaptor{Conn: conn}
+}
+
+func (r *replayAdaptor) Write(b []byte) (int, error) {
+	n, err := r.Conn.Write(b)
+
+	// Replay the previous packet and save the current one to
+	// replay next.
+	if r.prevWrite != nil {
+		r.Conn.Write(r.prevWrite)
+	}
+	r.prevWrite = append(r.prevWrite[:0], b...)
+
+	return n, err
+}
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 4b43481..44e15d1 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -156,6 +156,9 @@
 	// renegotiateCiphers is a list of ciphersuite ids that will be
 	// switched in just before renegotiation.
 	renegotiateCiphers []uint16
+	// replayWrites, if true, configures the underlying transport
+	// to replay every write it makes in DTLS tests.
+	replayWrites bool
 	// flags, if not empty, contains a list of command-line flags that will
 	// be passed to the shim program.
 	flags []string
@@ -497,6 +500,9 @@
 func doExchange(test *testCase, config *Config, conn net.Conn, messageLen int, isResume bool) error {
 	if test.protocol == dtls {
 		conn = newPacketAdaptor(conn)
+		if test.replayWrites {
+			conn = newReplayAdaptor(conn)
+		}
 	}
 
 	if test.sendPrefix != "" {
@@ -1854,6 +1860,28 @@
 	})
 }
 
+func addDTLSReplayTests() {
+	// Test that sequence number replays are detected.
+	testCases = append(testCases, testCase{
+		protocol:     dtls,
+		name:         "DTLS-Replay",
+		replayWrites: true,
+	})
+
+	// Test the outgoing sequence number skipping by values larger
+	// than the retransmit window.
+	testCases = append(testCases, testCase{
+		protocol: dtls,
+		name:     "DTLS-Replay-LargeGaps",
+		config: Config{
+			Bugs: ProtocolBugs{
+				SequenceNumberIncrement: 127,
+			},
+		},
+		replayWrites: true,
+	})
+}
+
 func worker(statusChan chan statusMsg, c chan *testCase, buildDir string, wg *sync.WaitGroup) {
 	defer wg.Done()
 
@@ -1911,6 +1939,7 @@
 	addResumptionVersionTests()
 	addExtendedMasterSecretTests()
 	addRenegotiationTests()
+	addDTLSReplayTests()
 	for _, async := range []bool{false, true} {
 		for _, splitHandshake := range []bool{false, true} {
 			for _, protocol := range []protocol{tls, dtls} {
diff --git a/ssl/test/runner/tls.go b/ssl/test/runner/tls.go
index c062a41..6b637c8 100644
--- a/ssl/test/runner/tls.go
+++ b/ssl/test/runner/tls.go
@@ -23,7 +23,9 @@
 // The configuration config must be non-nil and must have
 // at least one certificate.
 func Server(conn net.Conn, config *Config) *Conn {
-	return &Conn{conn: conn, config: config}
+	c := &Conn{conn: conn, config: config}
+	c.init()
+	return c
 }
 
 // Client returns a new TLS client side connection
@@ -31,7 +33,9 @@
 // The config cannot be nil: users must set either ServerHostname or
 // InsecureSkipVerify in the config.
 func Client(conn net.Conn, config *Config) *Conn {
-	return &Conn{conn: conn, config: config, isClient: true}
+	c := &Conn{conn: conn, config: config, isClient: true}
+	c.init()
+	return c
 }
 
 // A listener implements a network listener (net.Listener) for TLS connections.