feat: add sniffing suite and dial_mode option (#16)

This commit is contained in:
mzz 2023-02-15 01:53:53 +08:00 committed by GitHub
parent 2a586e6341
commit ebdbf9a4a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1561 additions and 19 deletions

24
common/consts/control.go Normal file
View File

@ -0,0 +1,24 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package consts
import "fmt"
type DialMode string
const (
DialMode_Ip DialMode = "ip"
DialMode_Domain DialMode = "domain"
)
func ParseDialMode(mode string) (DialMode, error) {
switch mode {
case "ip", "domain":
return DialMode(mode), nil
default:
return "", fmt.Errorf("unsupported dial mode: %v", mode)
}
}

View File

@ -8,6 +8,7 @@ package consts
import (
internal "github.com/v2rayA/dae/pkg/ebpf_internal"
"strconv"
"strings"
)
const (
@ -84,10 +85,14 @@ func (i OutboundIndex) String() string {
case OutboundLogicalAnd:
return "<AND>"
default:
return strconv.Itoa(int(i))
return "<index: " + strconv.Itoa(int(i)) + ">"
}
}
func (i OutboundIndex) IsReserved() bool {
return !strings.HasPrefix(i.String(), "<index: ")
}
const (
MaxMatchSetLen = 32 * 3
)

View File

@ -6,10 +6,13 @@
package common
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"fmt"
"golang.org/x/net/dns/dnsmessage"
"net/netip"
"net/url"
"path/filepath"
@ -374,3 +377,19 @@ func ConvergeIp(addr netip.Addr) netip.Addr {
}
return addr
}
func NewGcm(key []byte) (cipher.AEAD, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return cipher.NewGCM(block)
}
func AddrToDnsType(addr netip.Addr) dnsmessage.Type {
if addr.Is4() {
return dnsmessage.TypeA
} else {
return dnsmessage.TypeAAAA
}
}

View File

@ -33,12 +33,16 @@ type NetworkType struct {
func (t *NetworkType) String() string {
if t.IsDns {
return string(t.L4Proto) + string(t.IpVersion) + "(DNS)"
return t.StringWithoutDns() + "(DNS)"
} else {
return string(t.L4Proto) + string(t.IpVersion)
return t.StringWithoutDns()
}
}
func (t *NetworkType) StringWithoutDns() string {
return string(t.L4Proto) + string(t.IpVersion)
}
type collection struct {
// AliveDialerSetSet uses reference counting.
AliveDialerSetSet AliveDialerSetSet

View File

@ -0,0 +1,49 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package sniffing
import (
"errors"
"net"
"strings"
"sync"
)
type ConnSniffer struct {
net.Conn
*Sniffer
mu sync.Mutex
}
func NewConnSniffer(conn net.Conn, snifferBufSize int) *ConnSniffer {
s := &ConnSniffer{
Conn: conn,
Sniffer: NewStreamSniffer(conn, snifferBufSize),
}
return s
}
func (s *ConnSniffer) Read(p []byte) (n int, err error) {
s.mu.Lock()
n, err = s.Sniffer.Read(p)
s.mu.Unlock()
return n, err
}
func (s *ConnSniffer) Close() (err error) {
var errs []string
if err = s.Sniffer.Close(); err != nil {
errs = append(errs, err.Error())
}
if err = s.Conn.Close(); err != nil {
errs = append(errs, err.Error())
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, "; "))
}
return nil
}

View File

@ -0,0 +1,35 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package sniffing
import "bytes"
func (s *Sniffer) SniffHttp() (d string, err error) {
search := s.buf
if len(search) > 20 {
search = search[:20]
}
method, _, found := bytes.Cut(search, []byte(" "))
if !found {
return "", NotApplicableError
}
switch string(method) {
case "GET", "POST", "PUT", "PATCH", "DELETE", "COPY", "HEAD", "OPTIONS", "LINK", "UNLINK", "PURGE", "LOCK", "UNLOCK", "PROPFIND":
default:
return "", NotApplicableError
}
search = s.buf
prefix := []byte("Host: ")
_, afterHostKey, found := bytes.Cut(search, prefix)
if !found {
return "", NotFoundError
}
host, _, found := bytes.Cut(afterHostKey, []byte("\r\n"))
if !found {
return "", NotFoundError
}
return string(host), nil
}

View File

@ -0,0 +1,25 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package quicutils
import "io"
// BigEndianUvarint decodes a uint64 from buf and returns that value and the
// number of bytes read (> 0).
func BigEndianUvarint(buf []byte) (uint64, int, error) {
if len(buf) == 0 {
return 0, 0, io.ErrUnexpectedEOF
}
length := 1 << (buf[0] >> 6)
if length == 0 {
return 0, 0, io.ErrUnexpectedEOF
}
x := uint64(buf[0] & 0x3f)
for i := 1; i < length; i++ {
x = x<<8 | uint64(buf[i])
}
return x, length, nil
}

View File

