From 0582e931a554020c5091350fdc9e2988bf9c5ab8 Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Thu, 1 Feb 2024 14:54:04 +0700 Subject: [PATCH] wire: optimize generation of Version Negotiation packets (#4278) * wire: optimize generation of Version Negotiation packets * protocol: optimize adding greased version numbers --- internal/protocol/version.go | 27 +++++++++++++++-------- internal/wire/version_negotiation.go | 23 +++++++++---------- internal/wire/version_negotiation_test.go | 11 +++++++++ 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/internal/protocol/version.go b/internal/protocol/version.go index 6ee587f3..025ade9b 100644 --- a/internal/protocol/version.go +++ b/internal/protocol/version.go @@ -1,10 +1,13 @@ package protocol import ( - "crypto/rand" "encoding/binary" "fmt" "math" + "sync" + "time" + + "golang.org/x/exp/rand" ) // Version is a version number as int @@ -85,18 +88,24 @@ func ChooseSupportedVersion(ours, theirs []Version) (Version, bool) { return 0, false } -// generateReservedVersion generates a reserved version number (v & 0x0f0f0f0f == 0x0a0a0a0a) +var ( + versionNegotiationMx sync.Mutex + versionNegotiationRand = rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) +) + +// generateReservedVersion generates a reserved version (v & 0x0f0f0f0f == 0x0a0a0a0a) func generateReservedVersion() Version { - b := make([]byte, 4) - _, _ = rand.Read(b) // ignore the error here. Failure to read random data doesn't break anything - return Version((binary.BigEndian.Uint32(b) | 0x0a0a0a0a) & 0xfafafafa) + var b [4]byte + _, _ = versionNegotiationRand.Read(b[:]) // ignore the error here. Failure to read random data doesn't break anything + return Version((binary.BigEndian.Uint32(b[:]) | 0x0a0a0a0a) & 0xfafafafa) } -// GetGreasedVersions adds one reserved version number to a slice of version numbers, at a random position +// GetGreasedVersions adds one reserved version number to a slice of version numbers, at a random position. +// It doesn't modify the supported slice. func GetGreasedVersions(supported []Version) []Version { - b := make([]byte, 1) - _, _ = rand.Read(b) // ignore the error here. Failure to read random data doesn't break anything - randPos := int(b[0]) % (len(supported) + 1) + versionNegotiationMx.Lock() + defer versionNegotiationMx.Unlock() + randPos := rand.Intn(len(supported) + 1) greased := make([]Version, len(supported)+1) copy(greased, supported[:randPos]) greased[randPos] = generateReservedVersion() diff --git a/internal/wire/version_negotiation.go b/internal/wire/version_negotiation.go index 94d04917..a551aa8c 100644 --- a/internal/wire/version_negotiation.go +++ b/internal/wire/version_negotiation.go @@ -1,13 +1,11 @@ package wire import ( - "bytes" "crypto/rand" "encoding/binary" "errors" "github.com/quic-go/quic-go/internal/protocol" - "github.com/quic-go/quic-go/internal/utils" ) // ParseVersionNegotiationPacket parses a Version Negotiation packet. @@ -37,20 +35,19 @@ func ParseVersionNegotiationPacket(b []byte) (dest, src protocol.ArbitraryLenCon func ComposeVersionNegotiation(destConnID, srcConnID protocol.ArbitraryLenConnectionID, versions []protocol.Version) []byte { greasedVersions := protocol.GetGreasedVersions(versions) expectedLen := 1 /* type byte */ + 4 /* version field */ + 1 /* dest connection ID length field */ + destConnID.Len() + 1 /* src connection ID length field */ + srcConnID.Len() + len(greasedVersions)*4 - buf := bytes.NewBuffer(make([]byte, 0, expectedLen)) - r := make([]byte, 1) - _, _ = rand.Read(r) // ignore the error here. It is not critical to have perfect random here. + buf := make([]byte, 1+4 /* type byte and version field */, expectedLen) + _, _ = rand.Read(buf[:1]) // ignore the error here. It is not critical to have perfect random here. // Setting the "QUIC bit" (0x40) is not required by the RFC, // but it allows clients to demultiplex QUIC with a long list of other protocols. // See RFC 9443 and https://mailarchive.ietf.org/arch/msg/quic/oR4kxGKY6mjtPC1CZegY1ED4beg/ for details. - buf.WriteByte(r[0] | 0xc0) - utils.BigEndian.WriteUint32(buf, 0) // version 0 - buf.WriteByte(uint8(destConnID.Len())) - buf.Write(destConnID.Bytes()) - buf.WriteByte(uint8(srcConnID.Len())) - buf.Write(srcConnID.Bytes()) + buf[0] |= 0xc0 + // The next 4 bytes are left at 0 (version number). + buf = append(buf, uint8(destConnID.Len())) + buf = append(buf, destConnID.Bytes()...) + buf = append(buf, uint8(srcConnID.Len())) + buf = append(buf, srcConnID.Bytes()...) for _, v := range greasedVersions { - utils.BigEndian.WriteUint32(buf, uint32(v)) + buf = binary.BigEndian.AppendUint32(buf, uint32(v)) } - return buf.Bytes() + return buf } diff --git a/internal/wire/version_negotiation_test.go b/internal/wire/version_negotiation_test.go index d39d94a0..c5fe3e68 100644 --- a/internal/wire/version_negotiation_test.go +++ b/internal/wire/version_negotiation_test.go @@ -2,6 +2,7 @@ package wire import ( "encoding/binary" + "testing" "golang.org/x/exp/rand" @@ -91,3 +92,13 @@ var _ = Describe("Version Negotiation Packets", func() { Expect(reservedVersion&0x0f0f0f0f == 0x0a0a0a0a).To(BeTrue()) // check that it's a greased version number }) }) + +func BenchmarkComposeVersionNegotiationPacket(b *testing.B) { + b.ReportAllocs() + supportedVersions := []protocol.Version{protocol.Version2, protocol.Version1, 0x1337} + destConnID := protocol.ArbitraryLenConnectionID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0xa, 0xb, 0xc, 0xd} + srcConnID := protocol.ArbitraryLenConnectionID{10, 9, 8, 7, 6, 5, 4, 3, 2, 1} + for i := 0; i < b.N; i++ { + ComposeVersionNegotiation(destConnID, srcConnID, supportedVersions) + } +}