Przeglądaj źródła

Add domain sniffer

世界 3 lat temu
rodzic
commit
86a38a1c7e

+ 3 - 1
adapter/inbound.go

@@ -20,7 +20,9 @@ type InboundContext struct {
 
 	// cache
 
+	SniffEnabled             bool
+	SniffOverrideDestination bool
+
 	SourceGeoIPCode string
 	GeoIPCode       string
-	// ProcessPath     string
 }

+ 58 - 0
common/sniff/dns.go

@@ -0,0 +1,58 @@
+package sniff
+
+import (
+	"context"
+	"encoding/binary"
+	"io"
+	"os"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/task"
+	"golang.org/x/net/dns/dnsmessage"
+)
+
+func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
+	var length uint16
+	err := binary.Read(reader, binary.BigEndian, &length)
+	if err != nil {
+		return nil, err
+	}
+	if length > 512 {
+		return nil, os.ErrInvalid
+	}
+	_buffer := buf.StackNewSize(int(length))
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+
+	readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)
+	err = task.Run(readCtx, func() error {
+		return common.Error(buffer.ReadFullFrom(reader, buffer.FreeLen()))
+	})
+	cancel()
+	if err != nil {
+		return nil, err
+	}
+	return DomainNameQuery(readCtx, buffer.Bytes())
+}
+
+func DomainNameQuery(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
+	var parser dnsmessage.Parser
+	_, err := parser.Start(packet)
+	if err != nil {
+		return nil, err
+	}
+	question, err := parser.Question()
+	if err != nil {
+		return nil, os.ErrInvalid
+	}
+	domain := question.Name.String()
+	if question.Class == dnsmessage.ClassINET && (question.Type == dnsmessage.TypeA || question.Type == dnsmessage.TypeAAAA) && IsDomainName(domain) {
+		return &adapter.InboundContext{Protocol: C.ProtocolDNS, Domain: domain}, nil
+	}
+	return nil, os.ErrInvalid
+}

+ 6 - 0
common/sniff/domain.go

@@ -0,0 +1,6 @@
+package sniff
+
+import _ "unsafe" // for linkname
+
+//go:linkname IsDomainName net.isDomainName
+func IsDomainName(domain string) bool

+ 19 - 0
common/sniff/http.go

@@ -0,0 +1,19 @@
+package sniff
+
+import (
+	std_bufio "bufio"
+	"context"
+	"io"
+
+	"github.com/sagernet/sing-box/adapter"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing/protocol/http"
+)
+
+func HTTPHost(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
+	request, err := http.ReadRequest(std_bufio.NewReader(reader))
+	if err != nil {
+		return nil, err
+	}
+	return &adapter.InboundContext{Protocol: C.ProtocolHTTP, Domain: request.Host}, nil
+}

+ 148 - 0
common/sniff/internal/qtls/qtls.go