@ -0,0 +1,167 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package quicutils
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/binary"
"github.com/mzz2017/softwind/pool"
"github.com/v2rayA/dae/common"
"golang.org/x/crypto/hkdf"
"io"
)
const (
MaxVarintLen64 = 8
MaxPacketNumberLength = 4
SampleSize = 16
)
var (
InitialClientLabel = []byte("client in")
)
type Keys struct {
version Version
clientInitialSecret []byte
key []byte
iv []byte
headerProtectionKey []byte
newAead func(key []byte) (cipher.AEAD, error)
}
func (k *Keys) Close() error {
pool.Put(k.clientInitialSecret)
pool.Put(k.headerProtectionKey)
pool.Put(k.iv)
pool.Put(k.key)
return nil
}
func NewKeys(clientDstConnectionId []byte, version Version, newAead func(key []byte) (cipher.AEAD, error)) (keys *Keys, err error) {
// https://datatracker.ietf.org/doc/html/rfc9001#name-keys
initialSecret := hkdf.Extract(sha256.New, clientDstConnectionId, version.InitialSalt())
clientInitialSecret, err := HkdfExpandLabelFromPool(sha256.New, initialSecret, InitialClientLabel, nil, 32)
if err != nil {
return nil, err
}
keys = &Keys{
clientInitialSecret: clientInitialSecret,
version: version,
newAead: newAead,
}
// We differentiated a deriveKeys func is just for example test.
if err = keys.deriveKeys(); err != nil {
keys.Close()
return nil, err
}
return keys, nil
}
func (k *Keys) deriveKeys() (err error) {
k.key, err = HkdfExpandLabelFromPool(sha256.New, k.clientInitialSecret, k.version.KeyLabel(), nil, 16)
if err != nil {
return err
}
k.iv, err = HkdfExpandLabelFromPool(sha256.New, k.clientInitialSecret, k.version.IvLabel(), nil, 12)
if err != nil {
return err
}
k.headerProtectionKey, err = HkdfExpandLabelFromPool(sha256.New, k.clientInitialSecret, k.version.HpLabel(), nil, 16)
if err != nil {
return err
}
return nil
}
// HeaderProtection_ encrypt/decrypt firstByte and packetNumber in place.
func (k *Keys) HeaderProtection_(sample []byte, longHeader bool, firstByte *byte, potentialPacketNumber []byte) (packetNumber []byte, err error) {
block, err := aes.NewCipher(k.headerProtectionKey)
if err != nil {
return nil, err
}
// Get mask.
mask := pool.Get(block.BlockSize())
defer pool.Put(mask)
block.Encrypt(mask, sample)
// Encrypt/decrypt first byte.
if longHeader {
// Long header: 4 bits masked
// High 4 bits are not protected.
*firstByte ^= mask[0] & 0x0f
} else {
// Short header: 5 bits masked
// High 3 bits are not protected.
*firstByte ^= mask[0] & 0x1f
}
// The length of the Packet Number field is the value of this field plus one.
packetNumberLength := int((*firstByte & 0b11) + 1)
packetNumber = potentialPacketNumber[:packetNumberLength]
// Encrypt/decrypt packet number.
for i := range packetNumber {
packetNumber[i] ^= mask[1+i]
}
return packetNumber, nil
}
func (k *Keys) PayloadDecryptFromPool(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)
if err != nil {
return nil, err
}
// We only decrypt once, so we do not need to XOR it back.
// https://github.com/quic-go/qtls-go1-20/blob/e132a0e6cb45e20ac0b705454849a11d09ba5a54/cipher_suites.go#L496
for i := range packetNumber {
k.iv[len(k.iv)-len(packetNumber)+i] ^= packetNumber[i]
}
plaintext = pool.Get(len(ciphertext) - aead.Overhead())
plaintext, err = aead.Open(plaintext[:0], k.iv, ciphertext, header)
if err != nil {
pool.Put(plaintext)
}
return plaintext, nil
}
func DecryptQuicFromPool_(header []byte, blockEnd int, destConnId []byte) (plaintext []byte, err error) {
_version := binary.BigEndian.Uint32(header[1:])
version, err := ParseVersion(_version)
if err != nil {
return nil, err
}
keys, err := NewKeys(destConnId, version, common.NewGcm)
if err != nil {
return nil, err
}
defer keys.Close()
if blockEnd-len(header) < SampleSize {
return nil, io.ErrUnexpectedEOF
}
// Sample 16B
sample := header[len(header) : len(header)+SampleSize]
// Decrypt header flag and packet number.
var packetNumber []byte
if packetNumber, err = keys.HeaderProtection_(sample, true, &header[0], header[len(header)-MaxPacketNumberLength:]); err != nil {
return nil, err
}
header = header[:len(header)-MaxPacketNumberLength+len(packetNumber)] // Correct header
payload := header[len(header):blockEnd] // Correct payload
plaintext, err = keys.PayloadDecryptFromPool(payload, packetNumber, header)
if err != nil {
return nil, err
}
return plaintext, nil
}

View File

@ -0,0 +1,96 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package quicutils
import (
"bytes"
"encoding/hex"
"github.com/v2rayA/dae/common"
"testing"
)
var destConnId = []byte{0x83, 0x94, 0xc8, 0xf0, 0x3e, 0x51, 0x57, 0x08}
func TestDeriveKeys(t *testing.T) {
// https://datatracker.ietf.org/doc/html/rfc9001#name-keys
keys, err := NewKeys(destConnId, Version_V1, common.NewGcm)
if err != nil {
t.Fatal(err)
}
defer keys.Close()
t.Logf("%#v", keys)
clientInitialSecret, _ := hex.DecodeString("c00cf151ca5be075ed0ebfb5c80323c42d6b7db67881289af4008f1f6c357aea")
if !bytes.Equal(keys.clientInitialSecret, clientInitialSecret) {
t.Fatal("key")
}
key, _ := hex.DecodeString("1f369613dd76d5467730efcbe3b1a22d")
if !bytes.Equal(keys.key, key) {
t.Fatal("key")
}
iv, _ := hex.DecodeString("fa044b2f42a3fd3b46fb255c")
if !bytes.Equal(keys.iv, iv) {
t.Fatal("iv")
}
hp, _ := hex.DecodeString("9f50449e04a0e810283a1e9933adedd2")
if !bytes.Equal(keys.headerProtectionKey, hp) {
t.Fatal("hp")
}
}
func TestKeys_HeaderProtection_(t *testing.T) {
// https://datatracker.ietf.org/doc/html/rfc9001#name-client-initial
keys, err := NewKeys(destConnId, Version_V1, common.NewGcm)
if err != nil {
t.Fatal("NewKeys", err)
}
defer keys.Close()
sample, _ := hex.DecodeString("d1b1c98dd7689fb8ec11d242b123dc9b")
flag := byte(0xc3)
packetNumber, _ := hex.DecodeString("00000002")
if packetNumber, err = keys.HeaderProtection_(sample, true, &flag, packetNumber); err != nil {
t.Fatal("HeaderProtection_", err)
}
if flag != 0xc0 {
t.Fatal("flag:", flag)
}
if !bytes.Equal(packetNumber, []byte{0x7b}) {
t.Fatalf("packetNumber: %x", packetNumber)
}
}
func TestKeys_PayloadDecrypt_(t *testing.T) {
destConnId, _ := hex.DecodeString("7f9863b69d513af6a050f0272dfe4dd1")
keys, err := NewKeys(destConnId, Version_Draft, common.NewGcm)
if err != nil {
t.Fatal("NewKeys", err)
}
defer keys.Close()
data, _ := hex.DecodeString("cfff00001d107f9863b69d513af6a050f0272dfe4dd114cb9b88815f5c3e385f2a8756c2a76c61fe0a6ddf0041222cce1ec1f09bb7d134541f214437ebaac82ad3044e24abffb166407f6e8e41584fe9717fbec115d345c934408aa9314bb9e8a3487ea2c17a7ff02f65d3ed8f76a462034260bb41d6ef8f0fa78d6920074a10091f85d322c10f1f4eb7e207c2283c4df5857edea1279248ba03ba83c4727b759f564dcd4db3e6e11d40abce3d4362caf5ef592a3cde2d66acadc7428b5cccf28eb1461b0c3ca595ff7425f5898b95bf4917786a5f9ce7226dd0be61cff453bd74decfa057d3afaef136226e9ba23ad3e28da820a367b4788786efa97bf59033b87bc8a4555b86148cfde85ea16772eb1d81e14c9056f3f36a4f789bc608145712fa7cd28f93e76d3f90e80815e267aeefff2bc44299f8b65e3cf99816c96f33723d20565162cc843024bdbd83a90d2f")
header := data[:50]
potentialPacketNumber := header[len(header)-4:]
sample := data[50 : 50+16]
flag := &header[0]
var packetNumber []byte
if packetNumber, err = keys.HeaderProtection_(sample, true, flag, potentialPacketNumber); err != nil {
t.Fatal("HeaderProtection_", err)
}
if *flag != 0b11000000 {
t.Fatalf("flag: %b", *flag)
}
if !bytes.Equal(packetNumber, []byte{1}) {
t.Fatal("packetNumber:", packetNumber)
}
header = data[:len(header)-4+len(packetNumber)]
payload := data[len(header):]
plaintext, err := keys.PayloadDecryptFromPool(payload, packetNumber, header)
if err != nil {
t.Fatal("PayloadDecryptFromPool:", err)
}
t.Log(hex.EncodeToString(plaintext))
}

