analyzer: make http3/quic handling more reliable
Some checks failed
Quality check / Tests (push) Has been cancelled
Quality check / Static analysis (push) Has been cancelled

This commit is contained in:
2026-02-11 15:29:02 +05:30
parent 198f72814c
commit 20294716e3
3 changed files with 332 additions and 37 deletions

View File

@@ -13,6 +13,19 @@ import (
)
func ReadCryptoPayload(packet []byte) ([]byte, error) {
frs, err := ReadCryptoFrames(packet)
if err != nil {
return nil, err
}
data := assembleCryptoFrames(frs)
if data == nil {
return nil, errors.New("unable to assemble crypto frames")
}
return data, nil
}
// ReadCryptoFrames decrypts a QUIC Initial client packet and returns CRYPTO frames.
func ReadCryptoFrames(packet []byte) ([]CryptoFrame, error) {
hdr, offset, err := ParseInitialHeader(packet)
if err != nil {
return nil, err
@@ -46,11 +59,7 @@ func ReadCryptoPayload(packet []byte) ([]byte, error) {
if err != nil {
return nil, err
}
data := assembleCryptoFrames(frs)
if data == nil {
return nil, errors.New("unable to assemble crypto frames")
}
return data, nil
return frs, nil
}
const (
@@ -59,46 +68,225 @@ const (
cryptoFrameType = 0x06
)
type cryptoFrame struct {
type CryptoFrame struct {
Offset int64
Data []byte
}
func extractCryptoFrames(r *bytes.Reader) ([]cryptoFrame, error) {
var frames []cryptoFrame
func extractCryptoFrames(r *bytes.Reader) ([]CryptoFrame, error) {
var frames []CryptoFrame
for r.Len() > 0 {
typ, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
if typ == paddingFrameType || typ == pingFrameType {
switch typ {
case paddingFrameType, pingFrameType, 0x1e:
// PADDING, PING, HANDSHAKE_DONE: no payload.
continue
case 0x02, 0x03:
// ACK, ACK_ECN
if _, err := quicvarint.Read(r); err != nil { // Largest Acknowledged
return nil, err
}
if _, err := quicvarint.Read(r); err != nil { // ACK Delay
return nil, err
}
ackRangeCount, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
if _, err := quicvarint.Read(r); err != nil { // First ACK Range
return nil, err
}
for i := uint64(0); i < ackRangeCount; i++ {
if _, err := quicvarint.Read(r); err != nil { // Gap
return nil, err
}
if _, err := quicvarint.Read(r); err != nil { // ACK Range Length
return nil, err
}
}
if typ == 0x03 {
if _, err := quicvarint.Read(r); err != nil { // ECT0 Count
return nil, err
}
if _, err := quicvarint.Read(r); err != nil { // ECT1 Count
return nil, err
}
if _, err := quicvarint.Read(r); err != nil { // ECN-CE Count
return nil, err
}
}
case 0x04:
// RESET_STREAM
if _, err := quicvarint.Read(r); err != nil { // Stream ID
return nil, err
}
if _, err := quicvarint.Read(r); err != nil { // Application Error Code
return nil, err
}
if _, err := quicvarint.Read(r); err != nil { // Final Size
return nil, err
}
case 0x05:
// STOP_SENDING
if _, err := quicvarint.Read(r); err != nil { // Stream ID
return nil, err
}
if _, err := quicvarint.Read(r); err != nil { // Application Error Code
return nil, err
}
case cryptoFrameType:
// CRYPTO
var frame CryptoFrame
offset, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
frame.Offset = int64(offset)
dataLen, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
frame.Data = make([]byte, dataLen)
if _, err := io.ReadFull(r, frame.Data); err != nil {
return nil, err
}
frames = append(frames, frame)
case 0x07:
// NEW_TOKEN
tokenLen, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
if err := skipN(r, tokenLen); err != nil {
return nil, err
}
case 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f:
// STREAM
if _, err := quicvarint.Read(r); err != nil { // Stream ID
return nil, err
}
hasOffset := typ&0x04 != 0
hasLength := typ&0x02 != 0
if hasOffset {
if _, err := quicvarint.Read(r); err != nil { // Offset
return nil, err
}
}
var dataLen uint64
if hasLength {
n, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
dataLen = n
} else {
dataLen = uint64(r.Len())
}
if err := skipN(r, dataLen); err != nil {
return nil, err
}
case 0x10, 0x12, 0x13, 0x14, 0x16, 0x17, 0x19:
// MAX_DATA, MAX_STREAMS_*, DATA_BLOCKED, STREAMS_BLOCKED_*, RETIRE_CONNECTION_ID
if _, err := quicvarint.Read(r); err != nil {
return nil, err
}
case 0x11, 0x15:
// MAX_STREAM_DATA, STREAM_DATA_BLOCKED
if _, err := quicvarint.Read(r); err != nil {
return nil, err
}
if _, err := quicvarint.Read(r); err != nil {
return nil, err
}
case 0x18:
// NEW_CONNECTION_ID
if _, err := quicvarint.Read(r); err != nil { // Sequence Number
return nil, err
}
if _, err := quicvarint.Read(r); err != nil { // Retire Prior To
return nil, err
}
cidLen, err := r.ReadByte()
if err != nil {
return nil, err
}
if cidLen > 20 {
return nil, fmt.Errorf("invalid connection ID length: %d", cidLen)
}
if err := skipN(r, uint64(cidLen)); err != nil { // Connection ID
return nil, err
}
if err := skipN(r, 16); err != nil { // Stateless Reset Token
return nil, err
}
case 0x1a, 0x1b:
// PATH_CHALLENGE, PATH_RESPONSE
if err := skipN(r, 8); err != nil {
return nil, err
}
case 0x1c:
// CONNECTION_CLOSE (transport)
if _, err := quicvarint.Read(r); err != nil { // Error Code
return nil, err
}
if _, err := quicvarint.Read(r); err != nil { // Frame Type
return nil, err
}
reasonLen, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
if err := skipN(r, reasonLen); err != nil {
return nil, err
}
case 0x1d:
// CONNECTION_CLOSE (application)
if _, err := quicvarint.Read(r); err != nil { // Error Code
return nil, err
}
reasonLen, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
if err := skipN(r, reasonLen); err != nil {
return nil, err
}
case 0x30, 0x31:
// DATAGRAM
var dataLen uint64
if typ&0x01 != 0 {
n, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
dataLen = n
} else {
dataLen = uint64(r.Len())
}
if err := skipN(r, dataLen); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported frame type: %d", typ)
}
if typ != cryptoFrameType {
return nil, fmt.Errorf("encountered unexpected frame type: %d", typ)
}
var frame cryptoFrame
offset, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
frame.Offset = int64(offset)
dataLen, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
frame.Data = make([]byte, dataLen)
if _, err := io.ReadFull(r, frame.Data); err != nil {
return nil, err
}
frames = append(frames, frame)
}
return frames, nil
}
func skipN(r *bytes.Reader, n uint64) error {
if n > uint64(r.Len()) {
return io.EOF
}
_, err := r.Seek(int64(n), io.SeekCurrent)
return err
}
// assembleCryptoFrames assembles multiple crypto frames into a single slice (if possible).
// It returns an error if the frames cannot be assembled. This can happen if the frames are not contiguous.
func assembleCryptoFrames(frames []cryptoFrame) []byte {
func assembleCryptoFrames(frames []CryptoFrame) []byte {
if len(frames) == 0 {
return nil
}

View File

@@ -0,0 +1,39 @@
package quic
import (
"bytes"
"testing"
)
func TestExtractCryptoFrames_Interleaved(t *testing.T) {
// PADDING, CRYPTO(offset=0,"abc"), PING, STREAM(len=2), CRYPTO(offset=3,"de")
payload := []byte{
0x00,
0x06, 0x00, 0x03, 'a', 'b', 'c',
0x01,
0x0a, 0x01, 0x02, 'z', 'z',
0x06, 0x03, 0x02, 'd', 'e',
}
frames, err := extractCryptoFrames(bytes.NewReader(payload))
if err != nil {
t.Fatalf("extractCryptoFrames() error = %v", err)
}
if len(frames) != 2 {
t.Fatalf("extractCryptoFrames() len = %d, want 2", len(frames))
}
if frames[0].Offset != 0 || string(frames[0].Data) != "abc" {
t.Fatalf("frame0 = %+v, want offset=0 data=abc", frames[0])
}
if frames[1].Offset != 3 || string(frames[1].Data) != "de" {
t.Fatalf("frame1 = %+v, want offset=3 data=de", frames[1])
}
}
func TestExtractCryptoFrames_UnsupportedFrame(t *testing.T) {
// 0x20 is currently unsupported in this parser.
_, err := extractCryptoFrames(bytes.NewReader([]byte{0x20}))
if err == nil {
t.Fatal("extractCryptoFrames() error = nil, want non-nil")
}
}