add Transport config options to limit the number of handshakes (#4248)

* add Transport config options to limit the number of handshakes

* fix accounting for failed handshakes

* increase handshake limits, improve documentation
This commit is contained in:
Marten Seemann 2024-01-22 21:04:25 -08:00 committed by GitHub
parent bda5b7e6dc
commit 892851eb8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 466 additions and 142 deletions

View file

@ -83,6 +83,25 @@ var _ = Describe("Server", func() {
return hdr
}
checkConnectionCloseError := func(b []byte, origHdr *wire.Header, errorCode qerr.TransportErrorCode) {
replyHdr := parseHeader(b)
Expect(replyHdr.Type).To(Equal(protocol.PacketTypeInitial))
Expect(replyHdr.SrcConnectionID).To(Equal(origHdr.DestConnectionID))
Expect(replyHdr.DestConnectionID).To(Equal(origHdr.SrcConnectionID))
_, opener := handshake.NewInitialAEAD(origHdr.DestConnectionID, protocol.PerspectiveClient, replyHdr.Version)
extHdr, err := unpackLongHeader(opener, replyHdr, b, origHdr.Version)
Expect(err).ToNot(HaveOccurred())
data, err := opener.Open(nil, b[extHdr.ParsedLen():], extHdr.PacketNumber, b[:extHdr.ParsedLen()])
Expect(err).ToNot(HaveOccurred())
_, f, err := wire.NewFrameParser(false).ParseNext(data, protocol.EncryptionInitial, origHdr.Version)
Expect(err).ToNot(HaveOccurred())
Expect(f).To(BeAssignableToTypeOf(&wire.ConnectionCloseFrame{}))
ccf := f.(*wire.ConnectionCloseFrame)
Expect(ccf.IsApplicationError).To(BeFalse())
Expect(ccf.ErrorCode).To(BeEquivalentTo(errorCode))
Expect(ccf.ReasonPhrase).To(BeEmpty())
}
BeforeEach(func() {
conn = NewMockPacketConn(mockCtrl)
conn.EXPECT().LocalAddr().Return(&net.UDPAddr{}).AnyTimes()
@ -534,6 +553,9 @@ var _ = Describe("Server", func() {
})
It("drops packets if the receive queue is full", func() {
serv.maxNumHandshakesTotal = 10000
serv.maxNumHandshakesUnvalidated = 10000
phm.EXPECT().Get(gomock.Any()).AnyTimes()
phm.EXPECT().AddWithConnID(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_, _ protocol.ConnectionID, fn func() (packetHandler, bool)) bool {
phm.EXPECT().GetStatelessResetToken(gomock.Any())
@ -542,7 +564,7 @@ var _ = Describe("Server", func() {
}).AnyTimes()
acceptConn := make(chan struct{})
var counter uint32 // to be used as an atomic, so we query it in Eventually
var counter atomic.Uint32
serv.newConn = func(
_ sendConn,
runner connRunner,
@ -563,7 +585,7 @@ var _ = Describe("Server", func() {
_ protocol.VersionNumber,
) quicConn {
<-acceptConn
atomic.AddUint32(&counter, 1)
counter.Add(1)
conn := NewMockQUICConn(mockCtrl)
conn.EXPECT().handlePacket(gomock.Any()).MaxTimes(1)
conn.EXPECT().run().MaxTimes(1)
@ -590,10 +612,10 @@ var _ = Describe("Server", func() {
close(acceptConn)
Eventually(
func() uint32 { return atomic.LoadUint32(&counter) },
func() uint32 { return counter.Load() },
scaleDuration(100*time.Millisecond),
).Should(BeEquivalentTo(protocol.MaxServerUnprocessedPackets + 1))
Consistently(func() uint32 { return atomic.LoadUint32(&counter) }).Should(BeEquivalentTo(protocol.MaxServerUnprocessedPackets + 1))
Consistently(func() uint32 { return counter.Load() }).Should(BeEquivalentTo(protocol.MaxServerUnprocessedPackets + 1))
})
It("only creates a single connection for a duplicate Initial", func() {
@ -633,7 +655,20 @@ var _ = Describe("Server", func() {
Eventually(done).Should(BeClosed())
})
It("rejects new connection attempts if the accept queue is full", func() {
It("limits the number of unvalidated handshakes", func() {
const limit = 3
serv.maxNumHandshakesTotal = 10000
serv.maxNumHandshakesUnvalidated = limit
phm.EXPECT().Get(gomock.Any()).AnyTimes()
phm.EXPECT().AddWithConnID(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_, _ protocol.ConnectionID, fn func() (packetHandler, bool)) bool {
phm.EXPECT().GetStatelessResetToken(gomock.Any())
_, ok := fn()
return ok
}).AnyTimes()
handshakeChan := make(chan struct{})
connChan := make(chan *MockQUICConn, 1)
serv.newConn = func(
_ sendConn,
runner connRunner,
@ -653,73 +688,140 @@ var _ = Describe("Server", func() {
_ utils.Logger,
_ protocol.VersionNumber,
) quicConn {
conn := NewMockQUICConn(mockCtrl)
conn := <-connChan
conn.EXPECT().handlePacket(gomock.Any())
conn.EXPECT().run()
conn.EXPECT().Context().Return(context.Background())
c := make(chan struct{})
close(c)
conn.EXPECT().HandshakeComplete().Return(c)
conn.EXPECT().HandshakeComplete().Return(handshakeChan)
return conn
}
phm.EXPECT().Get(gomock.Any()).Times(protocol.MaxAcceptQueueSize + 1)
phm.EXPECT().AddWithConnID(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_, _ protocol.ConnectionID, fn func() (packetHandler, bool)) bool {
phm.EXPECT().GetStatelessResetToken(gomock.Any())
_, ok := fn()
return ok
}).Times(protocol.MaxAcceptQueueSize)
var wg sync.WaitGroup
wg.Add(protocol.MaxAcceptQueueSize)
for i := 0; i < protocol.MaxAcceptQueueSize; i++ {
go func() {
defer GinkgoRecover()
defer wg.Done()
serv.handlePacket(getInitialWithRandomDestConnID())
// make sure there are no Write calls on the packet conn
time.Sleep(50 * time.Millisecond)
}()
// Initiate the maximum number of allowed connection attempts.
for i := 0; i < limit; i++ {
conn := NewMockQUICConn(mockCtrl)
connChan <- conn
serv.handlePacket(getInitialWithRandomDestConnID())
}
wg.Wait()
// Now initiate another connection attempt.
p := getInitialWithRandomDestConnID()
hdr, _, _, err := wire.ParsePacket(p.data)
Expect(err).ToNot(HaveOccurred())
tracer.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
done := make(chan struct{})
conn.EXPECT().WriteTo(gomock.Any(), p.remoteAddr).DoAndReturn(func(b []byte, _ net.Addr) (int, error) {
tracer.EXPECT().SentPacket(p.remoteAddr, gomock.Any(), gomock.Any(), gomock.Any()).Do(func(_ net.Addr, replyHdr *logging.Header, _ logging.ByteCount, frames []logging.Frame) {
defer GinkgoRecover()
Expect(replyHdr.Type).To(Equal(protocol.PacketTypeRetry))
})
conn.EXPECT().WriteTo(gomock.Any(), gomock.Any()).DoAndReturn(func(b []byte, _ net.Addr) (int, error) {
defer GinkgoRecover()
defer close(done)
rejectHdr := parseHeader(b)
Expect(rejectHdr.Type).To(Equal(protocol.PacketTypeInitial))
Expect(rejectHdr.Version).To(Equal(hdr.Version))
Expect(rejectHdr.DestConnectionID).To(Equal(hdr.SrcConnectionID))
Expect(rejectHdr.SrcConnectionID).To(Equal(hdr.DestConnectionID))
hdr, _, _, err := wire.ParsePacket(b)
Expect(err).ToNot(HaveOccurred())
Expect(hdr.Type).To(Equal(protocol.PacketTypeRetry))
return len(b), nil
})
serv.handlePacket(p)
Eventually(done).Should(BeClosed())
close(handshakeChan)
for i := 0; i < limit; i++ {
_, err := serv.Accept(context.Background())
Expect(err).ToNot(HaveOccurred())
}
for i := 0; i < limit; i++ {
conn := NewMockQUICConn(mockCtrl)
conn.EXPECT().closeWithTransportError(gomock.Any()).MaxTimes(1) // called when the server is closed
connChan <- conn
serv.handlePacket(getInitialWithRandomDestConnID())
}
})
It("limits the number of total handshakes", func() {
const limit = 3
serv.maxNumHandshakesTotal = limit
serv.maxNumHandshakesUnvalidated = limit // same limit, but we check that we send CONNECTION_REFUSED and not Retry
phm.EXPECT().Get(gomock.Any()).AnyTimes()
phm.EXPECT().AddWithConnID(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_, _ protocol.ConnectionID, fn func() (packetHandler, bool)) bool {
phm.EXPECT().GetStatelessResetToken(gomock.Any())
_, ok := fn()
return ok
}).AnyTimes()
handshakeChan := make(chan struct{})
connChan := make(chan *MockQUICConn, 1)
serv.newConn = func(
_ sendConn,
runner connRunner,
_ protocol.ConnectionID,
_ *protocol.ConnectionID,
_ protocol.ConnectionID,
_ protocol.ConnectionID,
_ protocol.ConnectionID,
_ ConnectionIDGenerator,
_ protocol.StatelessResetToken,
_ *Config,
_ *tls.Config,
_ *handshake.TokenGenerator,
_ bool,
_ *logging.ConnectionTracer,
_ uint64,
_ utils.Logger,
_ protocol.VersionNumber,
) quicConn {
conn := <-connChan
conn.EXPECT().handlePacket(gomock.Any())
conn.EXPECT().run()
conn.EXPECT().Context().Return(context.Background())
conn.EXPECT().HandshakeComplete().Return(handshakeChan)
return conn
}
for i := 0; i < limit; i++ {
conn := NewMockQUICConn(mockCtrl)
connChan <- conn
serv.handlePacket(getInitialWithRandomDestConnID())
}
p := getInitialWithRandomDestConnID()
done := make(chan struct{})
tracer.EXPECT().SentPacket(p.remoteAddr, gomock.Any(), gomock.Any(), gomock.Any()).Do(func(_ net.Addr, replyHdr *logging.Header, _ logging.ByteCount, frames []logging.Frame) {
defer GinkgoRecover()
hdr, _, _, err := wire.ParsePacket(p.data)
Expect(err).ToNot(HaveOccurred())
Expect(replyHdr.Type).To(Equal(protocol.PacketTypeInitial))
Expect(replyHdr.SrcConnectionID).To(Equal(hdr.DestConnectionID))
Expect(replyHdr.DestConnectionID).To(Equal(hdr.SrcConnectionID))
Expect(frames).To(HaveLen(1))
Expect(frames[0]).To(BeAssignableToTypeOf(&wire.ConnectionCloseFrame{}))
ccf := frames[0].(*logging.ConnectionCloseFrame)
Expect(ccf.IsApplicationError).To(BeFalse())
Expect(ccf.ErrorCode).To(BeEquivalentTo(qerr.ConnectionRefused))
})
conn.EXPECT().WriteTo(gomock.Any(), gomock.Any()).DoAndReturn(func(b []byte, _ net.Addr) (int, error) {
defer GinkgoRecover()
defer close(done)
hdr, _, _, err := wire.ParsePacket(p.data)
Expect(err).ToNot(HaveOccurred())
checkConnectionCloseError(b, hdr, qerr.ConnectionRefused)
return len(b), nil
})
serv.handlePacket(p)
Eventually(done).Should(BeClosed())
close(handshakeChan)
for i := 0; i < limit; i++ {
_, err := serv.Accept(context.Background())
Expect(err).ToNot(HaveOccurred())
}
for i := 0; i < limit; i++ {
conn := NewMockQUICConn(mockCtrl)
conn.EXPECT().closeWithTransportError(gomock.Any()).MaxTimes(1) // called when the server is closed
connChan <- conn
serv.handlePacket(getInitialWithRandomDestConnID())
}
})
})
Context("token validation", func() {
checkInvalidToken := func(b []byte, origHdr *wire.Header) {
replyHdr := parseHeader(b)
Expect(replyHdr.Type).To(Equal(protocol.PacketTypeInitial))
Expect(replyHdr.SrcConnectionID).To(Equal(origHdr.DestConnectionID))
Expect(replyHdr.DestConnectionID).To(Equal(origHdr.SrcConnectionID))
_, opener := handshake.NewInitialAEAD(origHdr.DestConnectionID, protocol.PerspectiveClient, replyHdr.Version)
extHdr, err := unpackLongHeader(opener, replyHdr, b, origHdr.Version)
Expect(err).ToNot(HaveOccurred())
data, err := opener.Open(nil, b[extHdr.ParsedLen():], extHdr.PacketNumber, b[:extHdr.ParsedLen()])
Expect(err).ToNot(HaveOccurred())
_, f, err := wire.NewFrameParser(false).ParseNext(data, protocol.EncryptionInitial, origHdr.Version)
Expect(err).ToNot(HaveOccurred())
Expect(f).To(BeAssignableToTypeOf(&wire.ConnectionCloseFrame{}))
ccf := f.(*wire.ConnectionCloseFrame)
Expect(ccf.ErrorCode).To(BeEquivalentTo(qerr.InvalidToken))
Expect(ccf.ReasonPhrase).To(BeEmpty())
}
It("decodes the token from the token field", func() {
raddr := &net.UDPAddr{IP: net.IPv4(192, 168, 13, 37), Port: 1337}
token, err := serv.tokenGenerator.NewRetryToken(raddr, protocol.ConnectionID{}, protocol.ConnectionID{})
@ -771,7 +873,7 @@ var _ = Describe("Server", func() {
done := make(chan struct{})
conn.EXPECT().WriteTo(gomock.Any(), raddr).DoAndReturn(func(b []byte, _ net.Addr) (int, error) {
defer close(done)
checkInvalidToken(b, hdr)
checkConnectionCloseError(b, hdr, qerr.InvalidToken)
return len(b), nil
})
phm.EXPECT().Get(gomock.Any())
@ -809,7 +911,7 @@ var _ = Describe("Server", func() {
done := make(chan struct{})
conn.EXPECT().WriteTo(gomock.Any(), raddr).DoAndReturn(func(b []byte, _ net.Addr) (int, error) {
defer close(done)
checkInvalidToken(b, hdr)
checkConnectionCloseError(b, hdr, qerr.InvalidToken)
return len(b), nil
})
phm.EXPECT().Get(gomock.Any())
@ -1186,8 +1288,7 @@ var _ = Describe("Server", func() {
})
It("rejects new connection attempts if the accept queue is full", func() {
senderAddr := &net.UDPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 42}
connChan := make(chan *MockQUICConn, 1)
serv.baseServer.newConn = func(
_ sendConn,
runner connRunner,
@ -1209,7 +1310,7 @@ var _ = Describe("Server", func() {
) quicConn {
ready := make(chan struct{})
close(ready)
conn := NewMockQUICConn(mockCtrl)
conn := <-connChan
conn.EXPECT().handlePacket(gomock.Any())
conn.EXPECT().run()
conn.EXPECT().earlyConnReady().Return(ready)
@ -1224,27 +1325,22 @@ var _ = Describe("Server", func() {
return ok
}).Times(protocol.MaxAcceptQueueSize)
for i := 0; i < protocol.MaxAcceptQueueSize; i++ {
conn := NewMockQUICConn(mockCtrl)
connChan <- conn
serv.baseServer.handlePacket(getInitialWithRandomDestConnID())
}
Eventually(serv.baseServer.connQueue).Should(HaveLen(protocol.MaxAcceptQueueSize))
// make sure there are no Write calls on the packet conn
time.Sleep(50 * time.Millisecond)
p := getInitialWithRandomDestConnID()
hdr := parseHeader(p.data)
done := make(chan struct{})
conn.EXPECT().WriteTo(gomock.Any(), senderAddr).DoAndReturn(func(b []byte, _ net.Addr) (int, error) {
defer close(done)
rejectHdr := parseHeader(b)
Expect(rejectHdr.Type).To(Equal(protocol.PacketTypeInitial))
Expect(rejectHdr.Version).To(Equal(hdr.Version))
Expect(rejectHdr.DestConnectionID).To(Equal(hdr.SrcConnectionID))
Expect(rejectHdr.SrcConnectionID).To(Equal(hdr.DestConnectionID))
return len(b), nil
phm.EXPECT().AddWithConnID(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_, _ protocol.ConnectionID, fn func() (packetHandler, bool)) bool {
phm.EXPECT().GetStatelessResetToken(gomock.Any())
_, ok := fn()
return ok
})
serv.baseServer.handlePacket(p)
Eventually(done).Should(BeClosed())
conn := NewMockQUICConn(mockCtrl)
conn.EXPECT().closeWithTransportError(ConnectionRefused)
connChan <- conn
serv.baseServer.handlePacket(getInitialWithRandomDestConnID())
})
It("doesn't accept new connections if they were closed in the mean time", func() {