@@ -0,0 +1,148 @@
+package qtls
+
+import (
+	"crypto"
+	"crypto/aes"
+	"crypto/cipher"
+	"encoding/binary"
+	"io"
+
+	"golang.org/x/crypto/hkdf"
+)
+
+const (
+	VersionDraft29 = 0xff00001d
+	Version1       = 0x1
+	Version2       = 0x709a50c4
+)
+
+var (
+	SaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}
+	SaltV1  = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
+	SaltV2  = []byte{0xa7, 0x07, 0xc2, 0x03, 0xa5, 0x9b, 0x47, 0x18, 0x4a, 0x1d, 0x62, 0xca, 0x57, 0x04, 0x06, 0xea, 0x7a, 0xe3, 0xe5, 0xd3}
+)
+
+const (
+	HKDFLabelKeyV1              = "quic key"
+	HKDFLabelKeyV2              = "quicv2 key"
+	HKDFLabelIVV1               = "quic iv"
+	HKDFLabelIVV2               = "quicv2 iv"
+	HKDFLabelHeaderProtectionV1 = "quic hp"
+	HKDFLabelHeaderProtectionV2 = "quicv2 hp"
+)
+
+func AEADAESGCMTLS13(key, nonceMask []byte) cipher.AEAD {
+	if len(nonceMask) != 12 {
+		panic("tls: internal error: wrong nonce length")
+	}
+	aes, err := aes.NewCipher(key)
+	if err != nil {
+		panic(err)
+	}
+	aead, err := cipher.NewGCM(aes)
+	if err != nil {
+		panic(err)
+	}
+
+	ret := &xorNonceAEAD{aead: aead}
+	copy(ret.nonceMask[:], nonceMask)
+	return ret
+}
+
+type xorNonceAEAD struct {
+	nonceMask [12]byte
+	aead      cipher.AEAD
+}
+
+func (f *xorNonceAEAD) NonceSize() int        { return 8 } // 64-bit sequence number
+func (f *xorNonceAEAD) Overhead() int         { return f.aead.Overhead() }
+func (f *xorNonceAEAD) explicitNonceLen() int { return 0 }
+
+func (f *xorNonceAEAD) Seal(out, nonce, plaintext, additionalData []byte) []byte {
+	for i, b := range nonce {
+		f.nonceMask[4+i] ^= b
+	}
+	result := f.aead.Seal(out, f.nonceMask[:], plaintext, additionalData)
+	for i, b := range nonce {
+		f.nonceMask[4+i] ^= b
+	}
+
+	return result
+}
+
+func (f *xorNonceAEAD) Open(out, nonce, ciphertext, additionalData []byte) ([]byte, error) {
+	for i, b := range nonce {
+		f.nonceMask[4+i] ^= b
+	}
+	result, err := f.aead.Open(out, f.nonceMask[:], ciphertext, additionalData)
+	for i, b := range nonce {
+		f.nonceMask[4+i] ^= b
+	}
+
+	return result, err
+}
+
+func HKDFExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte {
+	b := make([]byte, 3, 3+6+len(label)+1+len(context))
+	binary.BigEndian.PutUint16(b, uint16(length))
+	b[2] = uint8(6 + len(label))
+	b = append(b, []byte("tls13 ")...)
+	b = append(b, []byte(label)...)
+	b = b[:3+6+len(label)+1]
+	b[3+6+len(label)] = uint8(len(context))
+	b = append(b, context...)
+	out := make([]byte, length)
+	n, err := hkdf.Expand(hash.New, secret, b).Read(out)
+	if err != nil || n != length {
+		panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
+	}
+	return out
+}
+
+func ReadUvarint(r io.ByteReader) (uint64, error) {
+	firstByte, err := r.ReadByte()
+	if err != nil {
+		return 0, err
+	}
+	// the first two bits of the first byte encode the length
+	len := 1 << ((firstByte & 0xc0) >> 6)
+	b1 := firstByte & (0xff - 0xc0)
+	if len == 1 {
+		return uint64(b1), nil
+	}
+	b2, err := r.ReadByte()
+	if err != nil {
+		return 0, err
+	}
+	if len == 2 {
+		return uint64(b2) + uint64(b1)<<8, nil
+	}
+	b3, err := r.ReadByte()
+	if err != nil {
+		return 0, err
+	}
+	b4, err := r.ReadByte()
+	if err != nil {
+		return 0, err
+	}
+	if len == 4 {
+		return uint64(b4) + uint64(b3)<<8 + uint64(b2)<<16 + uint64(b1)<<24, nil
+	}
+	b5, err := r.ReadByte()
+	if err != nil {
+		return 0, err
+	}
+	b6, err := r.ReadByte()
+	if err != nil {
+		return 0, err
+	}
+	b7, err := r.ReadByte()
+	if err != nil {
+		return 0, err
+	}
+	b8, err := r.ReadByte()
+	if err != nil {
+		return 0, err
+	}
+	return uint64(b8) + uint64(b7)<<8 + uint64(b6)<<16 + uint64(b5)<<24 + uint64(b4)<<32 + uint64(b3)<<40 + uint64(b2)<<48 + uint64(b1)<<56, nil
+}

