refactor: allow to pass in multiple packets to a quic sniffer

This commit is contained in:
mzz2017 2023-08-27 00:24:18 +08:00
parent d76919fc62
commit 2af52bb8bd
12 changed files with 326 additions and 366 deletions

View File

@ -17,10 +17,10 @@ type ConnSniffer struct {
*Sniffer
}
func NewConnSniffer(conn net.Conn, snifferBufSize int, dataWaitingTimeout time.Duration) *ConnSniffer {
func NewConnSniffer(conn net.Conn, snifferBufSize int, timeout time.Duration) *ConnSniffer {
s := &ConnSniffer{
Conn: conn,
Sniffer: NewStreamSniffer(conn, snifferBufSize, dataWaitingTimeout),
Sniffer: NewStreamSniffer(conn, snifferBufSize, timeout),
}
return s
}

View File

@ -8,19 +8,20 @@ package sniffing
import (
"bufio"
"bytes"
"github.com/daeuniverse/dae/common"
"strings"
"unicode"
"github.com/daeuniverse/dae/common"
)
func (s *Sniffer) SniffHttp() (d string, err error) {
// First byte should be printable.
if len(s.buf) == 0 || !unicode.IsPrint(rune(s.buf[0])) {
if s.buf.Len() == 0 || !unicode.IsPrint(rune(s.buf.Bytes()[0])) {
return "", NotApplicableError
}
// Search method.
search := s.buf
search := s.buf.Bytes()
if len(search) > 12 {
search = search[:12]
}
@ -35,7 +36,7 @@ func (s *Sniffer) SniffHttp() (d string, err error) {
// Now we assume it is an HTTP packet. We should not return NotApplicableError after here.
// Search Host.
scanner := bufio.NewScanner(bytes.NewReader(s.buf))
scanner := bufio.NewScanner(bytes.NewReader(s.buf.Bytes()))
// \r\n
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {

View File

@ -10,10 +10,11 @@ import (
"crypto/cipher"
"crypto/sha256"
"encoding/binary"
"io"
"github.com/daeuniverse/dae/common"
"github.com/daeuniverse/softwind/pool"
"golang.org/x/crypto/hkdf"
"io"
)
const (
@ -113,7 +114,7 @@ func (k *Keys) HeaderProtection_(sample []byte, longHeader bool, firstByte *byte
return packetNumber, nil
}
func (k *Keys) PayloadDecryptFromPool(ciphertext []byte, packetNumber []byte, header []byte) (plaintext []byte, err error) {
func (k *Keys) PayloadDecrypt(ciphertext []byte, packetNumber []byte, header []byte) (plaintext []byte, err error) {
// https://datatracker.ietf.org/doc/html/rfc9001#name-initial-secrets
aead, err := k.newAead(k.key)
@ -125,15 +126,15 @@ func (k *Keys) PayloadDecryptFromPool(ciphertext []byte, packetNumber []byte, he
for i := range packetNumber {
k.iv[len(k.iv)-len(packetNumber)+i] ^= packetNumber[i]
}
plaintext = pool.Get(len(ciphertext) - aead.Overhead())
plaintext = make([]byte, len(ciphertext)-aead.Overhead())
plaintext, err = aead.Open(plaintext[:0], k.iv, ciphertext, header)
if err != nil {
pool.Put(plaintext)
// Do nothing.
}
return plaintext, nil
}
func DecryptQuicFromPool_(header []byte, blockEnd int, destConnId []byte) (plaintext []byte, err error) {
func DecryptQuic_(header []byte, blockEnd int, destConnId []byte) (plaintext []byte, err error) {
_version := binary.BigEndian.Uint32(header[1:])
version, err := ParseVersion(_version)
if err != nil {
@ -158,7 +159,7 @@ func DecryptQuicFromPool_(header []byte, blockEnd int, destConnId []byte) (plain
header = header[:len(header)-MaxPacketNumberLength+len(packetNumber)] // Correct header
payload := header[len(header):blockEnd] // Correct payload
plaintext, err = keys.PayloadDecryptFromPool(payload, packetNumber, header)
plaintext, err = keys.PayloadDecrypt(payload, packetNumber, header)
if err != nil {
return nil, err
}

View File

@ -89,7 +89,7 @@ func TestKeys_PayloadDecrypt_(t *testing.T) {
}
header = data[:len(header)-4+len(packetNumber)]
payload := data[len(header):]
plaintext, err := keys.PayloadDecryptFromPool(payload, packetNumber, header)
plaintext, err := keys.PayloadDecrypt(payload, packetNumber, header)
if err != nil {
t.Fatal("PayloadDecryptFromPool:", err)
}

View File

@ -9,8 +9,6 @@ import (
"fmt"
"io/fs"
"sort"
"github.com/daeuniverse/softwind/pool"
)
var (
@ -32,69 +30,50 @@ type CryptoFrameOffset struct {
Data []byte
}
type CryptoFrameRelocation struct {
payload []byte
o []*CryptoFrameOffset
length int
}
func NewCryptoFrameRelocation(plaintextPayload []byte) (cryptoRelocation *CryptoFrameRelocation, err error) {
var frameSize int
var offset *CryptoFrameOffset
cryptoRelocation = &CryptoFrameRelocation{
payload: plaintextPayload,
o: nil,
}
// Extract crypto frames.
for iNextFrame := 0; iNextFrame < len(plaintextPayload); iNextFrame += frameSize {
offset, frameSize, err = ExtractCryptoFrameOffset(plaintextPayload[iNextFrame:], iNextFrame)
if err != nil {
return nil, err
}
if offset == nil {
continue
}
cryptoRelocation.o = append(cryptoRelocation.o, offset)
}
// Sort offsets by UpperAppOffset.
sort.Slice(cryptoRelocation.o, func(i, j int) bool {
return cryptoRelocation.o[i].UpperAppOffset < cryptoRelocation.o[j].UpperAppOffset
})
// Store length.
left := cryptoRelocation.o[0]
right := cryptoRelocation.o[len(cryptoRelocation.o)-1]
cryptoRelocation.length = right.UpperAppOffset + len(right.Data) - left.UpperAppOffset
return cryptoRelocation, nil
}
func ReassembleCryptoToBytesFromPool(plaintextPayload []byte) (b []byte, err error) {
func ReassembleCryptos(offsets []*CryptoFrameOffset, newPayload []byte) (newOffsets []*CryptoFrameOffset, err error) {
oldLen := len(offsets)
var frameSize int
var offset *CryptoFrameOffset
var boundary int
b = pool.Get(len(plaintextPayload))
// Extract crypto frames.
for iNextFrame := 0; iNextFrame < len(plaintextPayload); iNextFrame += frameSize {
offset, frameSize, err = ExtractCryptoFrameOffset(plaintextPayload[iNextFrame:], iNextFrame)
for iNextFrame := 0; iNextFrame < len(newPayload); iNextFrame += frameSize {
offset, frameSize, err = ExtractCryptoFrameOffset(newPayload[iNextFrame:], iNextFrame)
if err != nil {
pool.Put(b)
return nil, err
}
if offset == nil {
continue
}
if offset.UpperAppOffset+len(offset.Data) >= len(b) {
return nil, fmt.Errorf("offset.UpperAppOffset out of bound: %v:%v/%v", offset.UpperAppOffset, offset.UpperAppOffset+len(offset.Data), len(b))
}
copy(b[offset.UpperAppOffset:], offset.Data)
offsets = append(offsets, offset)
if offset.UpperAppOffset+len(offset.Data) > boundary {
boundary = offset.UpperAppOffset + len(offset.Data)
}
}
return b[:boundary], nil
// Sort the new part.
newPart := offsets[oldLen:]
sort.Slice(newPart, func(i, j int) bool {
return newPart[i].UpperAppOffset < newPart[j].UpperAppOffset
})
// Insertion sort.
for i := oldLen; i < len(offsets); i++ {
item := offsets[i]
j := i - 1
for ; j >= 0; j-- {
if item.UpperAppOffset < offsets[j].UpperAppOffset {
offsets[j+1] = offsets[j]
} else {
if offsets[j+1] != item {
offsets[j+1] = item
}
break
}
}
if j < 0 {
offsets[0] = item
}
}
return offsets, nil
}
func ExtractCryptoFrameOffset(remainder []byte, transportOffset int) (offset *CryptoFrameOffset, frameSize int, err error) {
@ -136,108 +115,16 @@ func ExtractCryptoFrameOffset(remainder []byte, transportOffset int) (offset *Cr
}
}
func (r *CryptoFrameRelocation) BinarySearch(iUpper int, leftOuter, rightOuter int) (iOuter int, iInner int, err error) {
rightOuterInstance := r.o[rightOuter]
if iUpper < r.o[leftOuter].UpperAppOffset || iUpper >= rightOuterInstance.UpperAppOffset+len(rightOuterInstance.Data) {
return 0, 0, fmt.Errorf("%w: %v is not in [%v, %v)", OutOfRangeError, iUpper, r.o[leftOuter].UpperAppOffset, rightOuterInstance.UpperAppOffset+len(rightOuterInstance.Data))
}
for leftOuter < rightOuter {
mid := leftOuter + ((rightOuter - leftOuter) >> 1)
if iUpper < r.o[mid].UpperAppOffset {
rightOuter = mid - 1
} else if iUpper >= r.o[mid].UpperAppOffset {
if iUpper < r.o[mid].UpperAppOffset+len(r.o[mid].Data) {
return mid, iUpper - r.o[mid].UpperAppOffset, nil
} else {
leftOuter = mid + 1
}
}
}
return leftOuter, iUpper - r.o[leftOuter].UpperAppOffset, nil
}
func (r *CryptoFrameRelocation) BytesFromPool() []byte {
if len(r.o) == 0 {
return pool.Get(0)
}
right := r.o[len(r.o)-1]
return r.copyBytesToPool(0, 0, len(r.o)-1, len(right.Data)-1, r.length)
}
// RangeFromPool copy bytes from iUpperAppOffset to jUpperAppOffset.
// It is not suggested to use it for large range and frequent copy.
func (r *CryptoFrameRelocation) RangeFromPool(i, j int) []byte {
if i > j {
panic(fmt.Sprintf("i > j: %v > %v", i, j))
}
// We find bytes including i and j, so we should sub j with 1.
j--
// Find i.
iOuter, iInner, err := r.BinarySearch(i, 0, len(r.o)-1)
if err != nil {
panic(err)
}
// Check if j and i is in the same outer or adjacent outers.
// It is very common because we usually have small access range.
var jOuter, jInner int
if iInner+j-i < len(r.o[iOuter].Data) {
jOuter = iOuter
jInner = iInner + j - i
} else if iOuter+1 < len(r.o) && j < r.o[iOuter+1].UpperAppOffset+len(r.o[iOuter+1].Data) {
jOuter = iOuter + 1
jInner = (j - i) + (len(r.o[iOuter].Data) - iInner)
} else {
// We have searched iOuter and iOuter+1
jOuter, jInner, err = r.BinarySearch(j, iOuter+2, len(r.o)-1)
if err != nil {
panic(err)
}
}
return r.copyBytesToPool(iOuter, iInner, jOuter, jInner, j-i+1)
}
// copyBytesToPool copy bytes including i and j.
func (r *CryptoFrameRelocation) copyBytesToPool(iOuter, iInner, jOuter, jInner, size int) []byte {
b := pool.Get(size)
// io := r.o[iOuter]
k := 0
for {
// Most accesses are small range accesses.
base := r.o[iOuter].Data
if iOuter == jOuter {
k += copy(b[k:], base[iInner:jInner+1])
if k != size {
panic("unmatched size")
}
return b
} else {
k += copy(b[k:], base[iInner:])
if iInner != 0 {
iInner = 0
}
iOuter++
}
}
}
func (r *CryptoFrameRelocation) At(i int) byte {
iOuter, iInner, err := r.BinarySearch(i, 0, len(r.o)-1)
if err != nil {
panic(err)
}
return r.o[iOuter].Data[iInner]
}
func (r *CryptoFrameRelocation) Len() int {
return r.length
}
var (
ErrMissingCrypto = fmt.Errorf("missing crypto frame")
)
type Locator interface {
Range(i, j int) []byte
Slice(i, j int) Locator
At(i int) byte
Range(i, j int) ([]byte, error)
Slice(i, j int) (Locator, error)
At(i int) (byte, error)
Len() int
Bytes() ([]byte, error)
}
// LinearLocator only searches forward and have no boundary check.
@ -248,95 +135,130 @@ type LinearLocator struct {
baseEnd int
baseStart int
baseData []byte
cfr *CryptoFrameRelocation
o []*CryptoFrameOffset
}
func NewLinearLocator(cfr *CryptoFrameRelocation) (linearLocator *LinearLocator) {
func NewLinearLocator(o []*CryptoFrameOffset) *LinearLocator {
if len(o) == 0 {
return &LinearLocator{}
}
return &LinearLocator{
left: 0,
length: cfr.length,
length: o[len(o)-1].UpperAppOffset + len(o[len(o)-1].Data),
iOuter: 0,
baseData: cfr.o[0].Data,
baseStart: cfr.o[0].UpperAppOffset,
baseEnd: cfr.o[0].UpperAppOffset + len(cfr.o[0].Data),
cfr: cfr,
baseData: o[0].Data,
baseStart: o[0].UpperAppOffset,
baseEnd: o[0].UpperAppOffset + len(o[0].Data),
o: o,
}
}
func (ll *LinearLocator) relocate(i int) {
func (l *LinearLocator) relocate(i int) error {
// Relocate ll.iOuter.
for i >= ll.baseEnd {
ll.iOuter++
ll.baseData = ll.cfr.o[ll.iOuter].Data
ll.baseStart = ll.cfr.o[ll.iOuter].UpperAppOffset
ll.baseEnd = ll.baseStart + len(ll.baseData)
for i >= l.baseEnd {
if l.iOuter+1 >= len(l.o) {
return ErrMissingCrypto
}
l.iOuter++
l.baseData = l.o[l.iOuter].Data
l.baseStart = l.o[l.iOuter].UpperAppOffset
l.baseEnd = l.baseStart + len(l.baseData)
}
if i < l.baseStart {
return ErrMissingCrypto
}
return nil
}
func (ll *LinearLocator) Range(i, j int) []byte {
func (l *LinearLocator) Range(i, j int) ([]byte, error) {
if i == j {
return []byte{}
return []byte{}, nil
}
if len(l.o) == 0 {
return nil, ErrMissingCrypto
}
size := j - i
// We find bytes including i and j, so we should sub j with 1.
i += ll.left
j += ll.left - 1
ll.relocate(i)
i += l.left
j += l.left - 1
if err := l.relocate(i); err != nil {
return nil, err
}
// Linearly copy.
if j < ll.baseEnd {
if j < l.baseEnd {
// In the same block, no copy needed.
return ll.baseData[i-ll.baseStart : j-ll.baseStart+1]
return l.baseData[i-l.baseStart : j-l.baseStart+1], nil
}
b := make([]byte, size)
k := 0
for j >= ll.baseEnd {
n := copy(b[k:], ll.baseData[i-ll.baseStart:])
for j >= l.baseEnd {
n := copy(b[k:], l.baseData[i-l.baseStart:])
k += n
i += n
ll.iOuter++
ll.baseData = ll.cfr.o[ll.iOuter].Data
ll.baseStart = ll.cfr.o[ll.iOuter].UpperAppOffset
ll.baseEnd = ll.baseStart + len(ll.baseData)
if l.iOuter+1 >= len(l.o) || l.o[l.iOuter].UpperAppOffset+len(l.o[l.iOuter+1].Data) != l.o[l.iOuter].UpperAppOffset {
// Some crypto is missing.
return nil, ErrMissingCrypto
}
l.iOuter++
l.baseData = l.o[l.iOuter].Data
l.baseStart = l.o[l.iOuter].UpperAppOffset
l.baseEnd = l.baseStart + len(l.baseData)
}
copy(b[k:], ll.baseData[i-ll.baseStart:j-ll.baseStart+1])
return b
copy(b[k:], l.baseData[i-l.baseStart:j-l.baseStart+1])
return b, nil
}
func (ll *LinearLocator) At(i int) byte {
i += ll.left
func (l *LinearLocator) At(i int) (byte, error) {
if len(l.o) == 0 {
return 0, ErrMissingCrypto
}
i += l.left
ll.relocate(i)
b := ll.baseData[i-ll.baseStart]
return b
if err := l.relocate(i); err != nil {
return 0, err
}
b := l.baseData[i-l.baseStart]
return b, nil
}
func (ll *LinearLocator) Slice(i, j int) Locator {
func (l *LinearLocator) Slice(i, j int) (Locator, error) {
// We do not care about right.
newLL := *ll
newLL := *l
newLL.left += i
newLL.length = j - i + 1
return &newLL
return &newLL, nil
}
func (ll *LinearLocator) Len() int {
return ll.length
func (l *LinearLocator) Bytes() ([]byte, error) {
return l.Range(0, l.length)
}
var _ Locator = &LinearLocator{}
func (l *LinearLocator) Len() int {
return l.length
}
type BuiltinBytesLocator []byte
func (l BuiltinBytesLocator) Range(i, j int) []byte {
return l[i:j]
func (l BuiltinBytesLocator) Range(i, j int) ([]byte, error) {
return l[i:j], nil
}
func (l BuiltinBytesLocator) At(i int) byte {
return l[i]
func (l BuiltinBytesLocator) At(i int) (byte, error) {
return l[i], nil
}
func (l BuiltinBytesLocator) Slice(i, j int) Locator {
return l[i:j]
func (l BuiltinBytesLocator) Slice(i, j int) (Locator, error) {
return l[i:j], nil
}
func (l BuiltinBytesLocator) Len() int {
return len(l)
}
func (l BuiltinBytesLocator) Bytes() ([]byte, error) {
return l, nil
}
var _ Locator = BuiltinBytesLocator{}

View File

@ -28,10 +28,6 @@ const (
QuicFlag_LongPacketType_Initial = 0
)
var (
QuicReassemble = QuicReassemblePolicy_ReassembleCryptoToBytesFromPool
)
type QuicReassemblePolicy int
const (
@ -41,50 +37,58 @@ const (
)
func (s *Sniffer) SniffQuic() (d string, err error) {
nextBlock := s.buf
nextBlock := s.buf.Bytes()
isQuic := false
for {
d, nextBlock, err = sniffQuicBlock(nextBlock)
if err == nil {
return d, nil
}
// If block is not a quic block, return it.
if errors.Is(err, NotApplicableError) {
// But if we have found quic block before, correct it.
if isQuic {
s.quicCryptos, nextBlock, err = sniffQuicBlock(s.quicCryptos, nextBlock)
if err != nil {
// If block is not a quic block, return it.
if errors.Is(err, NotApplicableError) {
// But if we have found quic block before, correct it.
if isQuic {
return "", NotFoundError
}
return "", err
}
if errors.Is(err, fs.ErrClosed) {
// ConnectionClose sniffed.
return "", NotFoundError
}
return "", err
}
if errors.Is(err, fs.ErrClosed) {
// ConnectionClose sniffed.
return "", NotFoundError
}
// Error is not NotApplicableError, should be quic block.
// Should be quic block.
isQuic = true
if len(nextBlock) == 0 {
return "", NotFoundError
break
}
}
// Is quic.
s.buf.Reset()
sni, err := extractSniFromTls(quicutils.NewLinearLocator(s.quicCryptos))
if err != nil {
s.needMore = true
return "", NotFoundError
}
return sni, nil
}
func sniffQuicBlock(buf []byte) (d string, next []byte, err error) {
func sniffQuicBlock(cryptos []*quicutils.CryptoFrameOffset, buf []byte) (new []*quicutils.CryptoFrameOffset, next []byte, err error) {
// QUIC: A UDP-Based Multiplexed and Secure Transport
// https://datatracker.ietf.org/doc/html/rfc9000#name-initial-packet
const dstConnIdPos = 6
boundary := dstConnIdPos
if len(buf) < boundary {
return "", nil, NotApplicableError
return nil, nil, NotApplicableError
}
// Check flag.
// Long header: 4 bits masked
// High 4 bits are not protected, so we can access QuicFlag_HeaderForm and QuicFlag_LongPacketType without decryption.
protectedFlag := buf[0]
if ((protectedFlag >> QuicFlag_HeaderForm) & 0b11) != QuicFlag_HeaderForm_LongHeader {
return "", nil, NotApplicableError
return nil, nil, NotApplicableError
}
if ((protectedFlag >> QuicFlag_LongPacketType) & 0b11) != QuicFlag_LongPacketType_Initial {
return "", nil, NotApplicableError
return nil, nil, NotApplicableError
}
// Skip version.
@ -92,37 +96,37 @@ func sniffQuicBlock(buf []byte) (d string, next []byte, err error) {
destConnIdLength := int(buf[boundary-1])
boundary += destConnIdLength + 1 // +1 because next field has 1B length
if len(buf) < boundary {
return "", nil, NotApplicableError
return nil, nil, NotApplicableError
}
destConnId := buf[dstConnIdPos : dstConnIdPos+destConnIdLength]
srcConnIdLength := int(buf[boundary-1])
boundary += srcConnIdLength + quicutils.MaxVarintLen64 // The next fields may have quic.MaxVarintLen64 bytes length
if len(buf) < boundary {
return "", nil, NotApplicableError
return nil, nil, NotApplicableError
}
tokenLength, n, err := quicutils.BigEndianUvarint(buf[boundary-quicutils.MaxVarintLen64:])
if err != nil {
return "", nil, NotApplicableError
return nil, nil, NotApplicableError
}
boundary = boundary - quicutils.MaxVarintLen64 + n // Correct boundary.
boundary += int(tokenLength) + quicutils.MaxVarintLen64 // Next fields may have quic.MaxVarintLen64 bytes length
if len(buf) < boundary {
return "", nil, NotApplicableError
return nil, nil, NotApplicableError
}
// https://datatracker.ietf.org/doc/html/rfc9000#name-variable-length-integer-enc
length, n, err := quicutils.BigEndianUvarint(buf[boundary-quicutils.MaxVarintLen64:])
if err != nil {
return "", nil, NotApplicableError
return nil, nil, NotApplicableError
}
boundary = boundary - quicutils.MaxVarintLen64 + n // Correct boundary.
blockEnd := boundary + int(length)
if len(buf) < blockEnd {
return "", nil, NotApplicableError
return nil, nil, NotApplicableError
}
boundary += quicutils.MaxPacketNumberLength
if len(buf) < boundary {
return "", nil, NotApplicableError
return nil, nil, NotApplicableError
}
header := buf[:boundary]
// Decrypt protected Packets.
@ -138,55 +142,18 @@ func sniffQuicBlock(buf []byte) (d string, next []byte, err error) {
copy(header[boundary-quicutils.MaxPacketNumberLength:], rawPacketNumber)
pool.Put(rawPacketNumber)
}()
plaintext, err := quicutils.DecryptQuicFromPool_(header, blockEnd, destConnId)
plaintext, err := quicutils.DecryptQuic_(header, blockEnd, destConnId)
if err != nil {
return "", nil, NotApplicableError
return nil, nil, NotApplicableError
}
defer pool.Put(plaintext)
// Now, we confirm it is exact a quic frame.
// After here, we should not return NotApplicableError.
// And we should return nextFrame.
if d, err = extractSniFromQuicPayload(plaintext); err != nil {
if new, err = quicutils.ReassembleCryptos(cryptos, plaintext); err != nil {
if errors.Is(err, fs.ErrClosed) {
return "", nil, err
return nil, nil, err
}
return "", buf[blockEnd:], NotFoundError
return nil, buf[blockEnd:], NotFoundError
}
return d, buf[blockEnd:], nil
}
func extractSniFromQuicPayload(payload []byte) (sni string, err error) {
// One payload may have multiple frames.
// Reassemble Crypto frames.
// Choose locator.
var locator quicutils.Locator
switch QuicReassemble {
case QuicReassemblePolicy_LinearLocator:
relocation, err := quicutils.NewCryptoFrameRelocation(payload)
if err != nil {
return "", err
}
locator = quicutils.NewLinearLocator(relocation)
case QuicReassemblePolicy_Slow:
relocation, err := quicutils.NewCryptoFrameRelocation(payload)
if err != nil {
return "", err
}
b := relocation.BytesFromPool()
defer pool.Put(b)
locator = quicutils.BuiltinBytesLocator(b)
case QuicReassemblePolicy_ReassembleCryptoToBytesFromPool:
b, err := quicutils.ReassembleCryptoToBytesFromPool(payload)
if err != nil {
return "", err
}
defer pool.Put(b)
locator = quicutils.BuiltinBytesLocator(b)
}
sni, err = extractSniFromTls(locator)
if err == nil {
return sni, nil
}
return "", NotFoundError
return new, buf[blockEnd:], nil
}

View File

@ -8,6 +8,7 @@ package sniffing
import (
"encoding/hex"
"testing"
"time"
"github.com/sirupsen/logrus"
)
@ -16,41 +17,10 @@ var QuicStream, _ = hex.DecodeString("c00000000108d60451e5cb0f7050000044bc9acdca
//var QuicStream, _ = hex.DecodeString("c6ff00001d100d5a802c52bfee4d71f3770529a5c6871415ea0d6ef29709e829432a18eb50f3af09c81c75004127f234d23fca9370573fd78cd781f4057ce9940111f0ad20e03e894b232013d76e268299644b036ac4557f03fead23ece9b788b3bcff3492b376861a188d5905e5e07cb156b57d7419e66235bedd44e5e774e1476d344eff64bdb1604aa9755a1fd08d4597a03a205e490f4223ddb32af2fc4023bc6784bcf6622ded2a49bbb976dec36e3712e0016272207f462b93b5a70dc66463131d2375bbfc38ece9215119b0b53676d05d470dcce52460f76d284d8f23846cbb38fcaa7e07fa1d6dec390e2876aea21bbd188dca3fe96dfc8c9f99237564e3db587b240279f46613ccc46c84e1b246cf1536be8275075fa4e63f0750df54f0cfbae986811cf3493c1d6ea63a836f387d1a3a02ac158b433ead3fc2035987f1f9c65c71c2d31803320f7a1a978a1aee3e1a50")
func BenchmarkLinearLocator(b *testing.B) {
logrus.SetLevel(logrus.DebugLevel)
QuicReassemble = QuicReassemblePolicy_LinearLocator
for i := 0; i < b.N; i++ {
sniffer := NewPacketSniffer(QuicStream)
d, err := sniffer.SniffQuic()
if err != nil {
b.Fatal(err)
}
if d == "" {
b.Fatal(d)
}
}
}
func BenchmarkBuiltinSlow(b *testing.B) {
logrus.SetLevel(logrus.DebugLevel)
QuicReassemble = QuicReassemblePolicy_Slow
for i := 0; i < b.N; i++ {
sniffer := NewPacketSniffer(QuicStream)
d, err := sniffer.SniffQuic()
if err != nil {
b.Fatal(err)
}
if d == "" {
b.Fatal(d)
}
}
}
func BenchmarkReassembleCryptoToBytesFromPool(b *testing.B) {
logrus.SetLevel(logrus.DebugLevel)
QuicReassemble = QuicReassemblePolicy_ReassembleCryptoToBytesFromPool
for i := 0; i < b.N; i++ {
sniffer := NewPacketSniffer(QuicStream)
sniffer := NewPacketSniffer(QuicStream, 300*time.Millisecond)
d, err := sniffer.SniffQuic()
if err != nil {
b.Fatal(err)

View File

@ -0,0 +1,46 @@
package sniffing
import (
"encoding/hex"
"fmt"
"strings"
"testing"
"time"
"github.com/daeuniverse/dae/component/sniffing/internal/quicutils"
"github.com/sirupsen/logrus"
)
var QuicStream2_2, _ = hex.DecodeString("cc0000000108e8da6ed9f385c987000044d026f109c2764c22f0ea2656550ea03e832d0ed5113eff115f2a057f77655cf5bbbb69fc98f7f70a3f407e0d94f37960c5ba5bd95a2df75f6f25020c2f2f21ddf9db5266bb4293991d58efec945468a820c61b743ca4b73663c3adcda58dee75607c5465e255b58477069a928687789c18c2ccb53911a47d64b83d5b58398ee4fd58f4f88f78788d5594218730cab9db3bac2fbfb947f2cb4eafb5e2964fce361042c622dfa7130afaf0e9d391ffc3aba2f5ee2f5c4d0dfaae0d71db2b3d7fab6dbccbb63d7961ddab55711d5a1beacf00ce5a82030a2c79c4ea65a2762f3b8e5f8fec8f6963b1a42c0f8a8d863225b2d6e7a15e9758e43095459e3d7ff88dc276605452b10de95a8795fe9952eb0b1eb200465ca9b00f98e2c4ad6a2a2e2bff2e2430438241525e1d16d5423c2262134a97056b7e86d5eb7eb2ac546086a3b8d7a97bc2263fa9a8b46f4b7d31cad63762c17a653b89593434aecf7a5e8fc169cfb5aa4a47e78ee817e115feceb9b68b29da6e15c647b7528980fb7cdc7c9ca660871228d0367f030f658d19ddddefe55908a2ec4ef5f5d89ec5aebee33f88a116c2857f7d1a2fd98321f28468a93938da406a68e4e660f0668fe49118812d5264073f28a8aa800c5970ef3f6fb4f0e9e4e48510700a5465c92886c50f2c6af570075f29f6a80636171f73d91864583d2d199e39b18623ee0cb489b449838bd9f7cd67ccc3e38f1b5a3ce08814f979f94db45cdcfa39a475e3efc4847def8e8e4c707a88d2f486fc85e10910ab0f1bbeb40468af777ff2bb0e655f1a006cde0d2e2ae036dafe60f110e859543699e0c9aa47eefa53d792b3cbcfa11ea1d3b55d3629de0345517d47f4e4c801104b81710ad28cd8611e150a1fc32160cb784cfcfdd908052cd43969b27929013edd2b0f3cd914590a32b2f99d4fc88873838b6fa0ec1450adb95f395988998801e85319fa448925ba767e3191df2b5b0983990beb4127216c93291a94463b453a4972c9a974742b0b22c935f4235c350120b6cf8296fc6d3c2812f74a17acf334e3c34ff9988f980e0cfff737a8b1a03508f47d8bf3748fbb5bd5ad7f1f47120c3a33822612f3a614aae7fe536b73db814aa4aac4b685aa1e7357309cf921b931113624881ce764feeff3292d2d794c6fa76529f3da8e6327e8f28aafe8b675a80ae3f478c65f1bf8fd7f2b140fea130dfa55982f0b0fcd61b42c8b2ea27a2b8bb44511eb44c1416ac16698f0ddb739e3d773f2afdd35bcfed0ffd7966aa3e727f8f08d02cab8d034a7ae363e42c9089901ddee147c98a856df4e5dcfeeb2f72e9edb12da513f32d99e1c653f4503e9a7f7fee1f4724ce9d6d530485362d993cb3bc4faff683327a02aee6f004bd9f98a8a4841091d48f5cd27af46431c66e68007750be57361e293650a0ae9fc9fa82ddf4483663c9805dc6e4a9b43529c0b2267cc3c0fb9084378acbda4962150a73e0c1b5aef6e40538d2630d8dbc2b084f9a53079cc73484906b7ad4a5021f280baf276a01b0fcea57d5c4284364f4d795645fc7bd8bb7d00021af924b75829e8a936e153676a182803537a23c76fee7c881e8063751ca0f5a585481b9077e9593734f9997e78b79ba38f6e13a1b631106a2ceddafdf51110b8bf07ec9337024355088d0bb3de2d46a03d3e3e7362b8b815613e36d746e5a9992f8e62ad5257e5798bd49b1a62717f02151b75a18e051df1292191d4")
var QuicStream2_1, _ = hex.DecodeString("ce0000000108e8da6ed9f385c987000044d0f34f94dcc26b99261ea264742abe4e552a146e16e89e4b7ef0ab3d6f3a34227b59742e4ba83a1e18cea494d2f67e469be4a7ff01334b151e9b7ca63b53735008eecc1f5c618419982292eca5731bb163ba81c1300e0bb99f2536d89ab0faf2dbd37ebfdb3d71f7343296a2190914bda556b8f9ccf5219964eb3cd373966fcfaca8a4735fb59fbaf69bbbdfc3a81b11570bb81fd3f5ef780fb7036e0666b997b0f4ed3305b68eafa1a99b3c8a6a2142ad9fe1e6b0a0eade6ace92b57416d4bf68fa2e9295bfc22757b0542ce91c8af3f547ef0ad385788db230a50158a0009fd95a7e8ee6e0dd11d6f9a906cbe8117e85bd507cdbd8f1a5a6cabf2617de7227d1ae8a8c6086b8ec325df90c0e16b37b4ed0ce617a00c7598a21924a19aec1b08c31b69430b23eefbe555ca2433431d28a4ffec548e463e8e6363b6b4fe9b8477c686c393571273c30b2e1785261faa0fd6f560c12418b27cd0491e013db5a8b3294e01a46a6e4c6b52e32756ab4be6f4ebc886c0c472d63f117ce30115182a97f1308c7f28989ce301cabced825154b0f4fa3bf4a55ce2f384ff11d9cbc0460d69db363664f92dc014bdb771b9b1e1ab6672c6da71c90aa514dcdc3a4ce45298bf9e5a395ebac3dff2a738c4b4690ee06fdab572a277addac7035d94afe794df05da75a56c79c37f42de1d727dc65e3060d9331e2fc82de2d7cef6cb9ae46f648b9930593975c35960b24deb770d5ee4332f8f57a05503399ca7bfdf7207f66a0f73d6b53269a944d5a3043b225adddfdd29d20ea8f500bb09ea3bb724083dd29ea8839e8192c4360ba3c5a6db0d695af5d357d6c4ed94aa28305033629201689764189774bbd4f0ae41b878b8f29a0fe0e124075ea08c5054871506a05be2f90e9ec0c2db48c0780580312e9ff4071054386e4206841f575f7ca06c228f7ee11e2333d08652b9b4f0b97f473a46a3d79c4f9a3416fb20fdbd88cacfa36f06fe1d73618195c6f0bf759a77c6a16b7e271c6cdb672ea53f6edfac860fcaf03313564abde1f66bca441d844d289a9e1025711c284f2c7c805353f2a89e9aeb52e3f452e879f0fafcdc0b48a0676afcf617a85037d991762664f6db64847eff2308447c4e8ea6688838bb7237a5fdfe0f1695afaa0bbb821b0004585adf151b029bd3458e28ba49dfc17eef1d2dd14ccda88d0848d4cd36d33cc5bab173c2448785ec1bdabc8873c904b95d7847d1b89857f2c7e078c6e2eb96029aa91c077e0efcf7b2ed2f30c7abc12189627793c7870dc0e70342cc27402ee1d6dec5ceea0ca06159002ea14a20c63b85689ed1840f404e46cb83d91c5e02f3ed938462364d3349f689310234083f7044e4b338ac54bed94530640d684c9688651b915d8c8895ef0f05f376292871b589751ac5b233e3d85572bb0c11bbbe91cc49a4ef0422f2676a2f3cc62bc88dbb7acf03cb5e847e976bfca6a90b9cee743ea77be5472ef162ff101c6873043df94c53c252840fd6a2662018f0897a06cd215997d6050917876500796fef718957212c773c39d1c7b839931af1e7dfae6e2c1d2251e78896521bb35b20057bad77df85aaed90288c17edb081398815e47239aeb77293a02a61a5125109fc3953593233fa83c17770a815fad7831c1b8647c6089ec621ee774a12a714def498d4335d0bb8a4a6a3dddead8ddb1176f58218477d55317df88cd2ca5a06b72679cf2ff7253ebd76a5ed3")
func dumpCryptos(t *testing.T, cryptos []*quicutils.CryptoFrameOffset) {
var b strings.Builder
for _, c := range cryptos {
b.WriteString(fmt.Sprintf("Offset %v; length: %v:\n", c.UpperAppOffset, len(c.Data)))
b.WriteString(fmt.Sprintf("Dump:\n%v\n", hex.Dump(c.Data)))
}
t.Log(b.String())
}
func TestQuic(t *testing.T) {
logrus.SetLevel(logrus.DebugLevel)
sniffer := NewPacketSniffer(QuicStream2_1, 300*time.Millisecond)
d, err := sniffer.SniffQuic()
if err != nil {
if sniffer.NeedMore() {
sniffer.AppendData(QuicStream2_2)
d, err = sniffer.SniffQuic()
} else {
t.Fatal(err)
}
}
dumpCryptos(t, sniffer.quicCryptos)
if err != nil {
t.Fatal(err)
}
if d == "" {
t.Fatal("domain is empty")
}
t.Log(d)
}

View File

@ -6,49 +6,65 @@
package sniffing
import (
"bytes"
"context"
"io"
"sync"
"time"
"github.com/daeuniverse/dae/component/sniffing/internal/quicutils"
"github.com/daeuniverse/softwind/pool"
)
type Sniffer struct {
// Stream
stream bool
r io.Reader
dataReady chan struct{}
dataError error
dataWaitingTimeout time.Duration
stream bool
r io.Reader
dataReady chan struct{}
dataError error
// Common
buf []byte
bufAt int
readMu sync.Mutex
buf *bytes.Buffer
readMu sync.Mutex
needMore bool
ctx context.Context
cancel func()
// Quic
quicCryptos []*quicutils.CryptoFrameOffset
}
func NewStreamSniffer(r io.Reader, bufSize int, dataWaitingTimeout time.Duration) *Sniffer {
func NewStreamSniffer(r io.Reader, bufSize int, timeout time.Duration) *Sniffer {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
s := &Sniffer{
stream: true,
r: r,
buf: make([]byte, bufSize),
dataReady: make(chan struct{}),
dataWaitingTimeout: dataWaitingTimeout,
stream: true,
r: r,
buf: pool.GetBuffer(),
dataReady: make(chan struct{}),
ctx: ctx,
cancel: cancel,
}
return s
}
func NewPacketSniffer(data []byte) *Sniffer {
func NewPacketSniffer(data []byte, timeout time.Duration) *Sniffer {
buffer := pool.GetBuffer()
buffer.Write(data)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
s := &Sniffer{
stream: false,
r: nil,
buf: data,
buf: buffer,
dataReady: make(chan struct{}),
ctx: ctx,
cancel: cancel,
}
return s
}
type sniff func() (d string, err error)
func sniffGroup(sniffs []sniff) (d string, err error) {
func sniffGroup(sniffs ...sniff) (d string, err error) {
for _, sniffer := range sniffs {
d, err = sniffer()
if err == nil {
@ -66,8 +82,7 @@ func (s *Sniffer) SniffTcp() (d string, err error) {
defer s.readMu.Unlock()
if s.stream {
go func() {
n, err := s.r.Read(s.buf)
s.buf = s.buf[:n]
_, err := s.buf.ReadFrom(s.r)
if err != nil {
s.dataError = err
}
@ -80,22 +95,22 @@ func (s *Sniffer) SniffTcp() (d string, err error) {
if s.dataError != nil {
return "", s.dataError
}
case <-time.After(s.dataWaitingTimeout):
case <-s.ctx.Done():
return "", NotApplicableError
}
} else {
close(s.dataReady)
}
if len(s.buf) == 0 {
if s.buf.Len() == 0 {
return "", NotApplicableError
}
return sniffGroup([]sniff{
return sniffGroup(
// Most sniffable traffic is TLS, thus we sniff it first.
s.SniffTls,
s.SniffHttp,
})
)
}
func (s *Sniffer) SniffUdp() (d string, err error) {
@ -105,13 +120,21 @@ func (s *Sniffer) SniffUdp() (d string, err error) {
// Always ready.
close(s.dataReady)
if len(s.buf) == 0 {
if s.buf.Len() == 0 {
return "", NotApplicableError
}
return sniffGroup([]sniff{
return sniffGroup(
s.SniffQuic,
})
)
}
func (s *Sniffer) AppendData(data []byte) {
s.buf.Write(data)
}
func (s *Sniffer) NeedMore() bool {
return s.needMore
}
func (s *Sniffer) Read(p []byte) (n int, err error) {
@ -121,21 +144,13 @@ func (s *Sniffer) Read(p []byte) (n int, err error) {
defer s.readMu.Unlock()
if s.dataError != nil {
if s.bufAt < len(s.buf) {
n = copy(p, s.buf[s.bufAt:])
s.bufAt += n
}
n, _ = s.buf.Read(p)
return n, s.dataError
}
if s.bufAt < len(s.buf) {
if s.buf.Len() > 0 {
// Read buf first.
n = copy(p, s.buf[s.bufAt:])
s.bufAt += n
if s.bufAt >= len(s.buf) {
s.buf = nil
}
return n, nil
return s.buf.Read(p)
}
if !s.stream {
return 0, io.EOF
@ -144,5 +159,11 @@ func (s *Sniffer) Read(p []byte) (n int, err error) {
}
func (s *Sniffer) Close() (err error) {
select {
case <-s.ctx.Done():
default:
s.cancel()
pool.PutBuffer(s.buf)
}
return nil
}

View File

@ -30,16 +30,16 @@ func (s *Sniffer) SniffTls() (d string, err error) {
// The Transport Layer Security (TLS) Protocol Version 1.3
// https://www.rfc-editor.org/rfc/rfc8446#page-27
boundary := 5
if len(s.buf) < boundary {
if s.buf.Len() < boundary {
return "", NotApplicableError
}
if s.buf[0] != ContentType_HandShake || (!bytes.Equal(s.buf[1:3], Version_Tls1_0) && !bytes.Equal(s.buf[1:3], Version_Tls1_2)) {
if s.buf.Bytes()[0] != ContentType_HandShake || (!bytes.Equal(s.buf.Bytes()[1:3], Version_Tls1_0) && !bytes.Equal(s.buf.Bytes()[1:3], Version_Tls1_2)) {
return "", NotApplicableError
}
length := int(binary.BigEndian.Uint16(s.buf[3:5]))
search := s.buf[5:]
length := int(binary.BigEndian.Uint16(s.buf.Bytes()[3:5]))
search := s.buf.Bytes()[5:]
if len(search) < length {
return "", NotApplicableError
}
@ -53,7 +53,10 @@ func extractSniFromTls(search quicutils.Locator) (sni string, err error) {
}
// Transport Layer Security (TLS) Extensions: Extension Definitions
// https://www.rfc-editor.org/rfc/rfc6066#page-5
b := search.Range(0, 6)
b, err := search.Range(0, 6)
if err != nil {
return "", err
}
if b[0] != HandShakeType_Hello {
return "", NotApplicableError
}
@ -70,43 +73,62 @@ func extractSniFromTls(search quicutils.Locator) (sni string, err error) {
// Skip 32 bytes random.
sessionIdLength := search.At(boundary - 1)
sessionIdLength, err := search.At(boundary - 1)
if err != nil {
return "", err
}
boundary += int(sessionIdLength) + 2 // +2 because the next field has 2B length
if search.Len() < boundary || search.Len() < boundary {
return "", NotApplicableError
}
b = search.Range(boundary-2, boundary)
b, err = search.Range(boundary-2, boundary)
if err != nil {
return "", err
}
cipherSuiteLength := int(binary.BigEndian.Uint16(b))
boundary += int(cipherSuiteLength) + 1 // +1 because the next field has 1B length
if search.Len() < boundary || search.Len() < boundary {
return "", NotApplicableError
}
compressMethodsLength := search.At(boundary - 1)
compressMethodsLength, err := search.At(boundary - 1)
if err != nil {
return "", err
}
boundary += int(compressMethodsLength) + 2 // +2 because the next field has 2B length
if search.Len() < boundary || search.Len() < boundary {
return "", NotApplicableError
}
b = search.Range(boundary-2, boundary)
b, err = search.Range(boundary-2, boundary)
if err != nil {
return "", err
}
extensionsLength := int(binary.BigEndian.Uint16(b))
boundary += extensionsLength + 0 // +0 because our search ends
if search.Len() < boundary || search.Len() < boundary {
return "", NotApplicableError
}
// Search SNI
return findSniExtension(search.Slice(boundary-extensionsLength, boundary))
extensions, err := search.Slice(boundary-extensionsLength, boundary)
if err != nil {
return "", err
}
return findSniExtension(extensions)
}
func findSniExtension(search quicutils.Locator) (string, error) {
func findSniExtension(search quicutils.Locator) (d string, err error) {
i := 0
var b []byte
for {
if i+4 >= search.Len() {
return "", NotFoundError
}
b = search.Range(i, i+4)
b, err = search.Range(i, i+4)
if err != nil {
return "", err
}
typ := binary.BigEndian.Uint16(b)
extLength := int(binary.BigEndian.Uint16(b[2:]))
@ -115,14 +137,20 @@ func findSniExtension(search quicutils.Locator) (string, error) {
return "", NotApplicableError
}
if typ == TlsExtension_ServerName {
b = search.Range(i+4, i+6)
b, err = search.Range(i+4, i+6)
if err != nil {
return "", err
}
sniLen := int(binary.BigEndian.Uint16(b))
if extLength < sniLen+2 {
return "", NotApplicableError
}
// Search HostName type SNI.
for j, indicatorLen := i+6, 0; j+3 <= iNextField; j += indicatorLen {
b = search.Range(j, j+3)
b, err = search.Range(j, j+3)
if err != nil {
return "", err
}
indicatorLen = int(binary.BigEndian.Uint16(b[1:]))
if b[0] != TlsExtension_ServerNameType_HostName {
continue
@ -130,7 +158,10 @@ func findSniExtension(search quicutils.Locator) (string, error) {
if j+3+indicatorLen > iNextField {
return "", NotApplicableError
}
b = search.Range(j+3, j+3+indicatorLen)
b, err = search.Range(j+3, j+3+indicatorLen)
if err != nil {
return "", err
}
// An SNI value may not include a trailing dot.
// https://tools.ietf.org/html/rfc6066#section-3
// But we accept it here.

View File

@ -8,6 +8,7 @@ package sniffing
import (
"encoding/hex"
"testing"
"time"
"github.com/sirupsen/logrus"
)
@ -28,7 +29,7 @@ func TestSniffer_SniffTls(t *testing.T) {
}}
logrus.SetLevel(logrus.DebugLevel)
for _, test := range tests {
sniffer := NewPacketSniffer(test.Stream)
sniffer := NewPacketSniffer(test.Stream, 300*time.Millisecond)
d, err := sniffer.SniffTls()
if err != nil {
t.Fatal(err)

View File

@ -140,7 +140,7 @@ func (c *ControlPlane) handlePkt(lConn *net.UDPConn, data []byte, src, pktDst, r
isDns := dnsMessage != nil
if !isDns {
// Sniff Quic, ...
sniffer := sniffing.NewPacketSniffer(data)
sniffer := sniffing.NewPacketSniffer(data, c.sniffingTimeout)
defer sniffer.Close()
domain, err = sniffer.SniffUdp()
if err != nil && !sniffing.IsSniffingError(err) {