View File

@ -0,0 +1,31 @@
// Modified from https://github.com/quic-go/quic-go/blob/58cedf7a4f/internal/handshake/hkdf.go
package quicutils
import (
"encoding/binary"
"github.com/mzz2017/softwind/pool"
"golang.org/x/crypto/hkdf"
"hash"
"io"
)
// HkdfExpandLabelFromPool HKDF expands a label.
// Since this implementation avoids using a cryptobyte.Builder, it is about 15% faster than the
// hkdfExpandLabel in the standard library.
func HkdfExpandLabelFromPool(h func() hash.Hash, secret, label []byte, context []byte, length int) ([]byte, error) {
b := pool.Get(3 + 6 + len(label) + 1 + len(context))
defer pool.Put(b)
binary.BigEndian.PutUint16(b, uint16(length))
b[2] = uint8(6 + len(label))
copy(b[3:], "tls13 ")
copy(b[9:], label)
b[9+len(label)] = uint8(len(context))
copy(b[10+len(label):], context)
out := pool.Get(length)
if _, err := io.ReadFull(hkdf.Expand(h, secret, b), out); err != nil {
return nil, err
}
return out, nil
}

View File

@ -0,0 +1,335 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package quicutils
import (
"fmt"
"github.com/mzz2017/softwind/pool"
"sort"
)
var (
UnknownFrameTypeError = fmt.Errorf("unknown frame type")
OutOfRangeError = fmt.Errorf("index out of range")
)
const (
Quic_FrameType_Padding = 0
Quic_FrameType_Ping = 1
Quic_FrameType_Crypto = 6
Quic_FrameType_ConnectionClose = 0x1c
Quic_FrameType_ConnectionClose2 = 0x1d
)
type CryptoFrameOffset struct {
UpperAppOffset int
// Offset of data in quic payload.
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) {
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)
if err != nil {
pool.Put(b)
return nil, err
}
if offset == nil {
continue
}
copy(b[offset.UpperAppOffset:], offset.Data)
if offset.UpperAppOffset+len(offset.Data) > boundary {
boundary = offset.UpperAppOffset + len(offset.Data)
}
}
return b[:boundary], nil
}
func ExtractCryptoFrameOffset(remainder []byte, transportOffset int) (offset *CryptoFrameOffset, frameSize int, err error) {
if len(remainder) == 0 {
return nil, 0, fmt.Errorf("frame has no length: %w", OutOfRangeError)
}
frameType, nextField, err := BigEndianUvarint(remainder[:])
if err != nil {
return nil, 0, err
}
switch frameType {
case Quic_FrameType_Ping:
return nil, nextField, nil
case Quic_FrameType_Padding:
for ; nextField < len(remainder) && remainder[nextField] == 0; nextField++ {
}
return nil, nextField, nil
case Quic_FrameType_Crypto:
offset, n, err := BigEndianUvarint(remainder[nextField:])
if err != nil {
return nil, 0, err
}
nextField += n
length, n, err := BigEndianUvarint(remainder[nextField:])
if err != nil {
return nil, 0, err
}
nextField += n
return &CryptoFrameOffset{
UpperAppOffset: int(offset),
Data: remainder[nextField : nextField+int(length)],
}, nextField + int(length), nil
default:
return nil, 0, fmt.Errorf("%w: %v", UnknownFrameTypeError, frameType)
}
}
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.copyBytes(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.copyBytes(iOuter, iInner, jOuter, jInner, j-i+1)
}
// copyBytes copy bytes including i and j.
func (r *CryptoFrameRelocation) copyBytes(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
}
type Locator interface {
Range(i, j int) []byte
Slice(i, j int) Locator
At(i int) byte
Len() int
}
// LinearLocator only searches forward and have no boundary check.
type LinearLocator struct {
left int
length int
iOuter int
baseEnd int
baseStart int
baseData []byte
cfr *CryptoFrameRelocation
}
func NewLinearLocator(cfr *CryptoFrameRelocation) (linearLocator *LinearLocator) {
return &LinearLocator{
left: 0,
length: cfr.length,
iOuter: 0,
baseData: cfr.o[0].Data,
baseStart: cfr.o[0].UpperAppOffset,
baseEnd: cfr.o[0].UpperAppOffset + len(cfr.o[0].Data),
cfr: cfr,
}
}
func (ll *LinearLocator) relocate(i int) {
// 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)
}
}
func (ll *LinearLocator) Range(i, j int) []byte {
if i == j {
return []byte{}
}
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)
// Linearly copy.
if j < ll.baseEnd {
// In the same block, no copy needed.
return ll.baseData[i-ll.baseStart : j-ll.baseStart+1]
}
b := make([]byte, size)
k := 0
for j >= ll.baseEnd {
n := copy(b[k:], ll.baseData[i-ll.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)
}
copy(b[k:], ll.baseData[i-ll.baseStart:j-ll.baseStart+1])
return b
}
func (ll *LinearLocator) At(i int) byte {
i += ll.left
ll.relocate(i)
b := ll.baseData[i-ll.baseStart]
return b
}
func (ll *LinearLocator) Slice(i, j int) Locator {
// We do not care about right.
newLL := *ll
newLL.left += i
newLL.length = j - i + 1
return &newLL
}
func (ll *LinearLocator) Len() int {
return ll.length
}
type BuiltinBytesLocator []byte
func (l BuiltinBytesLocator) Range(i, j int) []byte {
return l[i:j]
}
func (l BuiltinBytesLocator) At(i int) byte {
return l[i]
}
func (l BuiltinBytesLocator) Slice(i, j int) Locator {
return l[i:j]
}
func (l BuiltinBytesLocator) Len() int {
return len(l)
}