+ 187 - 0
common/sniff/quic.go

@@ -0,0 +1,187 @@
+package sniff
+
+import (
+	"bytes"
+	"context"
+	"crypto"
+	"crypto/aes"
+	"encoding/binary"
+	"io"
+	"os"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/sniff/internal/qtls"
+	C "github.com/sagernet/sing-box/constant"
+	"golang.org/x/crypto/hkdf"
+)
+
+func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
+	reader := bytes.NewReader(packet)
+
+	typeByte, err := reader.ReadByte()
+	if err != nil {
+		return nil, err
+	}
+
+	if typeByte&0x80 == 0 || typeByte&0x40 == 0 {
+		return nil, os.ErrInvalid
+	}
+	var versionNumber uint32
+	err = binary.Read(reader, binary.BigEndian, &versionNumber)
+	if err != nil {
+		return nil, err
+	}
+	if versionNumber != qtls.VersionDraft29 && versionNumber != qtls.Version1 && versionNumber != qtls.Version2 {
+		return nil, os.ErrInvalid
+	}
+	if (typeByte&0x30)>>4 == 0x0 {
+	} else if (typeByte&0x30)>>4 != 0x01 {
+		// 0-rtt
+	} else {
+		return nil, os.ErrInvalid
+	}
+
+	destConnIDLen, err := reader.ReadByte()
+	if err != nil {
+		return nil, err
+	}
+
+	destConnID := make([]byte, destConnIDLen)
+	_, err = io.ReadFull(reader, destConnID)
+	if err != nil {
+		return nil, err
+	}
+
+	srcConnIDLen, err := reader.ReadByte()
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = io.CopyN(io.Discard, reader, int64(srcConnIDLen))
+	if err != nil {
+		return nil, err
+	}
+
+	tokenLen, err := qtls.ReadUvarint(reader)
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = io.CopyN(io.Discard, reader, int64(tokenLen))
+	if err != nil {
+		return nil, err
+	}
+
+	packetLen, err := qtls.ReadUvarint(reader)
+	if err != nil {
+		return nil, err
+	}
+
+	hdrLen := int(reader.Size()) - reader.Len()
+	if hdrLen != len(packet)-int(packetLen) {
+		return nil, os.ErrInvalid
+	}
+
+	_, err = io.CopyN(io.Discard, reader, 4)
+	if err != nil {
+		return nil, err
+	}
+
+	pnBytes := make([]byte, aes.BlockSize)
+	_, err = io.ReadFull(reader, pnBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	var salt []byte
+	switch versionNumber {
+	case qtls.Version1:
+		salt = qtls.SaltV1
+	case qtls.Version2:
+		salt = qtls.SaltV2
+	default:
+		salt = qtls.SaltOld
+	}
+	var hkdfHeaderProtectionLabel string
+	switch versionNumber {
+	case qtls.Version2:
+		hkdfHeaderProtectionLabel = qtls.HKDFLabelHeaderProtectionV2
+	default:
+		hkdfHeaderProtectionLabel = qtls.HKDFLabelHeaderProtectionV1
+	}
+	initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt)
+	secret := qtls.HKDFExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size())
+	hpKey := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, hkdfHeaderProtectionLabel, 16)
+	block, err := aes.NewCipher(hpKey)
+	if err != nil {
+		return nil, err
+	}
+	mask := make([]byte, aes.BlockSize)
+	block.Encrypt(mask, pnBytes)
+	newPacket := make([]byte, len(packet))
+	copy(newPacket, packet)
+	newPacket[0] ^= mask[0] & 0xf
+	for i := range newPacket[hdrLen : hdrLen+4] {
+		newPacket[hdrLen+i] ^= mask[i+1]
+	}
+	packetNumberLength := newPacket[0]&0x3 + 1
+	if packetNumberLength != 1 {
+		return nil, os.ErrInvalid
+	}
+	packetNumber := newPacket[hdrLen]
+	if err != nil {
+		return nil, err
+	}
+	if packetNumber != 0 {
+		return nil, os.ErrInvalid
+	}
+
+	extHdrLen := hdrLen + int(packetNumberLength)
+	copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:])
+	data := newPacket[extHdrLen : int(packetLen)+hdrLen]
+
+	var keyLabel string
+	var ivLabel string
+	switch versionNumber {
+	case qtls.Version2:
+		keyLabel = qtls.HKDFLabelKeyV2
+		ivLabel = qtls.HKDFLabelIVV2
+	default:
+		keyLabel = qtls.HKDFLabelKeyV1
+		ivLabel = qtls.HKDFLabelIVV1
+	}
+
+	key := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, keyLabel, 16)
+	iv := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, ivLabel, 12)
+	cipher := qtls.AEADAESGCMTLS13(key, iv)
+	nonce := make([]byte, int32(cipher.NonceSize()))
+	binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber))
+	decrypted, err := cipher.Open(newPacket[extHdrLen:extHdrLen], nonce, data, newPacket[:extHdrLen])
+	if err != nil {
+		return nil, err
+	}
+	decryptedReader := bytes.NewReader(decrypted)
+	frameType, err := decryptedReader.ReadByte()
+	if frameType != 0x6 {
+		// not crypto frame
+		return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, nil
+	}
+	_, err = qtls.ReadUvarint(decryptedReader)
+	if err != nil {
+		return nil, err
+	}
+	_, err = qtls.ReadUvarint(decryptedReader)
+	if err != nil {
+		return nil, err
+	}
+	tlsHdr := make([]byte, 5)
+	tlsHdr[0] = 0x16
+	binary.BigEndian.PutUint16(tlsHdr[1:], uint16(0x0303))
+	binary.BigEndian.PutUint16(tlsHdr[3:], uint16(decryptedReader.Len()))
+	metadata, err := TLSClientHello(ctx, io.MultiReader(bytes.NewReader(tlsHdr), decryptedReader))
+	if err != nil {
+		return nil, err
+	}
+	metadata.Protocol = C.ProtocolQUIC
+	return metadata, nil
+}

