mirror of
https://github.com/daeuniverse/dae.git
synced 2024-12-23 01:24:45 +07:00
feat: add sniffing suite and dial_mode option (#16)
This commit is contained in:
parent
2a586e6341
commit
ebdbf9a4a7
24
common/consts/control.go
Normal file
24
common/consts/control.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
49
component/sniffing/conn_sniffer.go
Normal file
49
component/sniffing/conn_sniffer.go
Normal 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
|
||||
}
|
35
component/sniffing/http.go
Normal file
35
component/sniffing/http.go
Normal 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
|
||||
}
|
25
component/sniffing/internal/quicutils/binary.go
Normal file
25
component/sniffing/internal/quicutils/binary.go
Normal 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
|
||||
}
|
167
component/sniffing/internal/quicutils/cipher.go
Normal file
167
component/sniffing/internal/quicutils/cipher.go
Normal 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
|
||||
}
|
96
component/sniffing/internal/quicutils/cipher_test.go
Normal file
96
component/sniffing/internal/quicutils/cipher_test.go
Normal 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))
|
||||
}
|
31
component/sniffing/internal/quicutils/hkdf.go
Normal file
31
component/sniffing/internal/quicutils/hkdf.go
Normal 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
|
||||
}
|
335
component/sniffing/internal/quicutils/relocation.go
Normal file
335
component/sniffing/internal/quicutils/relocation.go
Normal 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)
|
||||
}
|
80
component/sniffing/internal/quicutils/version.go
Normal file
80
component/sniffing/internal/quicutils/version.go
Normal 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
183
component/sniffing/quic.go
Normal 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
|
||||
}
|
61
component/sniffing/quic_bench_test.go
Normal file
61
component/sniffing/quic_bench_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
89
component/sniffing/sniffer.go
Normal file
89
component/sniffing/sniffer.go
Normal 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
|
||||
}
|
21
component/sniffing/sniffing.go
Normal file
21
component/sniffing/sniffing.go
Normal 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)
|
||||
}
|
47
component/sniffing/sniffing_bench_test.go
Normal file
47
component/sniffing/sniffing_bench_test.go
Normal 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
136
component/sniffing/tls.go
Normal 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
|
||||
}
|
||||
}
|
26
component/sniffing/tls_test.go
Normal file
26
component/sniffing/tls_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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{
|
||||
|
@ -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)
|
||||
|
@ -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"):
|
||||
|
@ -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),
|
||||
)
|
||||
|
11
example.dae
11
example.dae
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user