Add domain sniffer

This commit is contained in:
世界 2022-07-06 12:39:44 +08:00
parent 2d9203ee74
commit 86a38a1c7e
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
15 changed files with 603 additions and 18 deletions

View file

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

58
common/sniff/dns.go Normal file
View file

@ -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
common/sniff/domain.go Normal file
View file

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

19
common/sniff/http.go Normal file
View file

@ -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
}

View file

@ -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
common/sniff/quic.go Normal file
View file

@ -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
common/sniff/quic_test.go Normal file
View file

@ -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
common/sniff/sniff.go Normal file
View file

@ -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
common/sniff/tls.go Normal file
View file

@ -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
constant/protocol.go Normal file
View file

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

14
go.mod
View file

@ -6,23 +6,25 @@ require (
github.com/database64128/tfo-go v1.0.4 github.com/database64128/tfo-go v1.0.4
github.com/goccy/go-json v0.9.8 github.com/goccy/go-json v0.9.8
github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/oschwald/geoip2-golang v1.7.0 github.com/oschwald/maxminddb-golang v1.9.0
github.com/sagernet/sing v0.0.0-20220705005401-57d12d875b7a github.com/sagernet/sing v0.0.0-20220706042103-9cd9268a7e3a
github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649 github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.5.0 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 ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.12 // 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/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // 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 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 lukechampine.com/blake3 v1.1.7 // indirect
) )

23
go.sum
View file

@ -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.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 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= 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 h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 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 h1:tIk4nv6VT9OiPyrnDAfJS1s1xKDQMZOsGojab6EjC1Y=
github.com/oschwald/maxminddb-golang v1.9.0/go.mod h1:TK+s/Z2oZq0rSl4PSeAEoP0bgm82Cp5HyvYbt8K3zLY= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/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-20220706042103-9cd9268a7e3a h1:QBAfegXTXY1sOZqxKrX3fQVzmvLESBlsiQZbmixSP/U=
github.com/sagernet/sing v0.0.0-20220705005401-57d12d875b7a/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c= 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 h1:whNDUGOAX5GPZkSy4G3Gv9QyIgk5SXRyjkRuP7ohF8k=
github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649/go.mod h1:MuyT+9fEPjvauAv0fSE0a6Q+l0Tv2ZrAafTkYfnxBFw= 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= 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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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.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.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.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 h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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-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 h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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 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.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.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 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=

View file

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

View file

@ -81,6 +81,8 @@ type ListenOptions struct {
Port uint16 `json:"listen_port"` Port uint16 `json:"listen_port"`
TCPFastOpen bool `json:"tcp_fast_open,omitempty"` TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
UDPTimeout int64 `json:"udp_timeout,omitempty"` UDPTimeout int64 `json:"udp_timeout,omitempty"`
SniffEnabled bool `json:"sniff,omitempty"`
SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"`
} }
type SimpleInboundOptions struct { type SimpleInboundOptions struct {

View file

@ -12,10 +12,13 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-box/common/geoip"
"github.com/sagernet/sing-box/common/geosite" "github.com/sagernet/sing-box/common/geosite"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata" 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 { 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) detour := r.match(ctx, metadata, r.defaultOutboundForConnection)
if !common.Contains(detour.Network(), C.NetworkTCP) { if !common.Contains(detour.Network(), C.NetworkTCP) {
conn.Close() 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 { 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) detour := r.match(ctx, metadata, r.defaultOutboundForPacketConnection)
if !common.Contains(detour.Network(), C.NetworkUDP) { if !common.Contains(detour.Network(), C.NetworkUDP) {
conn.Close() conn.Close()