+ 23 - 0
common/sniff/quic_test.go

@@ -0,0 +1,23 @@
+package sniff
+
+import (
+	"context"
+	"encoding/hex"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestSniffQUICv1(t *testing.T) {
+	pkt, err := hex.DecodeString("cc0000000108d2dc7bad02241f5003796e71004215a71bfcb05159416c724be418537389acdd9a4047306283dcb4d7a9cad5cc06322042d204da67a8dbaa328ab476bb428b48fd001501863afd203f8d4ef085629d664f1a734a65969a47e4a63d4e01a21f18c1d90db0c027180906dc135f9ae421bb8617314c8d54c175fef3d3383d310d0916ebcbd6eed9329befbbb109d8fd4af1d2cf9d6adce8e6c1260a7f8256e273e326da0aa7cc148d76e7a08489dc9d52ade89c027cbc3491ada46417c2c04e2ca768e9a7dd6aa00c594e48b678927325da796817693499bb727050cb3baf3d3291a397c3a8d868e8ec7b8f7295e347455c9dadbe2252ae917ac793d958c7fb8a3d2cdb34e3891eb4286f18617556ff7216dd60256aa5b1d11ff4753459fc5f9dedf11d483a26a0835dc6cd50e1c1f54f86e8f1e502821183cd874f6447a74e818bf3445c7795acf4559d1c1fac474911d2ead5c8d23e4aa4f67afb66efe305a30a0b5d825679b31ddc186cbea936535795c7e8c378c87b8c5adc065154d15bae8f85ac8fec2da40c3aa623b682a065440831555011d7647cde44446a0fb4cf5892f2c088ae1920643094be72e3c499fe8d265caf939e8ab607a5b9317917d2a32a812e8a0e6a2f84721bbb5984ffd242838f705d13f4cfb249bc6a5c80d58ac2595edf56648ec3fe21d787573c253a79805252d6d81e26d367d4ff29ef66b5fe8992086af7bada8cad10b82a7c0dc406c5b6d0c5ec3c583e767f759ce08cad6c3c8f91e5a8")
+	require.NoError(t, err)
+	metadata, err := QUICClientHello(context.Background(), pkt)
+	require.NoError(t, err)
+	require.Equal(t, metadata.Domain, "cloudflare-quic.com")
+}
+
+func FuzzSniffQUIC(f *testing.F) {
+	f.Fuzz(func(t *testing.T, data []byte) {
+		QUICClientHello(context.Background(), data)
+	})
+}

+ 36 - 0
common/sniff/sniff.go

@@ -0,0 +1,36 @@
+package sniff
+
+import (
+	"context"
+	"io"
+	"os"
+
+	"github.com/sagernet/sing-box/adapter"
+)
+
+type (
+	StreamSniffer = func(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error)
+	PacketSniffer = func(ctx context.Context, packet []byte) (*adapter.InboundContext, error)
+)
+
+func PeekStream(ctx context.Context, reader io.Reader, sniffers ...StreamSniffer) (*adapter.InboundContext, error) {
+	for _, sniffer := range sniffers {
+		sniffMetadata, err := sniffer(ctx, reader)
+		if err != nil {
+			return nil, err
+		}
+		return sniffMetadata, nil
+	}
+	return nil, os.ErrInvalid
+}
+
+func PeekPacket(ctx context.Context, packet []byte, sniffers ...PacketSniffer) (*adapter.InboundContext, error) {
+	for _, sniffer := range sniffers {
+		sniffMetadata, err := sniffer(ctx, packet)
+		if err != nil {
+			return nil, err
+		}
+		return sniffMetadata, nil
+	}
+	return nil, os.ErrInvalid
+}

+ 28 - 0
common/sniff/tls.go

@@ -0,0 +1,28 @@
+package sniff
+
+import (
+	"context"
+	"crypto/tls"
+	"io"
+
+	"github.com/sagernet/sing-box/adapter"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing/common/bufio"
+)
+
+func TLSClientHello(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
+	var clientHello *tls.ClientHelloInfo
+	err := tls.Server(bufio.NewReadOnlyConn(reader), &tls.Config{
+		GetConfigForClient: func(argHello *tls.ClientHelloInfo) (*tls.Config, error) {
+			clientHello = argHello
+			return nil, nil
+		},
+	}).HandshakeContext(ctx)
+	if clientHello != nil {
+		return &adapter.InboundContext{Protocol: C.ProtocolTLS, Domain: clientHello.ServerName}, nil
+	}
+	return nil, err
+}
+
+func Packet() {
+}

+ 8 - 0
constant/protocol.go

@@ -0,0 +1,8 @@
+package constant
+
+const (
+	ProtocolTLS  = "tls"
+	ProtocolHTTP = "http"
+	ProtocolQUIC = "quic"
+	ProtocolDNS  = "dns"
+)

+ 8 - 6
go.mod

@@ -6,23 +6,25 @@ require (
 	github.com/database64128/tfo-go v1.0.4
 	github.com/goccy/go-json v0.9.8
 	github.com/logrusorgru/aurora v2.0.3+incompatible
-	github.com/oschwald/geoip2-golang v1.7.0
-	github.com/sagernet/sing v0.0.0-20220705005401-57d12d875b7a
+	github.com/oschwald/maxminddb-golang v1.9.0
+	github.com/sagernet/sing v0.0.0-20220706042103-9cd9268a7e3a
 	github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649
 	github.com/sirupsen/logrus v1.8.1
 	github.com/spf13/cobra v1.5.0
-	github.com/stretchr/testify v1.7.1
+	github.com/stretchr/testify v1.8.0
+	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
+	golang.org/x/net v0.0.0-20220630215102-69896b714898
 )
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/klauspost/cpuid/v2 v2.0.12 // indirect
-	github.com/oschwald/maxminddb-golang v1.9.0 // indirect
+	github.com/kr/pretty v0.1.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
-	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
 	golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect
-	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
+	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 	lukechampine.com/blake3 v1.1.7 // indirect
 )

