From 50b7e80713ae2ab9946dddd54b1cce1f8a3485fd Mon Sep 17 00:00:00 2001 From: Gaukas Wang Date: Sat, 5 Aug 2023 22:43:55 -0600 Subject: [PATCH] new: support variable length quic frame padding Add variable length QUIC frame padding support. Refactor how QUIC frames are defined in a QUIC Spec. Update documentation and examples. Added Chrome and Firefox parrots. Close #3. --- README.md | 126 ++-- example/uquic/main.go | 288 +------- u_initial_packet_spec.go | 136 +--- u_packet_packer.go | 19 +- u_parrot.go | 686 ++++++++++++++++++ u_quic_frames.go | 295 ++++++++ ...cket_spec_test.go => u_quic_frames_test.go | 67 +- u_quic_spec.go | 11 +- 8 files changed, 1152 insertions(+), 476 deletions(-) create mode 100644 u_parrot.go create mode 100644 u_quic_frames.go rename u_initial_packet_spec_test.go => u_quic_frames_test.go (65%) diff --git a/README.md b/README.md index 70bd7aace..d01d8f08f 100644 --- a/README.md +++ b/README.md @@ -11,22 +11,31 @@ If you have any questions, bug reports or contributions, you are welcome to publ Development is still in progress and we welcome any contributions adding new features or fixing extant bugs. +# Disclaimer +This repository belongs to a large research project on how to fingerprint QUIC clients and how to mitigate such fingerprinting. We do not encourage any malicious use of this project's output, including this repository, [uTLS](https://github.com/refraction-networking/utls), and [clienthellod](https://github.com/gaukas/clienthellod). + +Our research paper is still yet to be published and therefore this repository is neither ready for production use nor peer-reviewed. And the scope of our research is limited that such mimicry backed by this library MAY NOT be realisticly indistinguishable from the real QUIC clients being mimicked, and some misuses of this library MAY lead to easier fingerprinting against the mimic. We welcome any contributions to improve the realism of the mimicry, as well as expanding the scope of this project. + +For anyone intending to use this library for censorship circumvention, please be sure to understand the risks and limitations of this library. + +If you are interested in our research, please stay tuned for our paper. + # Development in Progress ## Development Roadmap - [ ] Customize Initial Packet - [x] QUIC Header - - [ ] QUIC Frame ([#3](https://github.com/gaukas/uquic/issues/3)) + - [x] QUIC Frame (~~[#3](https://github.com/gaukas/uquic/issues/3)~~) - [x] QUIC Crypto Frame - [x] QUIC Padding Frame - [x] QUIC Ping Frame - - [ ] QUIC ACK Frame + - [ ] QUIC ACK Frame (on hold) - [x] TLS ClientHello Message (by [uTLS](https://github.com/refraction-networking/utls)) - [x] QUIC Transport Parameters (in a uTLS extension) - [ ] Customize Initial ACK behavior ([#1](https://github.com/gaukas/uquic/issues/1), [quic-go#4007](https://github.com/quic-go/quic-go/issues/4007)) - [ ] Customize Initial Retry behavior ([#2](https://github.com/gaukas/uquic/issues/2)) - [ ] Add preset QUIC parrots - - [ ] Google Chrome parrot - - [ ] Mozilla Firefox parrot + - [x] Google Chrome parrot (call for parrots w/ `Token/PSK`) + - [x] Mozilla Firefox parrot (call for parrots w/ `Token/PSK`) - [ ] Apple Safari parrot - [ ] Microsoft Edge parrot @@ -37,59 +46,66 @@ uQUIC provides a mechanism to customize the Initial Packet, which is unencrypted ### Build a QUIC Spec A QUIC Spec sets parameters and policies for uQUIC in establishing a QUIC connection. +See `u_parrot.go` for examples of building a QUIC Spec (parrot). + +### Use a preset QUIC Spec +We provide a few preset QUIC Specs (parrots) for popular QUIC clients in `u_parrot.go`. + +To use one, simple invoke `QUICID2Spec(id)`. See below for a complete example of using a preset QUIC Spec in an HTTP3 client. + ```go -func getQUICSpec() *uquic.QUICSpec { - return &uquic.QUICSpec{ - InitialPacketSpec: uquic.InitialPacketSpec{ - SrcConnIDLength: 3, - DestConnIDLength: 8, - InitPacketNumberLength: 1, - InitPacketNumber: 1, - ClientTokenLength: 0, - FrameOrder: uquic.QUICFrames{ - &uquic.QUICFrameCrypto{ - Offset: 0, - Length: 0, - }, - }, - }, - ClientHelloSpec: getClientHelloSpec(), - UDPDatagramMinSize: 1357, +package main + +import ( + "bytes" + "fmt" + "io" + "log" + "net/http" + + tls "github.com/refraction-networking/utls" + + quic "github.com/refraction-networking/uquic" + "github.com/refraction-networking/uquic/http3" +) + +func main() { + roundTripper := &http3.RoundTripper{ + TLSClientConfig: &tls.Config{}, + QuicConfig: &quic.Config{}, } -} -func getClientHelloSpec() *utls.ClientHelloSpec { - return &utls.ClientHelloSpec{ - // skipped a few mandatory fields, see uTLS for details - Extensions: []utls.TLSExtension{ - // skipped a few mandatory extensions, see uTLS for details - &utls.QUICTransportParametersExtension{ - TransportParameters: utls.TransportParameters{ - utls.InitialMaxStreamDataBidiRemote(0x100000), - utls.InitialMaxStreamsBidi(16), - utls.MaxDatagramFrameSize(1200), - utls.MaxIdleTimeout(30000), - utls.ActiveConnectionIDLimit(8), - &utls.GREASEQUICBit{}, - &utls.VersionInformation{ - ChoosenVersion: utls.VERSION_1, - AvailableVersions: []uint32{ - utls.VERSION_GREASE, - utls.VERSION_1, - }, - LegacyID: true, - }, - utls.InitialMaxStreamsUni(16), - &utls.GREASE{}, - utls.InitialMaxStreamDataBidiLocal(0xc00000), - utls.InitialMaxStreamDataUni(0x100000), - utls.InitialSourceConnectionID([]byte{}), - utls.MaxAckDelay(20), - utls.InitialMaxData(0x1800000), - &utls.DisableActiveMigration{}, - }, - }, - }, + quicSpec, err := quic.QUICID2Spec(quic.QUICFirefox_116) + // quicSpec, err := quic.QUICID2Spec(quic.QUICChrome_115) + if err != nil { + log.Fatal(err) + } + + uRoundTripper := http3.GetURoundTripper( + roundTripper, + &quicSpec, + // getCRQUICSpec(), + nil, + ) + defer uRoundTripper.Close() + + h3client := &http.Client{ + Transport: uRoundTripper, + } + + addr := "https://quic.tlsfingerprint.io/qfp/?beautify=true" + + rsp, err := h3client.Get(addr) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Got response for %s: %#v", addr, rsp) + + body := &bytes.Buffer{} + _, err = io.Copy(body, rsp.Body) + if err != nil { + log.Fatal(err) } + fmt.Printf("Response Body: %s", body.Bytes()) } -``` +``` \ No newline at end of file diff --git a/example/uquic/main.go b/example/uquic/main.go index 8e732e6c7..11b96eaf7 100644 --- a/example/uquic/main.go +++ b/example/uquic/main.go @@ -21,7 +21,7 @@ func main() { } tlsConf := &tls.Config{ - ServerName: "quic.tlsfingerprint.io", + // ServerName: "quic.tlsfingerprint.io", // ServerName: "www.cloudflare.com", // MinVersion: tls.VersionTLS13, KeyLogWriter: keyLogWriter, @@ -34,9 +34,16 @@ func main() { TLSClientConfig: tlsConf, QuicConfig: quicConf, } + + quicSpec, err := quic.QUICID2Spec(quic.QUICFirefox_116) + // quicSpec, err := quic.QUICID2Spec(quic.QUICChrome_115) + if err != nil { + log.Fatal(err) + } + uRoundTripper := http3.GetURoundTripper( roundTripper, - getFFQUICSpec(), + &quicSpec, // getCRQUICSpec(), nil, ) @@ -62,280 +69,3 @@ func main() { } fmt.Printf("Response Body: %s", body.Bytes()) } - -func getFFQUICSpec() *quic.QUICSpec { - return &quic.QUICSpec{ - InitialPacketSpec: quic.InitialPacketSpec{ - SrcConnIDLength: 3, - DestConnIDLength: 8, - InitPacketNumberLength: 1, - InitPacketNumber: 0, - ClientTokenLength: 0, - FrameOrder: quic.QUICFrames{}, // empty = single crypto - }, - ClientHelloSpec: getFFCHS(), - UDPDatagramMinSize: 1357, - } -} - -func getFFCHS() *tls.ClientHelloSpec { - return &tls.ClientHelloSpec{ - TLSVersMin: tls.VersionTLS13, - TLSVersMax: tls.VersionTLS13, - CipherSuites: []uint16{ - tls.TLS_AES_128_GCM_SHA256, - tls.TLS_CHACHA20_POLY1305_SHA256, - tls.TLS_AES_256_GCM_SHA384, - }, - CompressionMethods: []uint8{ - 0x0, // no compression - }, - Extensions: []tls.TLSExtension{ - &tls.SNIExtension{}, - &tls.ExtendedMasterSecretExtension{}, - &tls.RenegotiationInfoExtension{ - Renegotiation: tls.RenegotiateOnceAsClient, - }, - &tls.SupportedCurvesExtension{ - Curves: []tls.CurveID{ - tls.CurveX25519, - tls.CurveSECP256R1, - tls.CurveSECP384R1, - tls.CurveSECP521R1, - tls.FakeCurveFFDHE2048, - tls.FakeCurveFFDHE3072, - tls.FakeCurveFFDHE4096, - tls.FakeCurveFFDHE6144, - tls.FakeCurveFFDHE8192, - }, - }, - &tls.ALPNExtension{ - AlpnProtocols: []string{ - "h3", - }, - }, - &tls.StatusRequestExtension{}, - &tls.FakeDelegatedCredentialsExtension{ - SupportedSignatureAlgorithms: []tls.SignatureScheme{ - tls.ECDSAWithP256AndSHA256, - tls.ECDSAWithP384AndSHA384, - tls.ECDSAWithP521AndSHA512, - tls.ECDSAWithSHA1, - }, - }, - &tls.KeyShareExtension{ - KeyShares: []tls.KeyShare{ - { - Group: tls.X25519, - }, - }, - }, - &tls.SupportedVersionsExtension{ - Versions: []uint16{ - tls.VersionTLS13, - }, - }, - &tls.SignatureAlgorithmsExtension{ - SupportedSignatureAlgorithms: []tls.SignatureScheme{ - tls.ECDSAWithP256AndSHA256, - tls.ECDSAWithP384AndSHA384, - tls.ECDSAWithP521AndSHA512, - tls.ECDSAWithSHA1, - tls.PSSWithSHA256, - tls.PSSWithSHA384, - tls.PSSWithSHA512, - tls.PKCS1WithSHA256, - tls.PKCS1WithSHA384, - tls.PKCS1WithSHA512, - tls.PKCS1WithSHA1, - }, - }, - &tls.PSKKeyExchangeModesExtension{ - Modes: []uint8{ - tls.PskModeDHE, - }, - }, - &tls.FakeRecordSizeLimitExtension{ - Limit: 0x4001, - }, - &tls.QUICTransportParametersExtension{ - TransportParameters: tls.TransportParameters{ - tls.InitialMaxStreamDataBidiRemote(0x100000), - tls.InitialMaxStreamsBidi(16), - tls.MaxDatagramFrameSize(1200), - tls.MaxIdleTimeout(30000), - tls.ActiveConnectionIDLimit(8), - &tls.GREASEQUICBit{}, - &tls.VersionInformation{ - ChoosenVersion: tls.VERSION_1, - AvailableVersions: []uint32{ - tls.VERSION_GREASE, - tls.VERSION_1, - }, - LegacyID: true, - }, - tls.InitialMaxStreamsUni(16), - &tls.GREASE{ - IdOverride: 0xff02de1a, - ValueOverride: []byte{ - 0x43, 0xe8, - }, - }, - tls.InitialMaxStreamDataBidiLocal(0xc00000), - tls.InitialMaxStreamDataUni(0x100000), - tls.InitialSourceConnectionID([]byte{}), - tls.MaxAckDelay(20), - tls.InitialMaxData(0x1800000), - &tls.DisableActiveMigration{}, - }, - }, - &tls.UtlsPaddingExtension{ - GetPaddingLen: tls.BoringPaddingStyle, - }, - }, - } -} - -func getCRQUICSpec() *quic.QUICSpec { - return &quic.QUICSpec{ - InitialPacketSpec: quic.InitialPacketSpec{ - SrcConnIDLength: 0, - DestConnIDLength: 8, - InitPacketNumberLength: 1, - InitPacketNumber: 1, - ClientTokenLength: 0, - FrameOrder: quic.QUICFrames{ - &quic.QUICFrameCrypto{ - Offset: 300, - Length: 0, - }, - &quic.QUICFramePadding{ - Length: 125, - }, - &quic.QUICFramePing{}, - &quic.QUICFrameCrypto{ - Offset: 0, - Length: 300, - }, - }, - }, - ClientHelloSpec: getCRCHS(), - } -} -func getCRCHS() *tls.ClientHelloSpec { - return &tls.ClientHelloSpec{ - TLSVersMin: tls.VersionTLS13, - TLSVersMax: tls.VersionTLS13, - CipherSuites: []uint16{ - tls.TLS_AES_128_GCM_SHA256, - tls.TLS_CHACHA20_POLY1305_SHA256, - tls.TLS_AES_256_GCM_SHA384, - }, - CompressionMethods: []uint8{ - 0x0, // no compression - }, - Extensions: []tls.TLSExtension{ - &tls.SNIExtension{}, - &tls.ExtendedMasterSecretExtension{}, - &tls.RenegotiationInfoExtension{ - Renegotiation: tls.RenegotiateOnceAsClient, - }, - &tls.SupportedCurvesExtension{ - Curves: []tls.CurveID{ - tls.CurveX25519, - tls.CurveSECP256R1, - tls.CurveSECP384R1, - tls.CurveSECP521R1, - tls.FakeCurveFFDHE2048, - tls.FakeCurveFFDHE3072, - tls.FakeCurveFFDHE4096, - tls.FakeCurveFFDHE6144, - tls.FakeCurveFFDHE8192, - }, - }, - &tls.ALPNExtension{ - AlpnProtocols: []string{ - "h3", - }, - }, - &tls.StatusRequestExtension{}, - &tls.FakeDelegatedCredentialsExtension{ - SupportedSignatureAlgorithms: []tls.SignatureScheme{ - tls.ECDSAWithP256AndSHA256, - tls.ECDSAWithP384AndSHA384, - tls.ECDSAWithP521AndSHA512, - tls.ECDSAWithSHA1, - }, - }, - &tls.KeyShareExtension{ - KeyShares: []tls.KeyShare{ - { - Group: tls.X25519, - }, - // { - // Group: tls.CurveP256, - // }, - }, - }, - &tls.SupportedVersionsExtension{ - Versions: []uint16{ - tls.VersionTLS13, - }, - }, - &tls.SignatureAlgorithmsExtension{ - SupportedSignatureAlgorithms: []tls.SignatureScheme{ - tls.ECDSAWithP256AndSHA256, - tls.ECDSAWithP384AndSHA384, - tls.ECDSAWithP521AndSHA512, - tls.ECDSAWithSHA1, - tls.PSSWithSHA256, - tls.PSSWithSHA384, - tls.PSSWithSHA512, - tls.PKCS1WithSHA256, - tls.PKCS1WithSHA384, - tls.PKCS1WithSHA512, - tls.PKCS1WithSHA1, - }, - }, - &tls.PSKKeyExchangeModesExtension{ - Modes: []uint8{ - tls.PskModeDHE, - }, - }, - &tls.FakeRecordSizeLimitExtension{ - Limit: 0x4001, - }, - &tls.QUICTransportParametersExtension{ - TransportParameters: tls.TransportParameters{ - &tls.GREASE{ - IdOverride: 0x35967c5b9c37e023, - ValueOverride: []byte{ - 0xfc, 0x97, 0xbb, 0x57, 0xb8, 0x02, 0x19, 0xcd, - }, - }, - tls.InitialMaxStreamsUni(103), - tls.InitialSourceConnectionID([]byte{}), - tls.InitialMaxStreamsBidi(100), - tls.InitialMaxData(15728640), - &tls.VersionInformation{ - ChoosenVersion: tls.VERSION_1, - AvailableVersions: []uint32{ - tls.VERSION_1, - tls.VERSION_GREASE, - }, - LegacyID: true, - }, - tls.MaxIdleTimeout(30000), - tls.MaxUDPPayloadSize(1472), - tls.MaxDatagramFrameSize(65536), - tls.InitialMaxStreamDataBidiLocal(6291456), - tls.InitialMaxStreamDataUni(6291456), - tls.InitialMaxStreamDataBidiRemote(6291456), - }, - }, - &tls.UtlsPaddingExtension{ - GetPaddingLen: tls.BoringPaddingStyle, - }, - }, - } -} diff --git a/u_initial_packet_spec.go b/u_initial_packet_spec.go index 6c911493f..5b75a4027 100644 --- a/u_initial_packet_spec.go +++ b/u_initial_packet_spec.go @@ -1,12 +1,7 @@ package quic import ( - "bytes" "crypto/rand" - "errors" - - "github.com/gaukas/clienthellod" - "github.com/refraction-networking/uquic/quicvarint" ) type InitialPacketSpec struct { @@ -37,11 +32,11 @@ type InitialPacketSpec struct { // invalid since not assigned by the server. ClientTokenLength int - // QUICFrames specifies a list of QUIC frames to be sent in the first Initial + // FrameBuilder specifies how the frames should be encapsulated for the first Initial // packet. // - // If nil, it will be treated as a list with only a single QUICFrameCrypto. - FrameOrder QUICFrames + // If nil, there will be only one single Crypto frame in the first Initial packet. + FrameBuilder QUICFrameBuilder } func (ps *InitialPacketSpec) UpdateConfig(conf *Config) { @@ -78,128 +73,3 @@ func (d *dummyTokenStore) Pop(key string) (token *ClientToken) { func (d *dummyTokenStore) Put(_ string, _ *ClientToken) { // Do nothing } - -type QUICFrames []QUICFrame - -func (qfs QUICFrames) MarshalWithCryptoData(cryptoData []byte) (payload []byte, err error) { - if len(qfs) == 0 { // If no frames specified, send a single crypto frame - qfs = QUICFrames{QUICFrameCrypto{0, 0}} - return qfs.MarshalWithCryptoData(cryptoData) - } - - for _, frame := range qfs { - var frameBytes []byte - if offset, length, cryptoOK := frame.CryptoFrameInfo(); cryptoOK { - if length == 0 { - // calculate length: from offset to the end of cryptoData - length = len(cryptoData) - offset - } - frameBytes = []byte{0x06} // CRYPTO frame type - frameBytes = quicvarint.Append(frameBytes, uint64(offset)) - frameBytes = quicvarint.Append(frameBytes, uint64(length)) - frameCryptoData := make([]byte, length) - copy(frameCryptoData, cryptoData[offset:]) // copy at most length bytes - frameBytes = append(frameBytes, frameCryptoData...) - } else { // Handle none crypto frames: read and append to payload - frameBytes, err = frame.Read() - if err != nil { - return nil, err - } - } - payload = append(payload, frameBytes...) - } - return payload, nil -} - -func (qfs QUICFrames) MarshalWithFrames(frames []byte) (payload []byte, err error) { - // parse frames - r := bytes.NewReader(frames) - qchframes, err := clienthellod.ReadAllFrames(r) - if err != nil { - return nil, err - } - - // parse crypto data - cryptoData, err := clienthellod.ReassembleCRYPTOFrames(qchframes) - if err != nil { - return nil, err - } - - // marshal - return qfs.MarshalWithCryptoData(cryptoData) -} - -type QUICFrame interface { - // None crypto frames should return false for cryptoOK - CryptoFrameInfo() (offset, length int, cryptoOK bool) - - // None crypto frames should return the byte representation of the frame. - // Crypto frames' behavior is undefined and unused. - Read() ([]byte, error) -} - -// QUICFrameCrypto is used to specify the crypto frames containing the TLS ClientHello -// to be sent in the first Initial packet. -type QUICFrameCrypto struct { - // Offset is used to specify the starting offset of the crypto frame. - // Used when sending multiple crypto frames in a single packet. - // - // Multiple crypto frames in a single packet must not overlap and must - // make up an entire crypto stream continuously. - Offset int - - // Length is used to specify the length of the crypto frame. - // - // Must be set if it is NOT the last crypto frame in a packet. - Length int -} - -// CryptoFrameInfo() implements the QUICFrame interface. -// -// Crypto frames are later replaced by the crypto message using the information -// returned by this function. -func (q QUICFrameCrypto) CryptoFrameInfo() (offset, length int, cryptoOK bool) { - return q.Offset, q.Length, true -} - -// Read() implements the QUICFrame interface. -// -// Crypto frames are later replaced by the crypto message, so they are not Read()-able. -func (q QUICFrameCrypto) Read() ([]byte, error) { - return nil, errors.New("crypto frames are not Read()-able") -} - -// QUICFramePadding is used to specify the padding frames to be sent in the first Initial -// packet. -type QUICFramePadding struct { - // Length is used to specify the length of the padding frame. - Length int -} - -// CryptoFrameInfo() implements the QUICFrame interface. -func (q QUICFramePadding) CryptoFrameInfo() (offset, length int, cryptoOK bool) { - return 0, 0, false -} - -// Read() implements the QUICFrame interface. -// -// Padding simply returns a slice of bytes of the specified length filled with 0. -func (q QUICFramePadding) Read() ([]byte, error) { - return make([]byte, q.Length), nil -} - -// QUICFramePing is used to specify the ping frames to be sent in the first Initial -// packet. -type QUICFramePing struct{} - -// CryptoFrameInfo() implements the QUICFrame interface. -func (q QUICFramePing) CryptoFrameInfo() (offset, length int, cryptoOK bool) { - return 0, 0, false -} - -// Read() implements the QUICFrame interface. -// -// Ping simply returns a slice of bytes of size 1 with value 0x01(PING). -func (q QUICFramePing) Read() ([]byte, error) { - return []byte{0x01}, nil -} diff --git a/u_packet_packer.go b/u_packet_packer.go index 1e60f3a04..d4b85991c 100644 --- a/u_packet_packer.go +++ b/u_packet_packer.go @@ -1,8 +1,10 @@ package quic import ( + "bytes" "fmt" + "github.com/gaukas/clienthellod" "github.com/refraction-networking/uquic/internal/handshake" "github.com/refraction-networking/uquic/internal/protocol" "github.com/refraction-networking/uquic/internal/wire" @@ -234,10 +236,23 @@ func (p *uPacketPacker) MarshalInitialPacketPayload(pl payload, v protocol.Versi } } - uPayload, err := p.uSpec.InitialPacketSpec.FrameOrder.MarshalWithFrames(originalFrameBytes) + // extract CryptoData from originalFrameBytes + // parse frames + r := bytes.NewReader(originalFrameBytes) + qchframes, err := clienthellod.ReadAllFrames(r) if err != nil { return nil, err } - return uPayload, nil + // parse crypto data + cryptoData, err := clienthellod.ReassembleCRYPTOFrames(qchframes) + if err != nil { + return nil, err + } + + if p.uSpec.InitialPacketSpec.FrameBuilder == nil { + qfs := QUICFrames{} + return qfs.Build(cryptoData) + } + return p.uSpec.InitialPacketSpec.FrameBuilder.Build(cryptoData) } diff --git a/u_parrot.go b/u_parrot.go new file mode 100644 index 000000000..0c9455668 --- /dev/null +++ b/u_parrot.go @@ -0,0 +1,686 @@ +package quic + +import ( + "crypto/rand" + "fmt" + "math/big" + mrand "math/rand" + + tls "github.com/refraction-networking/utls" +) + +type QUICID struct { + Client string + + // Version specifies version of a mimicked clients (e.g. browsers). + Version string + + // Fingerprint is a unique identifier for each different QUIC client/spec. + Fingerprint string +} + +const ( + // clients + quicFirefox = "Firefox" + quicChrome = "Chrome" + quicIOS = "iOS" + quicAndroid = "Android" + quicEdge = "Edge" + quicSafari = "Safari" +) + +var ( + QUICFirefox_116 = QUICFirefox_116A // point to most-popular 8-byte DCID + QUICFirefox_116A = QUICID{quicFirefox, "116", "31ea0e4ffd75b477"} // DCID.len = 8 + QUICFirefox_116B = QUICID{quicFirefox, "116", "d07d3c9152fbc5e0"} // DCID.len = 9 + QUICFirefox_116C = QUICID{quicFirefox, "116", "c74f87b2a9ccc006"} // DCID.len = 15 + // TODO: add Firefox fingerprints with Token and PSK extension + + QUICChrome_115 = QUICChrome_115_IPv4 // IPv4 is still more popular + QUICChrome_115_IPv4 = QUICID{quicChrome, "115", "beeb454235791d5c"} // IPv4: UDP payload 20-byte longer than IPv6 due to padding + QUICChrome_115_IPv6 = QUICID{quicChrome, "115_ip6", "beeb454235791d5c"} // IPv6 + // TODO: add Chrome fingerprints with Token and PSK extension + + // TODO: add more QUIC clients and versions +) + +func QUICID2Spec(id QUICID) (QUICSpec, error) { + switch id { + case QUICChrome_115_IPv4: + return QUICSpec{ + InitialPacketSpec: InitialPacketSpec{ + SrcConnIDLength: 0, + DestConnIDLength: 8, + InitPacketNumberLength: 1, + InitPacketNumber: 1, // Chrome is special that it starts with 1 not 0 + ClientTokenLength: 0, + FrameBuilder: &QUICRandomFrames{ // Chrome randomly inserts padding frames + MinPING: 0, + MaxPING: 10, + MinCRYPTO: 1, + MaxCRYPTO: 10, + MinPADDING: 3, + MaxPADDING: 6, + Length: 1231 - 16, // 16-byte for Auth Tag + }, + }, + ClientHelloSpec: &tls.ClientHelloSpec{ + TLSVersMin: tls.VersionTLS13, + TLSVersMax: tls.VersionTLS13, + CipherSuites: []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, + }, + CompressionMethods: []uint8{ + 0x0, // no compression + }, + Extensions: ShuffleTLSExtensions([]tls.TLSExtension{ + ShuffleQUICTransportParameters(&tls.QUICTransportParametersExtension{ // Order of QTPs are always shuffled + TransportParameters: tls.TransportParameters{ + tls.InitialMaxStreamsUni(103), + tls.MaxIdleTimeout(30000), + tls.InitialMaxData(15728640), + tls.InitialMaxStreamDataUni(6291456), + &tls.VersionInformation{ + ChoosenVersion: tls.VERSION_1, + AvailableVersions: []uint32{ + tls.VERSION_GREASE, + tls.VERSION_1, + }, + LegacyID: true, + }, + &tls.FakeQUICTransportParameter{ // google_quic_version + Id: 0x4752, + Val: []byte{00, 00, 00, 01}, // Google QUIC version 1 + }, + &tls.FakeQUICTransportParameter{ // google_connection_options + Id: 0x3128, + Val: []byte{0x52, 0x56, 0x43, 0x4d}, + }, + tls.MaxDatagramFrameSize(65536), + tls.InitialMaxStreamsBidi(100), + tls.InitialMaxStreamDataBidiLocal(6291456), + VariableLengthGREASEQTP(0x10), // Random length for GREASE QTP + tls.InitialSourceConnectionID([]byte{}), + tls.MaxUDPPayloadSize(1472), + tls.InitialMaxStreamDataBidiRemote(6291456), + }, + }), + &tls.ApplicationSettingsExtension{ + SupportedProtocols: []string{ + "h3", + }, + }, + &tls.UtlsCompressCertExtension{ + Algorithms: []tls.CertCompressionAlgo{ + tls.CertCompressionBrotli, + }, + }, + &tls.KeyShareExtension{ + KeyShares: []tls.KeyShare{ + { + Group: tls.X25519, + }, + }, + }, + &tls.SignatureAlgorithmsExtension{ + SupportedSignatureAlgorithms: []tls.SignatureScheme{ + tls.ECDSAWithP256AndSHA256, + tls.PSSWithSHA256, + tls.PKCS1WithSHA256, + tls.ECDSAWithP384AndSHA384, + tls.PSSWithSHA384, + tls.PKCS1WithSHA384, + tls.PSSWithSHA512, + tls.PKCS1WithSHA512, + tls.PKCS1WithSHA1, + }, + }, + &tls.SNIExtension{}, + &tls.SupportedCurvesExtension{ + Curves: []tls.CurveID{ + tls.CurveX25519, + tls.CurveSECP256R1, + tls.CurveSECP384R1, + }, + }, + &tls.PSKKeyExchangeModesExtension{ + Modes: []uint8{ + tls.PskModeDHE, + }, + }, + &tls.ALPNExtension{ + AlpnProtocols: []string{ + "h3", + }, + }, + &tls.SupportedVersionsExtension{ + Versions: []uint16{ + tls.VersionTLS13, + }, + }, + }), + }, + }, nil + case QUICChrome_115_IPv6: + return QUICSpec{ + InitialPacketSpec: InitialPacketSpec{ + SrcConnIDLength: 0, + DestConnIDLength: 8, + InitPacketNumberLength: 1, + InitPacketNumber: 1, // Chrome is special that it starts with 1 not 0 + ClientTokenLength: 0, + FrameBuilder: &QUICRandomFrames{ // Chrome randomly inserts padding frames + MinPING: 0, + MaxPING: 10, + MinCRYPTO: 1, + MaxCRYPTO: 10, + MinPADDING: 3, + MaxPADDING: 6, + Length: 1211 - 16, // IPv6 pads to a length that is 20-byte shorter than IPv4's version + }, + }, + ClientHelloSpec: &tls.ClientHelloSpec{ + TLSVersMin: tls.VersionTLS13, + TLSVersMax: tls.VersionTLS13, + CipherSuites: []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, + }, + CompressionMethods: []uint8{ + 0x0, + }, + Extensions: ShuffleTLSExtensions([]tls.TLSExtension{ + ShuffleQUICTransportParameters(&tls.QUICTransportParametersExtension{ // Order of QTPs are always shuffled + TransportParameters: tls.TransportParameters{ + tls.InitialMaxStreamsUni(103), + tls.MaxIdleTimeout(30000), + tls.InitialMaxData(15728640), + tls.InitialMaxStreamDataUni(6291456), + &tls.VersionInformation{ + ChoosenVersion: tls.VERSION_1, + AvailableVersions: []uint32{ + tls.VERSION_GREASE, + tls.VERSION_1, + }, + LegacyID: true, + }, + &tls.FakeQUICTransportParameter{ // google_quic_version + Id: 0x4752, + Val: []byte{00, 00, 00, 01}, // Google QUIC version 1 + }, + &tls.FakeQUICTransportParameter{ // google_connection_options + Id: 0x3128, + Val: []byte{0x52, 0x56, 0x43, 0x4d}, + }, + tls.MaxDatagramFrameSize(65536), + tls.InitialMaxStreamsBidi(100), + tls.InitialMaxStreamDataBidiLocal(6291456), + VariableLengthGREASEQTP(0x10), // Random length for GREASE QTP + tls.InitialSourceConnectionID([]byte{}), + tls.MaxUDPPayloadSize(1472), + tls.InitialMaxStreamDataBidiRemote(6291456), + }, + }), + &tls.ApplicationSettingsExtension{ + SupportedProtocols: []string{ + "h3", + }, + }, + &tls.UtlsCompressCertExtension{ + Algorithms: []tls.CertCompressionAlgo{ + tls.CertCompressionBrotli, + }, + }, + &tls.KeyShareExtension{ + KeyShares: []tls.KeyShare{ + { + Group: tls.X25519, + }, + }, + }, + &tls.SignatureAlgorithmsExtension{ + SupportedSignatureAlgorithms: []tls.SignatureScheme{ + tls.ECDSAWithP256AndSHA256, + tls.PSSWithSHA256, + tls.PKCS1WithSHA256, + tls.ECDSAWithP384AndSHA384, + tls.PSSWithSHA384, + tls.PKCS1WithSHA384, + tls.PSSWithSHA512, + tls.PKCS1WithSHA512, + tls.PKCS1WithSHA1, + }, + }, + &tls.SNIExtension{}, + &tls.SupportedCurvesExtension{ + Curves: []tls.CurveID{ + tls.CurveX25519, + tls.CurveSECP256R1, + tls.CurveSECP384R1, + }, + }, + &tls.PSKKeyExchangeModesExtension{ + Modes: []uint8{ + tls.PskModeDHE, + }, + }, + &tls.ALPNExtension{ + AlpnProtocols: []string{ + "h3", + }, + }, + &tls.SupportedVersionsExtension{ + Versions: []uint16{ + tls.VersionTLS13, + }, + }, + }), + }, + }, nil + case QUICFirefox_116A: + return QUICSpec{ + InitialPacketSpec: InitialPacketSpec{ + SrcConnIDLength: 3, + DestConnIDLength: 8, + InitPacketNumberLength: 1, + InitPacketNumber: 0, + ClientTokenLength: 0, + FrameBuilder: QUICFrames{}, // empty = single crypto + }, + ClientHelloSpec: &tls.ClientHelloSpec{ + TLSVersMin: tls.VersionTLS13, + TLSVersMax: tls.VersionTLS13, + CipherSuites: []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_AES_256_GCM_SHA384, + }, + CompressionMethods: []uint8{ + 0x0, + }, + Extensions: []tls.TLSExtension{ + &tls.SNIExtension{}, + &tls.ExtendedMasterSecretExtension{}, + &tls.RenegotiationInfoExtension{ + Renegotiation: tls.RenegotiateOnceAsClient, + }, + &tls.SupportedCurvesExtension{ + Curves: []tls.CurveID{ + tls.CurveX25519, + tls.CurveSECP256R1, + tls.CurveSECP384R1, + tls.CurveSECP521R1, + tls.FakeCurveFFDHE2048, + tls.FakeCurveFFDHE3072, + tls.FakeCurveFFDHE4096, + tls.FakeCurveFFDHE6144, + tls.FakeCurveFFDHE8192, + }, + }, + &tls.ALPNExtension{ + AlpnProtocols: []string{ + "h3", + }, + }, + &tls.StatusRequestExtension{}, + &tls.FakeDelegatedCredentialsExtension{ + SupportedSignatureAlgorithms: []tls.SignatureScheme{ + tls.ECDSAWithP256AndSHA256, + tls.ECDSAWithP384AndSHA384, + tls.ECDSAWithP521AndSHA512, + tls.ECDSAWithSHA1, + }, + }, + &tls.KeyShareExtension{ + KeyShares: []tls.KeyShare{ + { + Group: tls.X25519, + }, + }, + }, + &tls.SupportedVersionsExtension{ + Versions: []uint16{ + tls.VersionTLS13, + }, + }, + &tls.SignatureAlgorithmsExtension{ + SupportedSignatureAlgorithms: []tls.SignatureScheme{ + tls.ECDSAWithP256AndSHA256, + tls.ECDSAWithP384AndSHA384, + tls.ECDSAWithP521AndSHA512, + tls.ECDSAWithSHA1, + tls.PSSWithSHA256, + tls.PSSWithSHA384, + tls.PSSWithSHA512, + tls.PKCS1WithSHA256, + tls.PKCS1WithSHA384, + tls.PKCS1WithSHA512, + tls.PKCS1WithSHA1, + }, + }, + &tls.PSKKeyExchangeModesExtension{ + Modes: []uint8{ + tls.PskModeDHE, + }, + }, + &tls.FakeRecordSizeLimitExtension{ + Limit: 0x4001, + }, + ShuffleQUICTransportParameters(&tls.QUICTransportParametersExtension{ + TransportParameters: tls.TransportParameters{ + tls.InitialMaxStreamDataBidiRemote(0x100000), + tls.InitialMaxStreamsBidi(16), + tls.MaxDatagramFrameSize(1200), + tls.MaxIdleTimeout(30000), + tls.ActiveConnectionIDLimit(8), + &tls.GREASEQUICBit{}, + &tls.VersionInformation{ + ChoosenVersion: tls.VERSION_1, + AvailableVersions: []uint32{ + tls.VERSION_GREASE, + tls.VERSION_1, + }, + LegacyID: true, + }, + tls.InitialMaxStreamsUni(16), + &tls.GREASE{ + Length: 2, // Firefox uses 2-byte GREASE values + }, + tls.InitialMaxStreamDataBidiLocal(0xc00000), + tls.InitialMaxStreamDataUni(0x100000), + tls.InitialSourceConnectionID([]byte{}), + tls.MaxAckDelay(20), + tls.InitialMaxData(0x1800000), + &tls.DisableActiveMigration{}, + }, + }), + &tls.UtlsPaddingExtension{ + GetPaddingLen: tls.BoringPaddingStyle, + }, + }, + }, + UDPDatagramMinSize: 1357, // Firefox pads with zeroes at the end of UDP datagrams + }, nil + case QUICFirefox_116B: + return QUICSpec{ + InitialPacketSpec: InitialPacketSpec{ + SrcConnIDLength: 3, + DestConnIDLength: 9, + InitPacketNumberLength: 1, + InitPacketNumber: 0, + ClientTokenLength: 0, + FrameBuilder: QUICFrames{}, + }, + ClientHelloSpec: &tls.ClientHelloSpec{ + TLSVersMin: tls.VersionTLS13, + TLSVersMax: tls.VersionTLS13, + CipherSuites: []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_AES_256_GCM_SHA384, + }, + CompressionMethods: []uint8{ + 0x0, + }, + Extensions: []tls.TLSExtension{ + &tls.SNIExtension{}, + &tls.ExtendedMasterSecretExtension{}, + &tls.RenegotiationInfoExtension{ + Renegotiation: tls.RenegotiateOnceAsClient, + }, + &tls.SupportedCurvesExtension{ + Curves: []tls.CurveID{ + tls.CurveX25519, + tls.CurveSECP256R1, + tls.CurveSECP384R1, + tls.CurveSECP521R1, + tls.FakeCurveFFDHE2048, + tls.FakeCurveFFDHE3072, + tls.FakeCurveFFDHE4096, + tls.FakeCurveFFDHE6144, + tls.FakeCurveFFDHE8192, + }, + }, + &tls.ALPNExtension{ + AlpnProtocols: []string{ + "h3", + }, + }, + &tls.StatusRequestExtension{}, + &tls.FakeDelegatedCredentialsExtension{ + SupportedSignatureAlgorithms: []tls.SignatureScheme{ + tls.ECDSAWithP256AndSHA256, + tls.ECDSAWithP384AndSHA384, + tls.ECDSAWithP521AndSHA512, + tls.ECDSAWithSHA1, + }, + }, + &tls.KeyShareExtension{ + KeyShares: []tls.KeyShare{ + { + Group: tls.X25519, + }, + }, + }, + &tls.SupportedVersionsExtension{ + Versions: []uint16{ + tls.VersionTLS13, + }, + }, + &tls.SignatureAlgorithmsExtension{ + SupportedSignatureAlgorithms: []tls.SignatureScheme{ + tls.ECDSAWithP256AndSHA256, + tls.ECDSAWithP384AndSHA384, + tls.ECDSAWithP521AndSHA512, + tls.ECDSAWithSHA1, + tls.PSSWithSHA256, + tls.PSSWithSHA384, + tls.PSSWithSHA512, + tls.PKCS1WithSHA256, + tls.PKCS1WithSHA384, + tls.PKCS1WithSHA512, + tls.PKCS1WithSHA1, + }, + }, + &tls.PSKKeyExchangeModesExtension{ + Modes: []uint8{ + tls.PskModeDHE, + }, + }, + &tls.FakeRecordSizeLimitExtension{ + Limit: 0x4001, + }, + ShuffleQUICTransportParameters(&tls.QUICTransportParametersExtension{ + TransportParameters: tls.TransportParameters{ + tls.InitialMaxStreamDataBidiRemote(0x100000), + tls.InitialMaxStreamsBidi(16), + tls.MaxDatagramFrameSize(1200), + tls.MaxIdleTimeout(30000), + tls.ActiveConnectionIDLimit(8), + &tls.GREASEQUICBit{}, + &tls.VersionInformation{ + ChoosenVersion: tls.VERSION_1, + AvailableVersions: []uint32{ + tls.VERSION_GREASE, + tls.VERSION_1, + }, + LegacyID: true, + }, + tls.InitialMaxStreamsUni(16), + &tls.GREASE{ + Length: 2, // Firefox uses 2-byte GREASE values + }, + tls.InitialMaxStreamDataBidiLocal(0xc00000), + tls.InitialMaxStreamDataUni(0x100000), + tls.InitialSourceConnectionID([]byte{}), + tls.MaxAckDelay(20), + tls.InitialMaxData(0x1800000), + &tls.DisableActiveMigration{}, + }, + }), + &tls.UtlsPaddingExtension{ + GetPaddingLen: tls.BoringPaddingStyle, + }, + }, + }, + UDPDatagramMinSize: 1357, + }, nil + case QUICFirefox_116C: + return QUICSpec{ + InitialPacketSpec: InitialPacketSpec{ + SrcConnIDLength: 3, + DestConnIDLength: 15, + InitPacketNumberLength: 1, + InitPacketNumber: 0, + ClientTokenLength: 0, + FrameBuilder: QUICFrames{}, + }, + ClientHelloSpec: &tls.ClientHelloSpec{ + TLSVersMin: tls.VersionTLS13, + TLSVersMax: tls.VersionTLS13, + CipherSuites: []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_AES_256_GCM_SHA384, + }, + CompressionMethods: []uint8{ + 0x0, + }, + Extensions: []tls.TLSExtension{ + &tls.SNIExtension{}, + &tls.ExtendedMasterSecretExtension{}, + &tls.RenegotiationInfoExtension{ + Renegotiation: tls.RenegotiateOnceAsClient, + }, + &tls.SupportedCurvesExtension{ + Curves: []tls.CurveID{ + tls.CurveX25519, + tls.CurveSECP256R1, + tls.CurveSECP384R1, + tls.CurveSECP521R1, + tls.FakeCurveFFDHE2048, + tls.FakeCurveFFDHE3072, + tls.FakeCurveFFDHE4096, + tls.FakeCurveFFDHE6144, + tls.FakeCurveFFDHE8192, + }, + }, + &tls.ALPNExtension{ + AlpnProtocols: []string{ + "h3", + }, + }, + &tls.StatusRequestExtension{}, + &tls.FakeDelegatedCredentialsExtension{ + SupportedSignatureAlgorithms: []tls.SignatureScheme{ + tls.ECDSAWithP256AndSHA256, + tls.ECDSAWithP384AndSHA384, + tls.ECDSAWithP521AndSHA512, + tls.ECDSAWithSHA1, + }, + }, + &tls.KeyShareExtension{ + KeyShares: []tls.KeyShare{ + { + Group: tls.X25519, + }, + }, + }, + &tls.SupportedVersionsExtension{ + Versions: []uint16{ + tls.VersionTLS13, + }, + }, + &tls.SignatureAlgorithmsExtension{ + SupportedSignatureAlgorithms: []tls.SignatureScheme{ + tls.ECDSAWithP256AndSHA256, + tls.ECDSAWithP384AndSHA384, + tls.ECDSAWithP521AndSHA512, + tls.ECDSAWithSHA1, + tls.PSSWithSHA256, + tls.PSSWithSHA384, + tls.PSSWithSHA512, + tls.PKCS1WithSHA256, + tls.PKCS1WithSHA384, + tls.PKCS1WithSHA512, + tls.PKCS1WithSHA1, + }, + }, + &tls.PSKKeyExchangeModesExtension{ + Modes: []uint8{ + tls.PskModeDHE, + }, + }, + &tls.FakeRecordSizeLimitExtension{ + Limit: 0x4001, + }, + ShuffleQUICTransportParameters(&tls.QUICTransportParametersExtension{ + TransportParameters: tls.TransportParameters{ + tls.InitialMaxStreamDataBidiRemote(0x100000), + tls.InitialMaxStreamsBidi(16), + tls.MaxDatagramFrameSize(1200), + tls.MaxIdleTimeout(30000), + tls.ActiveConnectionIDLimit(8), + &tls.GREASEQUICBit{}, + &tls.VersionInformation{ + ChoosenVersion: tls.VERSION_1, + AvailableVersions: []uint32{ + tls.VERSION_GREASE, + tls.VERSION_1, + }, + LegacyID: true, + }, + tls.InitialMaxStreamsUni(16), + &tls.GREASE{ + Length: 2, + }, + tls.InitialMaxStreamDataBidiLocal(0xc00000), + tls.InitialMaxStreamDataUni(0x100000), + tls.InitialSourceConnectionID([]byte{}), + tls.MaxAckDelay(20), + tls.InitialMaxData(0x1800000), + &tls.DisableActiveMigration{}, + }, + }), + &tls.UtlsPaddingExtension{ + GetPaddingLen: tls.BoringPaddingStyle, + }, + }, + }, + UDPDatagramMinSize: 1357, + }, nil + default: + return QUICSpec{}, fmt.Errorf("unknown QUIC ID: %v", id) + } +} + +func ShuffleTLSExtensions(exts []tls.TLSExtension) []tls.TLSExtension { + mrand.Shuffle(len(exts), func(i, j int) { + exts[i], exts[j] = exts[j], exts[i] + }) + return exts +} + +func ShuffleQUICTransportParameters(qtp *tls.QUICTransportParametersExtension) *tls.QUICTransportParametersExtension { + // shuffle the order of parameters + mrand.Shuffle(len(qtp.TransportParameters), func(i, j int) { + qtp.TransportParameters[i], qtp.TransportParameters[j] = qtp.TransportParameters[j], qtp.TransportParameters[i] + }) + return qtp +} + +func VariableLengthGREASEQTP(maxLen int) *tls.GREASE { + // get random length for GREASE + greaseMaxLen := big.NewInt(0x10) + greaseLen, err := rand.Int(rand.Reader, greaseMaxLen) + if err != nil { + panic(err) + } + + return &tls.GREASE{ + Length: uint16(greaseLen.Uint64()), + } +} diff --git a/u_quic_frames.go b/u_quic_frames.go new file mode 100644 index 000000000..7070e9926 --- /dev/null +++ b/u_quic_frames.go @@ -0,0 +1,295 @@ +package quic + +import ( + "bytes" + "crypto/rand" + "errors" + "math/big" + mrand "math/rand" + + "github.com/gaukas/clienthellod" + "github.com/refraction-networking/uquic/quicvarint" +) + +type QUICFrameBuilder interface { + // Build ingests data from crypto frames without the crypto frame header + // and returns the byte representation of all frames. + Build(cryptoData []byte) (allFrames []byte, err error) +} + +// QUICFrames is a slice of QUICFrame that implements QUICFrameBuilder. +// It could be used to deterministically build QUIC Frames from crypto data. +type QUICFrames []QUICFrame + +// Build ingests data from crypto frames without the crypto frame header +// and returns the byte representation of all frames as specified in +// the slice. +func (qfs QUICFrames) Build(cryptoData []byte) (payload []byte, err error) { + if len(qfs) == 0 { // If no frames specified, send a single crypto frame + qfsCryptoOnly := QUICFrames{QUICFrameCrypto{0, 0}} + return qfsCryptoOnly.Build(cryptoData) + } + + for _, frame := range qfs { + var frameBytes []byte + if offset, length, cryptoOK := frame.CryptoFrameInfo(); cryptoOK { + if length == 0 { + // calculate length: from offset to the end of cryptoData + length = len(cryptoData) - offset + } + frameBytes = []byte{0x06} // CRYPTO frame type + frameBytes = quicvarint.Append(frameBytes, uint64(offset)) + frameBytes = quicvarint.Append(frameBytes, uint64(length)) + frameCryptoData := make([]byte, length) + copy(frameCryptoData, cryptoData[offset:]) // copy at most length bytes + frameBytes = append(frameBytes, frameCryptoData...) + } else { // Handle none crypto frames: read and append to payload + frameBytes, err = frame.Read() + if err != nil { + return nil, err + } + } + payload = append(payload, frameBytes...) + } + return payload, nil +} + +// BuildFromFrames ingests data from all input frames and returns the byte representation +// of all frames as specified in the slice. +func (qfs QUICFrames) BuildFromFrames(frames []byte) (payload []byte, err error) { + // parse frames + r := bytes.NewReader(frames) + qchframes, err := clienthellod.ReadAllFrames(r) + if err != nil { + return nil, err + } + + // parse crypto data + cryptoData, err := clienthellod.ReassembleCRYPTOFrames(qchframes) + if err != nil { + return nil, err + } + + // marshal + return qfs.Build(cryptoData) +} + +// QUICFrame is the interface for all QUIC frames to be included in the Initial Packet. +type QUICFrame interface { + // None crypto frames should return false for cryptoOK + CryptoFrameInfo() (offset, length int, cryptoOK bool) + + // None crypto frames should return the byte representation of the frame. + // Crypto frames' behavior is undefined and unused. + Read() ([]byte, error) +} + +// QUICFrameCrypto is used to specify the crypto frames containing the TLS ClientHello +// to be sent in the first Initial packet. +type QUICFrameCrypto struct { + // Offset is used to specify the starting offset of the crypto frame. + // Used when sending multiple crypto frames in a single packet. + // + // Multiple crypto frames in a single packet must not overlap and must + // make up an entire crypto stream continuously. + Offset int + + // Length is used to specify the length of the crypto frame. + // + // Must be set if it is NOT the last crypto frame in a packet. + Length int +} + +// CryptoFrameInfo() implements the QUICFrame interface. +// +// Crypto frames are later replaced by the crypto message using the information +// returned by this function. +func (q QUICFrameCrypto) CryptoFrameInfo() (offset, length int, cryptoOK bool) { + return q.Offset, q.Length, true +} + +// Read() implements the QUICFrame interface. +// +// Crypto frames are later replaced by the crypto message, so they are not Read()-able. +func (q QUICFrameCrypto) Read() ([]byte, error) { + return nil, errors.New("crypto frames are not Read()-able") +} + +// QUICFramePadding is used to specify the padding frames to be sent in the first Initial +// packet. +type QUICFramePadding struct { + // Length is used to specify the length of the padding frame. + Length int +} + +// CryptoFrameInfo() implements the QUICFrame interface. +func (q QUICFramePadding) CryptoFrameInfo() (offset, length int, cryptoOK bool) { + return 0, 0, false +} + +// Read() implements the QUICFrame interface. +// +// Padding simply returns a slice of bytes of the specified length filled with 0. +func (q QUICFramePadding) Read() ([]byte, error) { + return make([]byte, q.Length), nil +} + +// QUICFramePing is used to specify the ping frames to be sent in the first Initial +// packet. +type QUICFramePing struct{} + +// CryptoFrameInfo() implements the QUICFrame interface. +func (q QUICFramePing) CryptoFrameInfo() (offset, length int, cryptoOK bool) { + return 0, 0, false +} + +// Read() implements the QUICFrame interface. +// +// Ping simply returns a slice of bytes of size 1 with value 0x01(PING). +func (q QUICFramePing) Read() ([]byte, error) { + return []byte{0x01}, nil +} + +// QUICRandomFrames could be used to indeterministically build QUIC Frames from +// crypto data. A caller may specify how many PING and CRYPTO frames are expected +// to be included in the Initial Packet, as well as the total length plus PADDING +// frames in the end. +type QUICRandomFrames struct { + // MinPING specifies the inclusive lower bound of the number of PING frames to be + // included in the Initial Packet. + MinPING uint8 + + // MaxPING specifies the exclusive upper bound of the number of PING frames to be + // included in the Initial Packet. It must be at least MinPING+1. + MaxPING uint8 + + // MinCRYPTO specifies the inclusive lower bound of the number of CRYPTO frames to + // split the Crypto data into. It must be at least 1. + MinCRYPTO uint8 + + // MaxCRYPTO specifies the exclusive upper bound of the number of CRYPTO frames to + // split the Crypto data into. It must be at least MinCRYPTO+1. + MaxCRYPTO uint8 + + // MinPADDING specifies the inclusive lower bound of the number of PADDING frames + // to be included in the Initial Packet. It must be at least 1 if Length is not 0. + MinPADDING uint8 + + // MaxPADDING specifies the exclusive upper bound of the number of PADDING frames + // to be included in the Initial Packet. It must be at least MinPADDING+1 if + // Length is not 0. + MaxPADDING uint8 + + // Length specifies the total length of all frames including PADDING frames. + // If the Length specified is already exceeded by the CRYPTO+PING frames, no + // PADDING frames will be included. + Length uint16 // 2 bytes, max 65535 +} + +// Build ingests data from crypto frames without the crypto frame header +// and returns the byte representation of all frames as specified in +// the slice. +func (qrf *QUICRandomFrames) Build(cryptoData []byte) (payload []byte, err error) { + // check all bounds + if qrf.MinPING > qrf.MaxPING { + return nil, errors.New("MinPING must be less than or equal to MaxPING") + } + if qrf.MinCRYPTO < 1 { + return nil, errors.New("MinCRYPTO must be at least 1") + } + if qrf.MinCRYPTO > qrf.MaxCRYPTO { + return nil, errors.New("MinCRYPTO must be less than or equal to MaxCRYPTO") + } + if qrf.MinPADDING < 1 && qrf.Length != 0 { + return nil, errors.New("MinPADDING must be at least 1 if Length is not 0") + } + if qrf.MinPADDING > qrf.MaxPADDING && qrf.Length != 0 { + return nil, errors.New("MinPADDING must be less than or equal to MaxPADDING if Length is not 0") + } + + var frameList QUICFrames = make([]QUICFrame, 0) + + var cryptoSafeRandUint64 = func(min, max uint64) (uint64, error) { + minMaxDiff := big.NewInt(int64(max - min)) + offset, err := rand.Int(rand.Reader, minMaxDiff) + if err != nil { + return 0, err + } + return min + offset.Uint64(), nil + } + + // determine number of PING frames with crypto.rand + numPING, err := cryptoSafeRandUint64(uint64(qrf.MinPING), uint64(qrf.MaxPING)) + if err != nil { + return nil, err + } + + // append PING frames + for i := uint64(0); i < numPING; i++ { + frameList = append(frameList, QUICFramePing{}) + } + + // determine number of CRYPTO frames with crypto.rand + numCRYPTO, err := cryptoSafeRandUint64(uint64(qrf.MinCRYPTO), uint64(qrf.MaxCRYPTO)) + if err != nil { + return nil, err + } + + lenCryptoData := uint64(len(cryptoData)) + offsetCryptoData := uint64(0) + for i := uint64(0); i < numCRYPTO-1; i++ { // select n-1 times, since the last one must be the remaining + // randomly select length of CRYPTO frame. + // Length must be at least 1 byte and at most the remaining length of cryptoData minus the remaining number of CRYPTO frames. + // i.e. len in [1, len(cryptoData)-offsetCryptoData-(numCRYPTO-i-2)) + lenCRYPTO, err := cryptoSafeRandUint64(1, lenCryptoData-(numCRYPTO-i-2)) + if err != nil { + return nil, err + } + frameList = append(frameList, QUICFrameCrypto{Offset: int(offsetCryptoData), Length: int(lenCRYPTO)}) + offsetCryptoData += lenCRYPTO + lenCryptoData -= lenCRYPTO + } + + // append the last CRYPTO frame + frameList = append(frameList, QUICFrameCrypto{Offset: int(offsetCryptoData), Length: 0}) // 0 means the remaining + + // dry-run to determine the total length of all frames so far + dryrunPayload, err := frameList.Build(cryptoData) + if err != nil { + return nil, err + } + + // determine length of PADDING frames to append + lenPADDINGsigned := int64(qrf.Length) - int64(len(dryrunPayload)) + if lenPADDINGsigned > 0 { + lenPADDING := uint64(lenPADDINGsigned) + // determine number of PADDING frames to append + numPADDING, err := cryptoSafeRandUint64(uint64(qrf.MinPADDING), uint64(qrf.MaxPADDING)) + if err != nil { + return nil, err + } + + for i := uint64(0); i < numPADDING-1; i++ { // select n-1 times, since the last one must be the remaining + // randomly select length of PADDING frame. + // Length must be at least 1 byte and at most the remaining length of cryptoData minus the remaining number of CRYPTO frames. + // i.e. len in [1, lenPADDING-(numPADDING-i-2)) + lenPADDINGFrame, err := cryptoSafeRandUint64(1, lenPADDING-(numPADDING-i-2)) + if err != nil { + return nil, err + } + frameList = append(frameList, QUICFramePadding{Length: int(lenPADDINGFrame)}) + lenPADDING -= lenPADDINGFrame + } + + // append the last CRYPTO frame + frameList = append(frameList, QUICFramePadding{Length: int(lenPADDING)}) // 0 means the remaining + } + + // shuffle the frameList + mrand.Shuffle(len(frameList), func(i, j int) { + frameList[i], frameList[j] = frameList[j], frameList[i] + }) + + // build the payload + return frameList.Build(cryptoData) +} diff --git a/u_initial_packet_spec_test.go b/u_quic_frames_test.go similarity index 65% rename from u_initial_packet_spec_test.go rename to u_quic_frames_test.go index 9d546c19e..c634cbb25 100644 --- a/u_initial_packet_spec_test.go +++ b/u_quic_frames_test.go @@ -7,14 +7,14 @@ import ( "github.com/gaukas/clienthellod" ) -func TestQUICFramesMarshalWithCryptoData(t *testing.T) { - resultQUICPayload, err := testQUICFrames.MarshalWithCryptoData(testCryptoFrameBytes) +func TestQUICFrames(t *testing.T) { + resultQUICPayload, err := testQUICFrames.Build(testCryptoFrameBytes) if err != nil { - t.Fatalf("Failed to marshal QUIC frames: %v", err) + t.Fatalf("Failed to build QUIC frames: %v", err) } - if len(resultQUICPayload) != len(truthQUICPayload) { - t.Fatalf("QUIC payload length mismatch: got %d, want %d. \n%x", len(resultQUICPayload), len(truthQUICPayload), resultQUICPayload) + if len(resultQUICPayload) != len(truthPayloadFromQUICFrames) { + t.Fatalf("QUIC payload length mismatch: got %d, want %d. \n%x", len(resultQUICPayload), len(truthPayloadFromQUICFrames), resultQUICPayload) } // verify that the crypto frames would actually assemble the original crypto data @@ -33,6 +33,51 @@ func TestQUICFramesMarshalWithCryptoData(t *testing.T) { } } +func TestQUICRandomFrames(t *testing.T) { + resultQUICPayload, err := testQUICRandomFrames.Build(testCryptoFrameBytes) + if err != nil { + t.Fatalf("Failed to build QUIC frames: %v", err) + } + + if len(resultQUICPayload) != 512 { + t.Fatalf("QUIC payload length mismatch: got %d, want 512. \n%x", len(resultQUICPayload), resultQUICPayload) + } + + // verify that the crypto frames would actually assemble the original crypto data + r := bytes.NewReader(resultQUICPayload) + qchframes, err := clienthellod.ReadAllFrames(r) + if err != nil { + t.Fatalf("Failed to read QUIC frames: %v", err) + } + + reassembledCryptoData, err := clienthellod.ReassembleCRYPTOFrames(qchframes) + if err != nil { + t.Fatalf("Failed to reassemble crypto data: %v", err) + } + if !bytes.Equal(reassembledCryptoData, testCryptoFrameBytes) { + t.Fatalf("Reassembled crypto data mismatch: \n%x", reassembledCryptoData) + } + + // count how many PING and CRYPTO frames are in the QUIC payload + var pingCount, cryptoCount int + for _, frame := range qchframes { + switch frame.FrameType() { + case clienthellod.QUICFrame_PING: + pingCount++ + case clienthellod.QUICFrame_CRYPTO: + cryptoCount++ + } + } + + if pingCount < 2 || pingCount > 8 { + t.Fatalf("PING frame count mismatch: got %d, want 2-8", pingCount) + } + + if cryptoCount < 2 || cryptoCount > 8 { + t.Fatalf("CRYPTO frame count mismatch: got %d, want 2-8", cryptoCount) + } +} + var ( testCryptoFrameBytes = []byte{ 0x00, 0x01, 0x02, 0x03, @@ -77,7 +122,7 @@ var ( &QUICFramePadding{Length: 45}, } - truthQUICPayload = []byte{ + truthPayloadFromQUICFrames = []byte{ 0x01, // ping 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -115,4 +160,14 @@ var ( 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 45 bytes of padding } + + testQUICRandomFrames = QUICRandomFrames{ + MinPING: 2, + MaxPING: 8, + MinCRYPTO: 2, + MaxCRYPTO: 8, + MinPADDING: 4, + MaxPADDING: 5, + Length: 512, + } ) diff --git a/u_quic_spec.go b/u_quic_spec.go index a7ef41dda..be4a2c308 100644 --- a/u_quic_spec.go +++ b/u_quic_spec.go @@ -7,9 +7,18 @@ const ( ) type QUICSpec struct { + // InitialPacketSpec specifies the QUIC Initial Packet, which includes Initial + // Packet Headers and Frames. InitialPacketSpec InitialPacketSpec - ClientHelloSpec *tls.ClientHelloSpec + // ClientHelloSpec specifies the TLS ClientHello to be sent in the first Initial + // Packet. It is implemented by the uTLS library and a valid ClientHelloSpec + // for QUIC MUST include (utls).QUICTransportParametersExtension. + ClientHelloSpec *tls.ClientHelloSpec + + // UDPDatagramMinSize specifies the minimum size of the UDP Datagram (UDP payload). + // If the UDP Datagram is smaller than this size, zeros will be padded to the end + // of the UDP Datagram until this size is reached. UDPDatagramMinSize int }