83f9040339
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>
167 lines
4.2 KiB
Go
167 lines
4.2 KiB
Go
// Copyright 2014 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"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. 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) 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)
|
|
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 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) {
|
|
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
|
|
}
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
type damageAdaptor struct {
|
|
net.Conn
|
|
damage bool
|
|
}
|
|
|
|
// newDamageAdaptor wraps a packeted net.Conn. It transforms it into one which
|
|
// optionally damages the final byte of every Write() call.
|
|
func newDamageAdaptor(conn net.Conn) *damageAdaptor {
|
|
return &damageAdaptor{Conn: conn}
|
|
}
|
|
|
|
func (d *damageAdaptor) setDamage(damage bool) {
|
|
d.damage = damage
|
|
}
|
|
|
|
func (d *damageAdaptor) Write(b []byte) (int, error) {
|
|
if d.damage && len(b) > 0 {
|
|
b = append([]byte{}, b...)
|
|
b[len(b)-1]++
|
|
}
|
|
return d.Conn.Write(b)
|
|
}
|