View File

@ -0,0 +1,80 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package quicutils
import "fmt"
type Version int
const (
Version_Draft = iota
Version_V1
Version_V2
)
func ParseVersion(version uint32) (Version, error) {
switch version {
case 0x6b3343cf:
return Version_V2, nil
case 1:
return Version_V1, nil
default:
if (version & 0xff000000) == 0xff000000 {
return Version_Draft, nil
}
return 0, fmt.Errorf("unknown version")
}
}
func (v Version) InitialSalt() []byte {
switch v {
case Version_Draft:
return []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}
case Version_V1:
return []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
case Version_V2:
return []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9}
default:
panic("unsupported quic version")
}
}
func (v Version) HpLabel() []byte {
switch v {
case Version_Draft:
fallthrough
case Version_V1:
return []byte("quic hp")
case Version_V2:
return []byte("quicv2 hp")
default:
panic("unsupported quic version")
}
}
func (v Version) KeyLabel() []byte {
switch v {
case Version_Draft:
fallthrough
case Version_V1:
return []byte("quic key")
case Version_V2:
return []byte("quicv2 key")
default:
panic("unsupported quic version")
}
}
func (v Version) IvLabel() []byte {
switch v {
case Version_Draft:
fallthrough
case Version_V1:
return []byte("quic iv")
case Version_V2:
return []byte("quicv2 iv")
default:
panic("unsupported quic version")
}
}

183
component/sniffing/quic.go Normal file
View File

