Skip to content

Commit

Permalink
quic: send and receive datagrams
Browse files Browse the repository at this point in the history
Add the ability for Conns to send and receive datagrams.
No socket handling yet; this only functions in tests for now.

Extend testConn to permit tests to send packets to Conns and
observe the packets Conns send.

There's a circular dependency here: We can't test Handshake and 1-RTT
packets until we have the handshake implemented, but we can't
implement the handshake without the ability to send and receive
Handshake and 1-RTT packets. This CL adds the ability to send and
receive those packets; tests for those paths will follow with the
handshake implementation.

For golang/go#58547

Change-Id: I4e7f88f5f039baf7e01f68a53639022866786af9
Reviewed-on: https://go-review.googlesource.com/c/net/+/509017
Run-TryBot: Damien Neil <[email protected]>
Reviewed-by: Jonathan Amsterdam <[email protected]>
TryBot-Result: Gopher Robot <[email protected]>
  • Loading branch information
neild committed Jul 18, 2023
1 parent 16cc77a commit 0adcadf
Show file tree
Hide file tree
Showing 16 changed files with 1,184 additions and 17 deletions.
90 changes: 83 additions & 7 deletions internal/quic/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,87 @@ package quic
import (
"errors"
"fmt"
"net/netip"
"time"
)

// A Conn is a QUIC connection.
//
// Multiple goroutines may invoke methods on a Conn simultaneously.
type Conn struct {
side connSide
listener connListener
testHooks connTestHooks
peerAddr netip.AddrPort

msgc chan any
donec chan struct{} // closed when conn loop exits
exited bool // set to make the conn loop exit immediately

testHooks connTestHooks
w packetWriter
acks [numberSpaceCount]ackState // indexed by number space
connIDState connIDState
tlsState tlsState
loss lossState

// idleTimeout is the time at which the connection will be closed due to inactivity.
// https://www.rfc-editor.org/rfc/rfc9000#section-10.1
maxIdleTimeout time.Duration
idleTimeout time.Time

peerAckDelayExponent int8 // -1 when unknown

// Tests only: Send a PING in a specific number space.
testSendPingSpace numberSpace
testSendPing sentVal
}

// The connListener is the Conn's Listener.
// Defined as an interface so we can swap it out in tests.
type connListener interface {
sendDatagram(p []byte, addr netip.AddrPort) error
}

// connTestHooks override conn behavior in tests.
type connTestHooks interface {
nextMessage(msgc chan any, nextTimeout time.Time) (now time.Time, message any)
}

func newConn(now time.Time, hooks connTestHooks) (*Conn, error) {
func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, l connListener, hooks connTestHooks) (*Conn, error) {
c := &Conn{
donec: make(chan struct{}),
testHooks: hooks,
maxIdleTimeout: defaultMaxIdleTimeout,
idleTimeout: now.Add(defaultMaxIdleTimeout),
side: side,
listener: l,
peerAddr: peerAddr,
msgc: make(chan any, 1),
donec: make(chan struct{}),
testHooks: hooks,
maxIdleTimeout: defaultMaxIdleTimeout,
idleTimeout: now.Add(defaultMaxIdleTimeout),
peerAckDelayExponent: -1,
}

// A one-element buffer allows us to wake a Conn's event loop as a
// non-blocking operation.
c.msgc = make(chan any, 1)

if c.side == clientSide {
if err := c.connIDState.initClient(newRandomConnID); err != nil {
return nil, err
}
initialConnID = c.connIDState.dstConnID()
} else {
if err := c.connIDState.initServer(newRandomConnID, initialConnID); err != nil {
return nil, err
}
}

// The smallest allowed maximum QUIC datagram size is 1200 bytes.
// TODO: PMTU discovery.
const maxDatagramSize = 1200
c.loss.init(c.side, maxDatagramSize, now)

c.tlsState.init(c.side, initialConnID)

go c.loop(now)
return c, nil
}
Expand Down Expand Up @@ -76,7 +121,14 @@ func (c *Conn) loop(now time.Time) {
}

for !c.exited {
nextTimeout := c.idleTimeout
sendTimeout := c.maybeSend(now) // try sending

// Note that we only need to consider the ack timer for the App Data space,
// since the Initial and Handshake spaces always ack immediately.
nextTimeout := sendTimeout
nextTimeout = firstTime(nextTimeout, c.idleTimeout)
nextTimeout = firstTime(nextTimeout, c.loss.timer)
nextTimeout = firstTime(nextTimeout, c.acks[appDataSpace].nextAck)

var m any
if hooks != nil {
Expand All @@ -100,6 +152,9 @@ func (c *Conn) loop(now time.Time) {
now = time.Now()
}
switch m := m.(type) {
case *datagram:
c.handleDatagram(now, m)
m.recycle()
case timerEvent:
// A connection timer has expired.
if !now.Before(c.idleTimeout) {
Expand All @@ -109,6 +164,7 @@ func (c *Conn) loop(now time.Time) {
c.exited = true
return
}
c.loss.advance(now, c.handleAckOrLoss)
case func(time.Time, *Conn):
// Send a func to msgc to run it on the main Conn goroutine
m(now, c)
Expand Down Expand Up @@ -146,10 +202,30 @@ func (c *Conn) runOnLoop(f func(now time.Time, c *Conn)) error {
return nil
}

// abort terminates a connection with an error.
func (c *Conn) abort(now time.Time, err error) {
// TODO: Send CONNECTION_CLOSE frames.
c.exit()
}

// exit fully terminates a connection immediately.
func (c *Conn) exit() {
c.runOnLoop(func(now time.Time, c *Conn) {
c.exited = true
})
<-c.donec
}

// firstTime returns the earliest non-zero time, or zero if both times are zero.
func firstTime(a, b time.Time) time.Time {
switch {
case a.IsZero():
return b
case b.IsZero():
return a
case a.Before(b):
return a
default:
return b
}
}
46 changes: 46 additions & 0 deletions internal/quic/conn_loss.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2023 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.

//go:build go1.21

package quic

import "fmt"

// handleAckOrLoss deals with the final fate of a packet we sent:
// Either the peer acknowledges it, or we declare it lost.
//
// In order to handle packet loss, we must retain any information sent to the peer
// until the peer has acknowledged it.
//
// When information is acknowledged, we can discard it.
//
// When information is lost, we mark it for retransmission.
// See RFC 9000, Section 13.3 for a complete list of information which is retransmitted on loss.
// https://www.rfc-editor.org/rfc/rfc9000#section-13.3
func (c *Conn) handleAckOrLoss(space numberSpace, sent *sentPacket, fate packetFate) {
// The list of frames in a sent packet is marshaled into a buffer in the sentPacket
// by the packetWriter. Unmarshal that buffer here. This code must be kept in sync with
// packetWriter.append*.
//
// A sent packet meets its fate (acked or lost) only once, so it's okay to consume
// the sentPacket's buffer here.
for !sent.done() {
switch f := sent.next(); f {
default:
panic(fmt.Sprintf("BUG: unhandled lost frame type %x", f))
case frameTypeAck:
// Unlike most information, loss of an ACK frame does not trigger
// retransmission. ACKs are sent in response to ack-eliciting packets,
// and always contain the latest information available.
//
// Acknowledgement of an ACK frame may allow us to discard information
// about older packets.
largest := packetNumber(sent.nextInt())
if fate == packetAcked {
c.acks[space].handleAck(largest)
}
}
}
}
Loading

0 comments on commit 0adcadf

Please sign in to comment.