+ 16 - 7
go.sum

@@ -11,17 +11,20 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
 github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
 github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
-github.com/oschwald/geoip2-golang v1.7.0 h1:JW1r5AKi+vv2ujSxjKthySK3jo8w8oKWPyXsw+Qs/S8=
-github.com/oschwald/geoip2-golang v1.7.0/go.mod h1:mdI/C7iK7NVMcIDDtf4bCKMJ7r0o7UwGeCo9eiitCMQ=
 github.com/oschwald/maxminddb-golang v1.9.0 h1:tIk4nv6VT9OiPyrnDAfJS1s1xKDQMZOsGojab6EjC1Y=
 github.com/oschwald/maxminddb-golang v1.9.0/go.mod h1:TK+s/Z2oZq0rSl4PSeAEoP0bgm82Cp5HyvYbt8K3zLY=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sagernet/sing v0.0.0-20220705005401-57d12d875b7a h1:FhrHCkox9scuTzcT5DDh6flVLFuqU+QSk3VONd41I+o=
-github.com/sagernet/sing v0.0.0-20220705005401-57d12d875b7a/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c=
+github.com/sagernet/sing v0.0.0-20220706042103-9cd9268a7e3a h1:QBAfegXTXY1sOZqxKrX3fQVzmvLESBlsiQZbmixSP/U=
+github.com/sagernet/sing v0.0.0-20220706042103-9cd9268a7e3a/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649 h1:whNDUGOAX5GPZkSy4G3Gv9QyIgk5SXRyjkRuP7ohF8k=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649/go.mod h1:MuyT+9fEPjvauAv0fSE0a6Q+l0Tv2ZrAafTkYfnxBFw=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
@@ -31,18 +34,24 @@ github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJ
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw=
+golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8=
 golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
 lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=