@ -0,0 +1,183 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package sniffing
import (
"errors"
"github.com/mzz2017/softwind/pool"
"github.com/v2rayA/dae/component/sniffing/internal/quicutils"
)
const (
QuicFlag_PacketNumberLength = iota
QuicFlag_PacketNumberLength1
QuicFlag_Reserved
QuicFlag_Reserved1
QuicFlag_LongPacketType
QuicFlag_LongPacketType1
QuicFlag_FixedBit
QuicFlag_HeaderForm
)
const (
QuicFlag_HeaderForm_LongHeader = 1
QuicFlag_LongPacketType_Initial = 0
)
var (
QuicReassemble = QuicReassemblePolicy_ReassembleCryptoToBytesFromPool
)
type QuicReassemblePolicy int
const (
QuicReassemblePolicy_ReassembleCryptoToBytesFromPool QuicReassemblePolicy = iota
QuicReassemblePolicy_LinearLocator
QuicReassemblePolicy_Slow
)
func (s *Sniffer) SniffQuic() (d string, err error) {
nextBlock := s.buf
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 {
return "", NotFoundError
}
return "", err
}
// Error is not NotApplicableError, should be quic block.
isQuic = true
if len(nextBlock) == 0 {
return "", NotFoundError
}
}
}
func sniffQuicBlock(buf []byte) (d string, 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
}
// 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
}
if ((protectedFlag >> QuicFlag_LongPacketType) & 0b11) != QuicFlag_LongPacketType_Initial {
return "", nil, NotApplicableError
}
// Skip version.
destConnIdLength := int(buf[boundary-1])
boundary += destConnIdLength + 1 // +1 because next field has 1B length
if len(buf) < boundary {
return "", 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
}
tokenLength, n, err := quicutils.BigEndianUvarint(buf[boundary-quicutils.MaxVarintLen64:])
if err != nil {
return "", 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
}
// 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
}
boundary = boundary - quicutils.MaxVarintLen64 + n // Correct boundary.
blockEnd := boundary + int(length)
if len(buf) < blockEnd {
return "", nil, NotApplicableError
}
boundary += quicutils.MaxPacketNumberLength
if len(buf) < boundary {
return "", nil, NotApplicableError
}
header := buf[:boundary]
// Decrypt protected Packets.
// https://datatracker.ietf.org/doc/html/rfc9000#packet-protected
// This function will modify the packet in place, thus we should save the first byte and MaxPacketNumberLength
// and recover it later.
firstByte := header[0]
rawPacketNumber := pool.Get(quicutils.MaxPacketNumberLength)
copy(rawPacketNumber, header[boundary-quicutils.MaxPacketNumberLength:])
defer pool.Put(rawPacketNumber)
defer func() {
header[0] = firstByte
copy(header[boundary-quicutils.MaxPacketNumberLength:], rawPacketNumber)
}()
plaintext, err := quicutils.DecryptQuicFromPool_(header, blockEnd, destConnId)
if err != nil {
return "", 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 {
return "", 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
}

View File

@ -0,0 +1,61 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package sniffing
import (
"encoding/hex"
"github.com/sirupsen/logrus"
"testing"
)
var QuicStream, _ = hex.DecodeString("c00000000108d60451e5cb0f7050000044bc9acdcae1f0bbb8b98bb6ce5371cec3db5d4212d08d370fd220bd92fd3f42b06d8b25feea28c190ce741b316d28d8be11bd5637db3b00bf945ee330066b5262e7cef82d1f1fcc19d1879e1a7e062d068217a7a62c2fc8dd5226e6988d605578f05bebe102825af36fdfd50514cc41de82acde9d10c3f6c0650273c6f750251925d730a558ec820c221665550b0d41e38494b884393197d38cf9e32dcd0d4cbe6a75be00069bdd04836c6e83a360ee072b9da44c9dfec05762bb0bea1585755e22fdb0b11c67166b3178207801847a8d079fb6600a8bc22860ffb98d49dd56fbdd0c16bbae54d219adbcda402e43fc524667b7f08171820958ddf5b111cf776555aa0b23d6820748a4bb9695d754a1a2ce30901d90cdd5ab8aecae5b9fce5b6cf0e993a7316ce8eded51b382587a5c31ed27c6e96808a7eab84ec168b1648c6c0218760d6304c718266df2ac5c73e1f34f1b57a025a8fe5fecfbf3ff90b51cd26f5b93ef47fe790324951cb347a85b3ed5ba3e04880f83041e13f142f72173a9cc79ff3303a7b981b133600e47852ea8a03b5414f9cc39f5401b187bb22373bd75c64b79bb4601e12283f7d4f0916509e8f0114574e1ac316b07b83887eb451f268a325b713fe2f53b9eda5db85eaef20c632bd839d8fe49318fc913192d107d5416bc57d619c0db700abc4db48a73abd32a3d98f06c90aa17924321774b2d8b073ca1e1062a6473eba613037deefd306962a5bc0b7843678f25fbf74cff6430ef1c324a12f0beadb2bbe861cb211b7114b55164fe00907d7b41bd0140bdbe9c405405030b58478803e1d76082583d7122ee8552b5f901ca924afdefe3a30f345793108f50a9d934077864d6533db956d5d424f0aeed1fd85a6292d2ae7bd392a8223e66f46d7ffd1ad16871774c133af6dec7a97e67987158738d40bb496e3e3d478a01e0b995c44e08e37f137d4885f382176d9f5f7b00b81c96e83f2b60b762c8ed89a1077bb860c317332af8595cbe9afc3dab1da1a62cbd721f8eca8ee8be4240356ed9918fb4beddcab654ed085112e6abfeb0680979a20edd9426719d0f97bff2031f2af40dc47f47b815665f28f920fe5ceef4481f5195811e9cdc23dcec251550ed17111ab45124498dfbaae87b2081814d3fdfdb581a651ea3929fede7e9e01f8475360a484478bcde90d463b76b0ff486aba41f9b7d043ad3dac35e50569bf007f12c08b433474c4aa085bf0ff1ebbb2ee6e7dbdc92c429f9b54b098a5d34b0448458919bbb13b0b4c0ba82a130e8d9f105d724729a22c9c31476bfa609eb53f2d7a1c7134e60ba365a3082775d601f7c96f52e915da5e7670aa33e6027bca2a99ec8871ea424febb741bf2ca0e1d911b7047b6ddfe17c03d9c1aedb106e0ec40b1a8c980bfdcce083a1769bbdd8fd149f86f6cae50ab9b8dc7c1306c03a4a9bb2dcf0c477ae7431fba5f1e7e999bccb7ad73df33d9931f9e7291a4f882b4065ac63d58d81746e48f5035074b90cac149210efabded7f6099df28959f74aeb8ab2b8c63cf85c2ca3e054d70d31d3b3d24f4f8d36dfb82c9371f31313fa7a5bb56b10ef75d8aa4b976d9196728e6ef4401f9e2cf33058cda0b1464c8f14d404c48e4c9258830985eecccbaddd06407605f1af40542c727ab80e460cb322613204a60fb135e890e1f3b95d9a64a45450815db1c741837bf4a")
//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)
d, err := sniffer.SniffQuic()
if err != nil {
b.Fatal(err)
}
if d == "" {
b.Fatal(d)
}
}
}

View File

@ -0,0 +1,89 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package sniffing
import (
"github.com/mzz2017/softwind/pool"
"io"
)
type Sniffer struct {
r io.Reader
buf []byte
bufAt int
stream bool
}
func NewStreamSniffer(r io.Reader, bufSize int) *Sniffer {
s := &Sniffer{
r: r,
buf: pool.Get(bufSize),
stream: true,
}
return s
}
func NewPacketSniffer(data []byte) *Sniffer {
s := &Sniffer{
buf: data,
stream: false,
}
return s
}
type sniff func() (d string, err error)
func (s *Sniffer) SniffTcp() (d string, err error) {
if s.stream {
n, err := s.r.Read(s.buf)
if err != nil {
return "", err
}
s.buf = s.buf[:n]
}
if len(s.buf) == 0 {
return "", NotApplicableError
}
sniffs := []sniff{
// Most sniffable traffic is TLS, thus we sniff it first.
s.SniffTls,
s.SniffHttp,
}
for _, sniffer := range sniffs {
d, err = sniffer()
if err == nil {
return d, nil
}
if err != NotApplicableError {
return "", err
}
}
return "", NotApplicableError
}
func (s *Sniffer) Read(p []byte) (n int, err error) {
if s.buf != nil && s.bufAt < len(s.buf) {
// Read buf first.
n = copy(p, s.buf[s.bufAt:])
s.bufAt += n
if s.bufAt >= len(s.buf) {
if s.stream {
pool.Put(s.buf)
}
s.buf = nil
}
return n, nil
}
if !s.stream {
return 0, io.EOF
}
return s.r.Read(p)
}
func (s *Sniffer) Close() (err error) {
// DO NOT use pool.Put() here because Close() may not interrupt the reading, which will modify the value of the pool buffer.
return nil
}

View File

@ -0,0 +1,21 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package sniffing
import (
"errors"
"fmt"
)
var (
Error = fmt.Errorf("sniffing error")
NotApplicableError = fmt.Errorf("%w: not applicable", Error)
NotFoundError = fmt.Errorf("%w: not found", Error)
)
func IsSniffingError(err error) bool {
return errors.Is(err, Error)
}

View File

