Skip to content

Commit

Permalink
update: add documentation to newly added contents
Browse files Browse the repository at this point in the history
Signed-off-by: Gaukas Wang <[email protected]>
  • Loading branch information
gaukas committed Jun 3, 2024
1 parent 3f06ffa commit fd645b2
Show file tree
Hide file tree
Showing 11 changed files with 49 additions and 10 deletions.
3 changes: 2 additions & 1 deletion clienthello.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"golang.org/x/crypto/cryptobyte"
)

// ClientHello represents a captured ClientHello message with all fingerprintable fields.
type ClientHello struct {
raw []byte

Expand Down Expand Up @@ -53,7 +54,7 @@ type ClientHello struct {
lengthPrefixedCertCompressAlgos []uint8
keyshareGroupsWithLengths []uint16

// QUIC-only
// QUIC-only, nil if not QUIC
qtp *QUICTransportParameters
}

Expand Down
12 changes: 5 additions & 7 deletions fingerprint_hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/binary"
"encoding/hex"
"hash"
"sort"

"github.com/gaukas/clienthellod/internal/utils"
)
Expand All @@ -23,16 +22,17 @@ func updateU64(h hash.Hash, i uint64) {
binary.Write(h, binary.BigEndian, i)
}

// FingerprintID is the type of fingerprint ID.
type FingerprintID int64

// AsHex returns the hex representation of this fingerprint ID.
func (id FingerprintID) AsHex() string {
hid := make([]byte, 8)
binary.BigEndian.PutUint64(hid, uint64(id))
return hex.EncodeToString(hid)
}