+ 6 - 0
inbound/default.go

@@ -132,6 +132,8 @@ func (a *myInboundAdapter) loopTCPIn() {
 			ctx := log.ContextWithID(a.ctx)
 			var metadata adapter.InboundContext
 			metadata.Inbound = a.tag
+			metadata.SniffEnabled = a.listenOptions.SniffEnabled
+			metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
 			metadata.Network = C.NetworkTCP
 			metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr())
 			a.logger.WithContext(ctx).Info("inbound connection from ", metadata.Source)
@@ -161,6 +163,8 @@ func (a *myInboundAdapter) loopUDPIn() {
 		buffer.Truncate(n)
 		var metadata adapter.InboundContext
 		metadata.Inbound = a.tag
+		metadata.SniffEnabled = a.listenOptions.SniffEnabled
+		metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
 		metadata.Network = C.NetworkUDP
 		metadata.Source = M.SocksaddrFromNetIP(addr)
 		err = a.packetHandler.NewPacket(a.ctx, packetService, buffer, metadata)
@@ -183,6 +187,8 @@ func (a *myInboundAdapter) loopUDPInThreadSafe() {
 		buffer.Truncate(n)
 		var metadata adapter.InboundContext
 		metadata.Inbound = a.tag
+		metadata.SniffEnabled = a.listenOptions.SniffEnabled
+		metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
 		metadata.Network = C.NetworkUDP
 		metadata.Source = M.SocksaddrFromNetIP(addr)
 		err = a.packetHandler.NewPacket(a.ctx, packetService, buffer, metadata)

+ 6 - 4
option/inbound.go

@@ -77,10 +77,12 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
 }
 
 type ListenOptions struct {
-	Listen      ListenAddress `json:"listen"`
-	Port        uint16        `json:"listen_port"`
-	TCPFastOpen bool          `json:"tcp_fast_open,omitempty"`
-	UDPTimeout  int64         `json:"udp_timeout,omitempty"`
+	Listen                   ListenAddress `json:"listen"`
+	Port                     uint16        `json:"listen_port"`
+	TCPFastOpen              bool          `json:"tcp_fast_open,omitempty"`
+	UDPTimeout               int64         `json:"udp_timeout,omitempty"`
+	SniffEnabled             bool          `json:"sniff,omitempty"`
+	SniffOverrideDestination bool          `json:"sniff_override_destination,omitempty"`
 }
 
 type SimpleInboundOptions struct {

+ 51 - 0
route/router.go

@@ -12,10 +12,13 @@ import (
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/geoip"
 	"github.com/sagernet/sing-box/common/geosite"
+	"github.com/sagernet/sing-box/common/sniff"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/bufio"
 	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
 	M "github.com/sagernet/sing/common/metadata"
@@ -188,6 +191,29 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
 }
 
 func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	if metadata.SniffEnabled {
+		_buffer := buf.StackNew()
+		defer common.KeepAlive(_buffer)
+		buffer := common.Dup(_buffer)
+		defer buffer.Release()
+		reader := io.TeeReader(conn, buffer)
+		sniffMetadata, err := sniff.PeekStream(ctx, reader, sniff.TLSClientHello, sniff.HTTPHost)
+		if err == nil {
+			metadata.Protocol = sniffMetadata.Protocol
+			metadata.Domain = sniffMetadata.Domain
+			if metadata.SniffOverrideDestination && sniff.IsDomainName(metadata.Domain) {
+				metadata.Destination.Fqdn = metadata.Domain
+			}
+			if metadata.Domain != "" {
+				r.logger.WithContext(ctx).Info("sniffed protocol: ", metadata.Protocol, ", domain: ", metadata.Domain)
+			} else {
+				r.logger.WithContext(ctx).Info("sniffed protocol: ", metadata.Protocol)
+			}
+		}
+		if !buffer.IsEmpty() {
+			conn = bufio.NewCachedConn(conn, buffer)
+		}
+	}
 	detour := r.match(ctx, metadata, r.defaultOutboundForConnection)
 	if !common.Contains(detour.Network(), C.NetworkTCP) {
 		conn.Close()
@@ -197,6 +223,31 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
 }
 
 func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	if metadata.SniffEnabled {
+		_buffer := buf.StackNewPacket()
+		defer common.KeepAlive(_buffer)
+		buffer := common.Dup(_buffer)
+		defer buffer.Release()
+		_, err := conn.ReadPacket(buffer)
+		if err != nil {
+			return err
+		}
+		sniffMetadata, err := sniff.PeekPacket(ctx, buffer.Bytes(), sniff.QUICClientHello)
+		originDestination := metadata.Destination
+		if err == nil {
+			metadata.Protocol = sniffMetadata.Protocol
+			metadata.Domain = sniffMetadata.Domain
+			if metadata.SniffOverrideDestination && sniff.IsDomainName(metadata.Domain) {
+				metadata.Destination.Fqdn = metadata.Domain
+			}
+			if metadata.Domain != "" {
+				r.logger.WithContext(ctx).Info("sniffed protocol: ", metadata.Protocol, ", domain: ", metadata.Domain)
+			} else {
+				r.logger.WithContext(ctx).Info("sniffed protocol: ", metadata.Protocol)
+			}
+		}
+		conn = bufio.NewCachedPacketConn(conn, buffer, originDestination)
+	}
 	detour := r.match(ctx, metadata, r.defaultOutboundForPacketConnection)
 	if !common.Contains(detour.Network(), C.NetworkUDP) {
 		conn.Close()