@ -0,0 +1,47 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package sniffing
import (
"fmt"
"github.com/mzz2017/softwind/pkg/fastrand"
"testing"
)
var (
httpMethodSet map[string]struct{}
)
func init() {
httpMethods := []string{"GET", "POST", "PUT", "PATCH", "DELETE", "COPY", "HEAD", "OPTIONS", "LINK", "UNLINK", "PURGE", "LOCK", "UNLOCK", "PROPFIND"}
httpMethodSet = make(map[string]struct{})
for _, method := range httpMethods {
httpMethodSet[method] = struct{}{}
}
}
func BenchmarkStringSet(b *testing.B) {
for i := 0; i < b.N; i++ {
var test [5]byte
fastrand.Read(test[:])
_, ok := httpMethodSet[string(test[:])]
if !ok {
fmt.Sprintf("%v", string(test[:]))
}
}
}
func BenchmarkStringSwitch(b *testing.B) {
for i := 0; i < b.N; i++ {
var test [5]byte
fastrand.Read(test[:])
switch string(test[:]) {
case "GET", "POST", "PUT", "PATCH", "DELETE", "COPY", "HEAD", "OPTIONS", "LINK", "UNLINK", "PURGE", "LOCK", "UNLOCK", "PROPFIND":
default:
fmt.Sprintf("%v", string(test[:]))
}
}
}

136
component/sniffing/tls.go Normal file
View File

@ -0,0 +1,136 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package sniffing
import (
"bytes"
"encoding/binary"
"github.com/v2rayA/dae/component/sniffing/internal/quicutils"
)
const (
ContentType_HandShake byte = 22
HandShakeType_Hello byte = 1
TlsExtension_ServerName uint16 = 0
TlsExtension_ServerNameType_HostName byte = 0
)
var (
Version_Tls1_0 = []byte{0x03, 0x01}
Version_Tls1_2 = []byte{0x03, 0x03}
HandShakePrefix = []byte{ContentType_HandShake, Version_Tls1_0[0], Version_Tls1_0[1]}
)
// SniffTls only supports tls1.2, tls1.3
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 {
return "", NotApplicableError
}
if !bytes.Equal(s.buf[:3], HandShakePrefix) {
return "", NotApplicableError
}
length := int(binary.BigEndian.Uint16(s.buf[3:5]))
search := s.buf[5:]
if len(search) < length {
return "", NotApplicableError
}
return extractSniFromTls(quicutils.BuiltinBytesLocator(search[:length]))
}
func extractSniFromTls(search quicutils.Locator) (sni string, err error) {
boundary := 39
if search.Len() < boundary {
return "", NotApplicableError
}
// Transport Layer Security (TLS) Extensions: Extension Definitions
// https://www.rfc-editor.org/rfc/rfc6066#page-5
b := search.Range(0, 6)
if b[0] != HandShakeType_Hello {
return "", NotApplicableError
}
// Three bytes length.
length2 := (int(b[1]) << 16) + (int(b[2]) << 8) + int(b[3])
if search.Len() > length2+4 {
return "", NotApplicableError
}
if !bytes.Equal(b[4:], Version_Tls1_2) {
return "", NotApplicableError
}
// Skip 32 bytes random.
sessionIdLength := search.At(boundary - 1)
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)
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)
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)
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))
}
func findSniExtension(search quicutils.Locator) (string, error) {
i := 0
var b []byte
for {
if i+4 >= search.Len() {
return "", NotApplicableError
}
b = search.Range(i, i+4)
typ := binary.BigEndian.Uint16(b)
extLength := int(binary.BigEndian.Uint16(b[2:]))
iNextField := i + 4 + extLength
if iNextField > search.Len() {
return "", NotApplicableError
}
if typ == TlsExtension_ServerName {
b = search.Range(i+4, i+9)
sniLen := int(binary.BigEndian.Uint16(b))
if extLength != sniLen+2 {
return "", NotApplicableError
}
// There may be multiple server names, we only pick the first.
if b[2] != TlsExtension_ServerNameType_HostName {
return "", NotApplicableError
}
snLen := int(binary.BigEndian.Uint16(b[3:]))
if i+9+snLen > iNextField {
return "", NotApplicableError
}
b = search.Range(i+9, i+9+snLen)
sni := string(b)
return sni, nil
}
i = iNextField
}
}

View File