// FingerprintNID calculates fingerprint Numeric ID of ClientHello.
// Fingerprint is defined by
// calcNumericID returns the numeric ID of this client hello.
func (ch *ClientHello) calcNumericID() (orig, norm int64) {
for _, normalized := range []bool{false, true} {
h := sha1.New() // skipcq: GO-S1025, GSC-G401,
Expand Down Expand Up @@ -66,6 +66,7 @@ func (ch *ClientHello) calcNumericID() (orig, norm int64) {
return
}

// calcNumericID returns the numeric ID of this gathered client initial.
func (gci *GatheredClientInitials) calcNumericID() uint64 {
h := sha1.New() // skipcq: GO-S1025, GSC-G401
updateArr(h, gci.Packets[0].Header.Version)
Expand All @@ -79,9 +80,6 @@ func (gci *GatheredClientInitials) calcNumericID() uint64 {
allFrameIDs = append(allFrameIDs, p.frames.FrameTypesUint8()...)
}
dedupAllFrameIDs := utils.DedupIntArr(allFrameIDs)
sort.Slice(dedupAllFrameIDs, func(i, j int) bool {
return dedupAllFrameIDs[i] < dedupAllFrameIDs[j]
})
updateArr(h, dedupAllFrameIDs)

if gci.Packets[0].Header.HasToken {
Expand All @@ -93,7 +91,7 @@ func (gci *GatheredClientInitials) calcNumericID() uint64 {
return binary.BigEndian.Uint64(h.Sum(nil)[0:8])
}

// NID returns the numeric ID of this transport parameters combination.
// calcNumericID returns the numeric ID of this transport parameters combination.
func (qtp *QUICTransportParameters) calcNumericID() uint64 {
h := sha1.New() // skipcq: GO-S1025, GSC-G401
updateArr(h, qtp.MaxIdleTimeout)
Expand Down
5 changes: 3 additions & 2 deletions internal/utils/arr_dedup.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package utils

import "sort"

type SliceType interface {
type SliceIntType interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

func DedupIntArr[T SliceType](arr []T) []T {
// DedupIntArr eliminates the duplicates in an integer array.
func DedupIntArr[T SliceIntType](arr []T) []T {
// Sort the array
sort.Slice(arr, func(i, j int) bool { return arr[i] < arr[j] })

Expand Down
2 changes: 2 additions & 0 deletions quic_client_initial.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"
)

// ClientInitial represents a QUIC Initial Packet sent by the Client.
type ClientInitial struct {
Header *QUICHeader `json:"header,omitempty"` // QUIC header
FrameTypes []uint64 `json:"frames,omitempty"` // frames ID in order
Expand Down Expand Up @@ -106,6 +107,7 @@ func GatherClientInitials() *GatheredClientInitials {
return gci
}

// GatherClientInitialsWithDeadline is a helper function to create a GatheredClientInitials with a deadline.
func GatherClientInitialsWithDeadline(deadline time.Time) *GatheredClientInitials {
gci := GatherClientInitials()
gci.SetDeadline(deadline)
Expand Down
1 change: 1 addition & 0 deletions quic_clienthello.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
)

// QUICClientHello represents a QUIC ClientHello.
type QUICClientHello struct {
ClientHello
}
Expand Down
4 changes: 4 additions & 0 deletions quic_clienthello_reconstructor.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import (
"runtime"
)

// QUICClientHello can be used to parse fragments of a QUIC ClientHello.
type QUICClientHelloReconstructor struct {
fullLen uint32 // parse from first fragment
buf []byte

frags map[uint64][]byte // offset: fragment, pending to be parsed
}

// NewQUICClientHelloReconstructor creates a new QUICClientHelloReconstructor.
func NewQUICClientHelloReconstructor() *QUICClientHelloReconstructor {
qchr := &QUICClientHelloReconstructor{
frags: make(map[uint64][]byte),
Expand Down Expand Up @@ -113,6 +115,7 @@ func (qchr *QUICClientHelloReconstructor) AddCRYPTOFragment(offset uint64, frag
return nil
}

// ReconstructAsBytes reassembles the ClientHello as bytes.
func (qchr *QUICClientHelloReconstructor) ReconstructAsBytes() []byte {
if qchr.fullLen == 0 {
return nil
Expand All @@ -123,6 +126,7 @@ func (qchr *QUICClientHelloReconstructor) ReconstructAsBytes() []byte {
}
}

// Reconstruct reassembles the ClientHello as a QUICClientHello struct.
func (qchr *QUICClientHelloReconstructor) Reconstruct() (*QUICClientHello, error) {
if b := qchr.ReconstructAsBytes(); len(b) > 0 {
return ParseQUICClientHello(b)
Expand Down
2 changes: 2 additions & 0 deletions quic_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func ReadNextVLI(r io.Reader) (val uint64, n int, err error) {
return
}

// DecodeVLI decodes a variable-length integer from the given byte slice.
func DecodeVLI(vli []byte) (val uint64, err error) {
var n int
val, n, err = ReadNextVLI(bytes.NewReader(vli))
Expand All @@ -74,6 +75,7 @@ func unsetVLIBits(vli []byte) {
}
}

// IsGREASETransportParameter checks if the given transport parameter type is a GREASE value.
func IsGREASETransportParameter(paramType uint64) bool {
return paramType >= 27 && (paramType-27)%31 == 0 // reserved values are 27, 58, 89, ...
}
Expand Down
3 changes: 3 additions & 0 deletions quic_crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"golang.org/x/crypto/hkdf"
)

// ClientInitialKeysCalc calculates the client key, IV and header protection key from the initial random.
func ClientInitialKeysCalc(initialRandom []byte) (clientKey, clientIV, clientHpKey []byte, err error) {
initialSalt := []byte{
0x38, 0x76, 0x2c, 0xf7,
Expand Down Expand Up @@ -70,6 +71,7 @@ func hkdfExpandLabel(key []byte, label string, context []byte, length uint16) ([
return out, nil
}

// ComputeHeaderProtection computes the header protection for the client.
func ComputeHeaderProtection(clientHpKey, sample []byte) ([]byte, error) {
if len(clientHpKey) != 16 || len(sample) != 16 {
panic("invalid input")
Expand All @@ -87,6 +89,7 @@ func ComputeHeaderProtection(clientHpKey, sample []byte) ([]byte, error) {
return headerProtection[:5], nil
}

// DecryptAES128GCM decrypts the AES-128-GCM encrypted data.
func DecryptAES128GCM(iv []byte, recordNum uint64, key, ciphertext, recdata, authtag []byte) (plaintext []byte, err error) {
buildIV(iv, recordNum)

Expand Down
12 changes: 12 additions & 0 deletions quic_fingerprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/gaukas/clienthellod/internal/utils"
)

// QUICFingerprint can be used to generate a fingerprint of a QUIC connection.
type QUICFingerprint struct {
ClientInitials *GatheredClientInitials

Expand All @@ -23,6 +24,7 @@ type QUICFingerprint struct {
UserAgent string `json:"user_agent,omitempty"` // User-Agent header, set by the caller
}

// GenerateQUICFingerprint generates a QUICFingerprint from the gathered ClientInitials.
func GenerateQUICFingerprint(gci *GatheredClientInitials) (*QUICFingerprint, error) {
if err := gci.Wait(); err != nil {
return nil, err // GatheringClientInitials failed (expired before complete)
Expand Down Expand Up @@ -51,20 +53,23 @@ func GenerateQUICFingerprint(gci *GatheredClientInitials) (*QUICFingerprint, err

const DEFAULT_QUICFINGERPRINT_EXPIRY = 10 * time.Second

// QUICFingerprinter can be used to fingerprint QUIC connections.
type QUICFingerprinter struct {
mapGatheringClientInitials *sync.Map

timeout time.Duration
closed atomic.Bool
}

// NewQUICFingerprinter creates a new QUICFingerprinter.
func NewQUICFingerprinter() *QUICFingerprinter {
return &QUICFingerprinter{
mapGatheringClientInitials: new(sync.Map),
closed: atomic.Bool{},
}
}

// NewQUICFingerprinterWithTimeout creates a new QUICFingerprinter with a timeout.
func NewQUICFingerprinterWithTimeout(timeout time.Duration) *QUICFingerprinter {
return &QUICFingerprinter{
mapGatheringClientInitials: new(sync.Map),
Expand All @@ -73,10 +78,12 @@ func NewQUICFingerprinterWithTimeout(timeout time.Duration) *QUICFingerprinter {
}
}

// SetTimeout sets the timeout for gathering ClientInitials.
func (qfp *QUICFingerprinter) SetTimeout(timeout time.Duration) {
qfp.timeout = timeout
}

// HandlePacket handles a QUIC packet.
func (qfp *QUICFingerprinter) HandlePacket(from string, p []byte) error {
if qfp.closed.Load() {
return errors.New("QUICFingerprinter closed")
Expand Down Expand Up @@ -120,6 +127,7 @@ func (qfp *QUICFingerprinter) HandlePacket(from string, p []byte) error {
return gci.AddPacket(ci)
}

// HandleUDPConn handles a QUIC connection over UDP.
func (qfp *QUICFingerprinter) HandleUDPConn(pc net.PacketConn) error {
var buf [2048]byte
for {
Expand All @@ -139,6 +147,7 @@ func (qfp *QUICFingerprinter) HandleUDPConn(pc net.PacketConn) error {
}
}

// HandleIPConn handles a QUIC connection over IP.
func (qfp *QUICFingerprinter) HandleIPConn(ipc *net.IPConn) error {
var buf [2048]byte
for {
Expand Down Expand Up @@ -167,6 +176,7 @@ func (qfp *QUICFingerprinter) HandleIPConn(ipc *net.IPConn) error {
}
}

// Lookup looks up a QUICFingerprint for a given key.
func (qfp *QUICFingerprinter) Lookup(from string) *QUICFingerprint {
gci, ok := qfp.mapGatheringClientInitials.Load(from) // when using LoadAndDelete, some implementations "wasting" QUIC connections will fail
if !ok {
Expand All @@ -190,6 +200,7 @@ func (qfp *QUICFingerprinter) Lookup(from string) *QUICFingerprint {
return qf
}

// LookupAwait looks up a QUICFingerprint for a given key, waiting for the gathering to complete.
func (qfp *QUICFingerprinter) LookupAwait(from string) (*QUICFingerprint, error) {
gci, ok := qfp.mapGatheringClientInitials.Load(from) // when using LoadAndDelete, some implementations "wasting" QUIC connections will fail
if !ok {
Expand All @@ -209,6 +220,7 @@ func (qfp *QUICFingerprinter) LookupAwait(from string) (*QUICFingerprint, error)
return qf, nil
}

// Close closes the QUICFingerprinter.
func (qfp *QUICFingerprinter) Close() {
qfp.closed.Store(true)
}
7 changes: 7 additions & 0 deletions quic_frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (
QUICFrame_CRYPTO uint64 = 6 // 6
)

// QUICFrame is the interface that wraps the basic methods of a QUIC frame.
type QUICFrame interface {
// FrameType returns the type of the frame.
FrameType() uint64
Expand All @@ -27,6 +28,7 @@ type QUICFrame interface {
ReadReader(io.Reader) (io.Reader, error)
}

// ReadAllFrames reads all QUIC frames from the input reader.
func ReadAllFrames(r io.Reader) ([]QUICFrame, error) {
var frames []QUICFrame = make([]QUICFrame, 0)

Expand Down Expand Up @@ -64,6 +66,8 @@ func ReadAllFrames(r io.Reader) ([]QUICFrame, error) {
}
}

// ReassembleCRYPTOFrames reassembles CRYPTO frames into a single byte slice that
// consists of the entire CRYPTO data.
func ReassembleCRYPTOFrames(frames []QUICFrame) ([]byte, error) {
var cryptoFrames []QUICFrame = make([]QUICFrame, 0)

Expand Down Expand Up @@ -96,8 +100,10 @@ func ReassembleCRYPTOFrames(frames []QUICFrame) ([]byte, error) {
return reassembled, nil
}

// QUICFrames is a slice of QUICFrame.
type QUICFrames []QUICFrame

// FrameTypes returns the frame types of all QUIC frames.
func (qfs QUICFrames) FrameTypes() []uint64 {
var frameTypes []uint64 = make([]uint64, 0)

Expand All @@ -108,6 +114,7 @@ func (qfs QUICFrames) FrameTypes() []uint64 {
return frameTypes
}

// FrameTypesUint8 returns the frame types of all QUIC frames as uint8.
func (qfs QUICFrames) FrameTypesUint8() []uint8 {
var frameTypesUint8 []uint8 = make([]uint8, 0)

Expand Down
8 changes: 8 additions & 0 deletions tls_fingerprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,23 @@ import (

const DEFAULT_TLSFINGERPRINT_EXPIRY = 10 * time.Second

// TLSFingerprinter can be used to fingerprint TLS connections.
type TLSFingerprinter struct {
mapClientHellos *sync.Map

timeout time.Duration
closed atomic.Bool
}

// NewTLSFingerprinter creates a new TLSFingerprinter.
func NewTLSFingerprinter() *TLSFingerprinter {
return &TLSFingerprinter{
mapClientHellos: new(sync.Map),
closed: atomic.Bool{},
}
}

// NewTLSFingerprinterWithTimeout creates a new TLSFingerprinter with a timeout.
func NewTLSFingerprinterWithTimeout(timeout time.Duration) *TLSFingerprinter {
return &TLSFingerprinter{
mapClientHellos: new(sync.Map),
Expand All @@ -35,10 +38,12 @@ func NewTLSFingerprinterWithTimeout(timeout time.Duration) *TLSFingerprinter {
}
}

// SetTimeout sets the timeout for the TLSFingerprinter.
func (tfp *TLSFingerprinter) SetTimeout(timeout time.Duration) {
tfp.timeout = timeout
}

// HandleMessage handles a message.
func (tfp *TLSFingerprinter) HandleMessage(from string, p []byte) error {
if tfp.closed.Load() {
return errors.New("TLSFingerprinter closed")
Expand All @@ -62,6 +67,7 @@ func (tfp *TLSFingerprinter) HandleMessage(from string, p []byte) error {
return nil
}

// HandleTCPConn handles a TCP connection.
func (tfp *TLSFingerprinter) HandleTCPConn(conn net.Conn) (rewindConn net.Conn, err error) {
if tfp.closed.Load() {
return nil, errors.New("TLSFingerprinter closed")
Expand Down Expand Up @@ -89,6 +95,7 @@ func (tfp *TLSFingerprinter) HandleTCPConn(conn net.Conn) (rewindConn net.Conn,
return utils.RewindConn(conn, ch.Raw())
}

// Lookup looks up a ClientHello.
func (tfp *TLSFingerprinter) Lookup(from string) *ClientHello {
ch, ok := tfp.mapClientHellos.LoadAndDelete(from)
if !ok {
Expand All @@ -103,6 +110,7 @@ func (tfp *TLSFingerprinter) Lookup(from string) *ClientHello {
return clientHello
}

// Close closes the TLSFingerprinter.
func (tfp *TLSFingerprinter) Close() {
tfp.closed.Store(true)
}

0 comments on commit fd645b2

Please sign in to comment.