From 86a38a1c7e4f7f6cdd7d03e891919c596e86bf09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 6 Jul 2022 12:39:44 +0800 Subject: [PATCH] Add domain sniffer --- adapter/inbound.go | 4 +- common/sniff/dns.go | 58 +++++++++ common/sniff/domain.go | 6 + common/sniff/http.go | 19 +++ common/sniff/internal/qtls/qtls.go | 148 +++++++++++++++++++++++ common/sniff/quic.go | 187 +++++++++++++++++++++++++++++ common/sniff/quic_test.go | 23 ++++ common/sniff/sniff.go | 36 ++++++ common/sniff/tls.go | 28 +++++ constant/protocol.go | 8 ++ go.mod | 14 ++- go.sum | 23 ++-- inbound/default.go | 6 + option/inbound.go | 10 +- route/router.go | 51 ++++++++ 15 files changed, 603 insertions(+), 18 deletions(-) create mode 100644 common/sniff/dns.go create mode 100644 common/sniff/domain.go create mode 100644 common/sniff/http.go create mode 100644 common/sniff/internal/qtls/qtls.go create mode 100644 common/sniff/quic.go create mode 100644 common/sniff/quic_test.go create mode 100644 common/sniff/sniff.go create mode 100644 common/sniff/tls.go create mode 100644 constant/protocol.go diff --git a/adapter/inbound.go b/adapter/inbound.go index 7401f64c..821b7b65 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -20,7 +20,9 @@ type InboundContext struct { // cache + SniffEnabled bool + SniffOverrideDestination bool + SourceGeoIPCode string GeoIPCode string - // ProcessPath string } diff --git a/common/sniff/dns.go b/common/sniff/dns.go new file mode 100644 index 00000000..5637eb4c --- /dev/null +++ b/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 +} diff --git a/common/sniff/domain.go b/common/sniff/domain.go new file mode 100644 index 00000000..44021d3b --- /dev/null +++ b/common/sniff/domain.go @@ -0,0 +1,6 @@ +package sniff + +import _ "unsafe" // for linkname + +//go:linkname IsDomainName net.isDomainName +func IsDomainName(domain string) bool diff --git a/common/sniff/http.go b/common/sniff/http.go new file mode 100644 index 00000000..9cd16624 --- /dev/null +++ b/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 +} diff --git a/common/sniff/internal/qtls/qtls.go b/common/sniff/internal/qtls/qtls.go new file mode 100644 index 00000000..8a13d9ec --- /dev/null +++ b/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 +} diff --git a/common/sniff/quic.go b/common/sniff/quic.go new file mode 100644 index 00000000..7e4c5c87 --- /dev/null +++ b/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 +} diff --git a/common/sniff/quic_test.go b/common/sniff/quic_test.go new file mode 100644 index 00000000..fd0a5cd5 --- /dev/null +++ b/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) + }) +} diff --git a/common/sniff/sniff.go b/common/sniff/sniff.go new file mode 100644 index 00000000..12055bed --- /dev/null +++ b/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 +} diff --git a/common/sniff/tls.go b/common/sniff/tls.go new file mode 100644 index 00000000..063ccb18 --- /dev/null +++ b/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() { +} diff --git a/constant/protocol.go b/constant/protocol.go new file mode 100644 index 00000000..62a33f49 --- /dev/null +++ b/constant/protocol.go @@ -0,0 +1,8 @@ +package constant + +const ( + ProtocolTLS = "tls" + ProtocolHTTP = "http" + ProtocolQUIC = "quic" + ProtocolDNS = "dns" +) diff --git a/go.mod b/go.mod index 3db06801..e1456ba9 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index e997a183..76e51bd4 100644 --- a/go.sum +++ b/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= diff --git a/inbound/default.go b/inbound/default.go index 4d56aea7..119547e8 100644 --- a/inbound/default.go +++ b/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) diff --git a/option/inbound.go b/option/inbound.go index 79ef23e4..970b5cdf 100644 --- a/option/inbound.go +++ b/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 { diff --git a/route/router.go b/route/router.go index 17e6a1dd..de3f45bc 100644 --- a/route/router.go +++ b/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()