@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
*/
package sniffing
import (
"encoding/hex"
"github.com/sirupsen/logrus"
"testing"
)
var tlsStream, _ = hex.DecodeString("1603010200010001fc0303d90fdf25b0c7a11c3eb968604a065157a149407c139c22ed32f5c6f486ed2c04206c51c32da7f83c3c19766be60d45d264e898c77504e34915c44caa69513c2221003e130213031301c02cc030009fcca9cca8ccaac02bc02f009ec024c028006bc023c0270067c00ac0140039c009c0130033009d009c003d003c0035002f00ff0100017500000013001100000e7777772e676f6f676c652e636f6d000b000403000102000a00160014001d0017001e00190018010001010102010301040010000e000c02683208687474702f312e31001600000017000000310000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602002b0009080304030303020301002d00020101003300260024001d00207fe08226bdc4fb1715e477506b6afe8f3abe2d20daa1f8c78c5483f1a90a9b19001500af00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
func TestName(t *testing.T) {
logrus.SetLevel(logrus.DebugLevel)
sniffer := NewPacketSniffer(tlsStream)
d, err := sniffer.SniffTls()
if err != nil {
t.Fatal(err)
}
if d == "" {
t.Fatal(d)
}
}

View File

@ -27,6 +27,7 @@ type Global struct {
LanNatDirect bool `mapstructure:"lan_nat_direct" required:""`
WanInterface []string `mapstructure:"wan_interface"`
AllowInsecure bool `mapstructure:"allow_insecure" default:"false"`
DialMode string `mapstructure:"dial_mode" default:"domain"`
}
type Group struct {

View File

@ -49,9 +49,11 @@ type ControlPlane struct {
Final string
// mutex protects the dnsCache.
mutex sync.Mutex
dnsCacheMu sync.Mutex
dnsCache map[string]*dnsCache
dnsUpstream DnsUpstreamRaw
dialMode consts.DialMode
}
func NewControlPlane(
@ -294,6 +296,8 @@ func NewControlPlane(
return nil, fmt.Errorf("RoutingMatcherBuilder.Build: %w", err)
}
dialMode, err := consts.ParseDialMode(global.DialMode)
c = &ControlPlane{
log: log,
core: core,
@ -304,12 +308,13 @@ func NewControlPlane(
SimulatedLpmTries: builder.SimulatedLpmTries,
SimulatedDomainSet: builder.SimulatedDomainSet,
Final: routingA.Final,
mutex: sync.Mutex{},
dnsCacheMu: sync.Mutex{},
dnsCache: make(map[string]*dnsCache),
dnsUpstream: DnsUpstreamRaw{
Raw: global.DnsUpstream,
FinishInitCallback: nil,
},
dialMode: dialMode,
}
/// DNS upstream
@ -401,6 +406,30 @@ func (c *ControlPlane) finishInitDnsUpstreamResolve(raw common.UrlOrEmpty, dnsUp
return nil
}
func (c *ControlPlane) ChooseDialTarget(outbound consts.OutboundIndex, dst netip.AddrPort, domain string) (dialTarget string) {
mode := consts.DialMode_Ip
if c.dialMode == consts.DialMode_Domain &&
!outbound.IsReserved() && // Direct, block, etc. should be skipped.
domain != "" {
dstIp := common.ConvergeIp(dst.Addr())
cache := c.lookupDnsRespCache(domain, common.AddrToDnsType(dstIp))
if cache != nil && cache.IncludeIp(dstIp) {
mode = consts.DialMode_Domain
}
}
switch mode {
case consts.DialMode_Ip:
dialTarget = dst.String()
case consts.DialMode_Domain:
dialTarget = net.JoinHostPort(domain, strconv.Itoa(int(dst.Port())))
c.log.WithFields(logrus.Fields{
"from": dst.String(),
"to": dialTarget,
}).Debugln("Reset dial target to domain")
}
return dialTarget
}
func (c *ControlPlane) ListenAndServe(port uint16) (err error) {
// Listen.
var listenConfig = net.ListenConfig{

View File

@ -50,6 +50,29 @@ func (c *dnsCache) FillInto(req *dnsmessage.Message) {
req.Truncated = false
}
func (c *dnsCache) IncludeIp(ip netip.Addr) bool {
ip = common.ConvergeIp(ip)
for _, ans := range c.Answers {
switch body := ans.Body.(type) {
case *dnsmessage.AResource:
if !ip.Is4() {
continue
}
if netip.AddrFrom4(body.A) == ip {
return true
}
case *dnsmessage.AAAAResource:
if !ip.Is6() {
continue
}
if netip.AddrFrom16(body.AAAA) == ip {
return true
}
}
}
return false
}
// BatchUpdateDomainRouting update bpf map domain_routing. Since one IP may have multiple domains, this function should
// be invoked every A/AAAA-record lookup.
func (c *ControlPlane) BatchUpdateDomainRouting(cache *dnsCache) error {
@ -83,6 +106,22 @@ func (c *ControlPlane) BatchUpdateDomainRouting(cache *dnsCache) error {
return nil
}
func (c *ControlPlane) lookupDnsRespCache(domain string, t dnsmessage.Type) (cache *dnsCache) {
now := time.Now()
// To fqdn.
if !strings.HasSuffix(domain, ".") {
domain = domain + "."
}
c.dnsCacheMu.Lock()
cache, ok := c.dnsCache[strings.ToLower(domain)+t.String()]
c.dnsCacheMu.Unlock()
if ok && cache.Deadline.After(now) {
return cache
}
return nil
}
func (c *ControlPlane) LookupDnsRespCache(msg *dnsmessage.Message) (resp []byte) {
if len(msg.Questions) == 0 {
return nil
@ -96,12 +135,8 @@ func (c *ControlPlane) LookupDnsRespCache(msg *dnsmessage.Message) (resp []byte)
default:
return nil
}
now := time.Now()
c.mutex.Lock()
cache, ok := c.dnsCache[strings.ToLower(q.Name.String())+q.Type.String()]
c.mutex.Unlock()
if ok && cache.Deadline.After(now) {
//c.log.Debugln("DNS cache hit:", q.Name, q.Type)
cache := c.lookupDnsRespCache(q.Name.String(), q.Type)
if cache != nil {
cache.FillInto(msg)
b, err := msg.Pack()
if err != nil {
@ -292,7 +327,7 @@ loop:
}
func (c *ControlPlane) UpdateDnsCache(host string, typ dnsmessage.Type, answers []dnsmessage.Resource, deadline time.Time) (err error) {
c.mutex.Lock()
c.dnsCacheMu.Lock()
fqdn := strings.ToLower(host)
if !strings.HasSuffix(fqdn, ".") {
fqdn += "."
@ -300,7 +335,7 @@ func (c *ControlPlane) UpdateDnsCache(host string, typ dnsmessage.Type, answers
cacheKey := fqdn + typ.String()
cache, ok := c.dnsCache[cacheKey]
if ok {
c.mutex.Unlock()
c.dnsCacheMu.Unlock()
cache.Deadline = deadline
cache.Answers = answers
} else {
@ -310,7 +345,7 @@ func (c *ControlPlane) UpdateDnsCache(host string, typ dnsmessage.Type, answers
Deadline: deadline,
}
c.dnsCache[cacheKey] = cache
c.mutex.Unlock()
c.dnsCacheMu.Unlock()
}
if err = c.BatchUpdateDomainRouting(cache); err != nil {
return fmt.Errorf("BatchUpdateDomainRouting: %w", err)

View File

@ -12,6 +12,7 @@ import (
"github.com/v2rayA/dae/common"
"github.com/v2rayA/dae/common/consts"
"github.com/v2rayA/dae/component/outbound/dialer"
"github.com/v2rayA/dae/component/sniffing"
internal "github.com/v2rayA/dae/pkg/ebpf_internal"
"golang.org/x/sys/unix"
"net"
@ -20,8 +21,23 @@ import (
"time"
)
const (
TcpSniffBufSize = 4096
)
func (c *ControlPlane) handleConn(lConn net.Conn) (err error) {
defer lConn.Close()
// Sniff target domain.
sniffer := sniffing.NewConnSniffer(lConn, TcpSniffBufSize)
domain, err := sniffer.SniffTcp()
if err != nil && !sniffing.IsSniffingError(err) {
return err
}
// ConnSniffer should be used later, so we cannot close it now.
defer sniffer.Close()
// Get tuples and outbound.
src := lConn.RemoteAddr().(*net.TCPAddr).AddrPort()
dst := lConn.LocalAddr().(*net.TCPAddr).AddrPort()
outboundIndex, err := c.core.RetrieveOutboundIndex(src, dst, unix.IPPROTO_TCP)
@ -75,13 +91,17 @@ func (c *ControlPlane) handleConn(lConn net.Conn) (err error) {
"outbound": outbound.Name,
"policy": outbound.GetSelectionPolicy(),
"dialer": d.Name(),
"domain": domain,
}).Infof("%v <-> %v", RefineSourceToShow(src, dst.Addr(), consts.LanWanFlag_NotApplicable), RefineAddrPortToShow(dst))
rConn, err := d.Dial("tcp", dst.String())
// Dial and relay.
rConn, err := d.Dial("tcp", c.ChooseDialTarget(outboundIndex, dst, domain))
if err != nil {
return fmt.Errorf("failed to dial %v: %w", dst, err)
}
defer rConn.Close()
if err = RelayTCP(lConn, rConn); err != nil {
if err = RelayTCP(sniffer, rConn); err != nil {
switch {
case strings.HasSuffix(err.Error(), "write: broken pipe"),
strings.HasSuffix(err.Error(), "i/o timeout"):

View File

@ -14,6 +14,7 @@ import (
"github.com/v2rayA/dae/common"
"github.com/v2rayA/dae/common/consts"
"github.com/v2rayA/dae/component/outbound/dialer"
"github.com/v2rayA/dae/component/sniffing"
"golang.org/x/net/dns/dnsmessage"
"io"
"net"
@ -140,6 +141,7 @@ func (c *ControlPlane) WriteToUDP(lanWanFlag consts.LanWanFlag, lConn *net.UDPCo
func (c *ControlPlane) handlePkt(lConn *net.UDPConn, data []byte, src, pktDst, realDst netip.AddrPort, outboundIndex consts.OutboundIndex) (err error) {
var lanWanFlag consts.LanWanFlag
var realSrc netip.AddrPort
var domain string
useAssign := pktDst == realDst // Use sk_assign instead of modify target ip/port.
if useAssign {
lanWanFlag = consts.LanWanFlag_IsLan
@ -199,6 +201,15 @@ func (c *ControlPlane) handlePkt(lConn *net.UDPConn, data []byte, src, pktDst, r
if data, err = dnsMessage.Pack(); err != nil {
return fmt.Errorf("pack flipped dns packet: %w", err)
}
} else {
// Sniff Quic
sniffer := sniffing.NewPacketSniffer(data)
domain, err = sniffer.SniffQuic()
if err != nil && !sniffing.IsSniffingError(err) {
sniffer.Close()
return err
}
sniffer.Close()
}
l4proto := consts.L4ProtoStr_UDP
@ -277,7 +288,7 @@ func (c *ControlPlane) handlePkt(lConn *net.UDPConn, data []byte, src, pktDst, r
if dialerForNew == nil {
dialerForNew, _, err = outbound.Select(networkType)
if err != nil {
return fmt.Errorf("failed to select dialer from group %v (%v, dns?:%v,from: %v): %w", outbound.Name, networkType.String()[:4], isDns, realSrc.String(), err)
return fmt.Errorf("failed to select dialer from group %v (%v, dns?:%v,from: %v): %w", outbound.Name, networkType.StringWithoutDns(), isDns, realSrc.String(), err)
}
}
@ -307,6 +318,7 @@ func (c *ControlPlane) handlePkt(lConn *net.UDPConn, data []byte, src, pktDst, r
DialerFunc: func() (*dialer.Dialer, error) {
return dialerForNew, nil
},
// FIXME: how to write domain into UDP tunnel?
Target: destToSend,
})
if err != nil {
@ -332,6 +344,7 @@ func (c *ControlPlane) handlePkt(lConn *net.UDPConn, data []byte, src, pktDst, r
if err != nil {
c.log.WithFields(logrus.Fields{
"to": destToSend.String(),
"domain": domain,
"from": realSrc.String(),
"network": networkType.String(),
"err": err.Error(),
@ -346,11 +359,12 @@ func (c *ControlPlane) handlePkt(lConn *net.UDPConn, data []byte, src, pktDst, r
if !isDns {
return fmt.Errorf("UDP to TCP only support DNS request")
}
isNew = true
realDialer = dialerForNew
// We can block because we are in a coroutine.
conn, err := dialerForNew.Dial("tcp", destToSend.String())
conn, err := dialerForNew.Dial("tcp", c.ChooseDialTarget(outboundIndex, destToSend, domain))
if err != nil {
return fmt.Errorf("failed to dial proxy to tcp: %w", err)
}
@ -411,6 +425,7 @@ func (c *ControlPlane) handlePkt(lConn *net.UDPConn, data []byte, src, pktDst, r
"outbound": outbound.Name,
"policy": outbound.GetSelectionPolicy(),
"dialer": realDialer.Name(),
"domain": domain,
}).Infof("%v <-> %v",
RefineSourceToShow(realSrc, realDst.Addr(), lanWanFlag), RefineAddrPortToShow(destToSend),
)

View File

@ -9,7 +9,7 @@ global {
# Node connectivity check.
# Host of URL should have both IPv4 and IPv6 if you have double stack in local.
# Considering traffic consumption, it is recommended to choose a site with anycast IP and less response.
tcp_check_url: 'http://detectportal.firefox.com/success.txt'
tcp_check_url: 'http://keep-alv.google.com/generate_204'
# This DNS will be used to check UDP connectivity of nodes. And if dns_upstream below contains tcp, it also be used to check
# TCP DNS connectivity of nodes.
@ -46,6 +46,15 @@ global {
# Allow insecure TLS certificates. It is not recommended to turn it on unless you have to.
allow_insecure: false
# Experimental function. Optional values are:
# 1. "ip". Dial proxy using the IP from DNS directly. This allows your ipv4, ipv6 to choose the optimal path
# respectively, and makes the IP version requested by the application meet expectations. For example, if you
# use curl -4 ip.sb, you will request IPv4 via proxy and get a IPv4 echo. And curl -6 ip.sb will request IPv6.
# 2. "domain". Dial proxy using the domain from sniffing. This will relieve DNS pollution problem to a great extent
# if have impure DNS environment. This policy does not impact routing. That is to say, domain reset will be
# after traffic split.
dial_mode: domain
}
# Subscriptions defined here will be resolved as nodes and merged as a part of the global node pool.