mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-22 00:21:30 +00:00
Improve QUIC sniffer
This commit is contained in:
parent
3a7acaa92a
commit
9dc3bb975a
|
@ -31,11 +31,16 @@ type InboundContext struct {
|
||||||
Network string
|
Network string
|
||||||
Source M.Socksaddr
|
Source M.Socksaddr
|
||||||
Destination M.Socksaddr
|
Destination M.Socksaddr
|
||||||
Domain string
|
|
||||||
Protocol string
|
|
||||||
User string
|
User string
|
||||||
Outbound string
|
Outbound string
|
||||||
|
|
||||||
|
// sniffer
|
||||||
|
|
||||||
|
Protocol string
|
||||||
|
Domain string
|
||||||
|
Client string
|
||||||
|
SniffContext any
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
|
|
||||||
InboundDetour string
|
InboundDetour string
|
||||||
|
|
29
common/ja3/LICENSE
Normal file
29
common/ja3/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2018, Open Systems AG
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
3
common/ja3/README.md
Normal file
3
common/ja3/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# JA3
|
||||||
|
|
||||||
|
mod from: https://github.com/open-ch/ja3
|
31
common/ja3/error.go
Normal file
31
common/ja3/error.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright (c) 2018, Open Systems AG. All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style license
|
||||||
|
// that can be found in the LICENSE file in the root of the source
|
||||||
|
// tree.
|
||||||
|
|
||||||
|
package ja3
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Error types
|
||||||
|
const (
|
||||||
|
LengthErr string = "length check %v failed"
|
||||||
|
ContentTypeErr string = "content type not matching"
|
||||||
|
VersionErr string = "version check %v failed"
|
||||||
|
HandshakeTypeErr string = "handshake type not matching"
|
||||||
|
SNITypeErr string = "SNI type not supported"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseError can be encountered while parsing a segment
|
||||||
|
type ParseError struct {
|
||||||
|
errType string
|
||||||
|
check int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ParseError) Error() string {
|
||||||
|
if e.errType == LengthErr || e.errType == VersionErr {
|
||||||
|
return fmt.Sprintf(e.errType, e.check)
|
||||||
|
}
|
||||||
|
return fmt.Sprint(e.errType)
|
||||||
|
}
|
83
common/ja3/ja3.go
Normal file
83
common/ja3/ja3.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// Copyright (c) 2018, Open Systems AG. All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style license
|
||||||
|
// that can be found in the LICENSE file in the root of the source
|
||||||
|
// tree.
|
||||||
|
|
||||||
|
package ja3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientHello struct {
|
||||||
|
Version uint16
|
||||||
|
CipherSuites []uint16
|
||||||
|
Extensions []uint16
|
||||||
|
EllipticCurves []uint16
|
||||||
|
EllipticCurvePF []uint8
|
||||||
|
Versions []uint16
|
||||||
|
SignatureAlgorithms []uint16
|
||||||
|
ServerName string
|
||||||
|
ja3ByteString []byte
|
||||||
|
ja3Hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ClientHello) Equals(another *ClientHello, ignoreExtensionsSequence bool) bool {
|
||||||
|
if j.Version != another.Version {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !slices.Equal(j.CipherSuites, another.CipherSuites) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.Extensions) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.sortedExtensions()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !slices.Equal(j.EllipticCurves, another.EllipticCurves) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !slices.Equal(j.EllipticCurvePF, another.EllipticCurvePF) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !slices.Equal(j.SignatureAlgorithms, another.SignatureAlgorithms) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ClientHello) sortedExtensions() []uint16 {
|
||||||
|
extensions := make([]uint16, len(j.Extensions))
|
||||||
|
copy(extensions, j.Extensions)
|
||||||
|
slices.Sort(extensions)
|
||||||
|
return extensions
|
||||||
|
}
|
||||||
|
|
||||||
|
func Compute(payload []byte) (*ClientHello, error) {
|
||||||
|
ja3 := ClientHello{}
|
||||||
|
err := ja3.parseSegment(payload)
|
||||||
|
return &ja3, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ClientHello) String() string {
|
||||||
|
if j.ja3ByteString == nil {
|
||||||
|
j.marshalJA3()
|
||||||
|
}
|
||||||
|
return string(j.ja3ByteString)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ClientHello) Hash() string {
|
||||||
|
if j.ja3ByteString == nil {
|
||||||
|
j.marshalJA3()
|
||||||
|
}
|
||||||
|
if j.ja3Hash == "" {
|
||||||
|
h := md5.Sum(j.ja3ByteString)
|
||||||
|
j.ja3Hash = hex.EncodeToString(h[:])
|
||||||
|
}
|
||||||
|
return j.ja3Hash
|
||||||
|
}
|
357
common/ja3/parser.go
Normal file
357
common/ja3/parser.go
Normal file
|
@ -0,0 +1,357 @@
|
||||||
|
// Copyright (c) 2018, Open Systems AG. All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style license
|
||||||
|
// that can be found in the LICENSE file in the root of the source
|
||||||
|
// tree.
|
||||||
|
|
||||||
|
package ja3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Constants used for parsing
|
||||||
|
recordLayerHeaderLen int = 5
|
||||||
|
handshakeHeaderLen int = 6
|
||||||
|
randomDataLen int = 32
|
||||||
|
sessionIDHeaderLen int = 1
|
||||||
|
cipherSuiteHeaderLen int = 2
|
||||||
|
compressMethodHeaderLen int = 1
|
||||||
|
extensionsHeaderLen int = 2
|
||||||
|
extensionHeaderLen int = 4
|
||||||
|
sniExtensionHeaderLen int = 5
|
||||||
|
ecExtensionHeaderLen int = 2
|
||||||
|
ecpfExtensionHeaderLen int = 1
|
||||||
|
versionExtensionHeaderLen int = 1
|
||||||
|
signatureAlgorithmsExtensionHeaderLen int = 2
|
||||||
|
contentType uint8 = 22
|
||||||
|
handshakeType uint8 = 1
|
||||||
|
sniExtensionType uint16 = 0
|
||||||
|
sniNameDNSHostnameType uint8 = 0
|
||||||
|
ecExtensionType uint16 = 10
|
||||||
|
ecpfExtensionType uint16 = 11
|
||||||
|
versionExtensionType uint16 = 43
|
||||||
|
signatureAlgorithmsExtensionType uint16 = 13
|
||||||
|
|
||||||
|
// Versions
|
||||||
|
// The bitmask covers the versions SSL3.0 to TLS1.2
|
||||||
|
tlsVersionBitmask uint16 = 0xFFFC
|
||||||
|
tls13 uint16 = 0x0304
|
||||||
|
|
||||||
|
// GREASE values
|
||||||
|
// The bitmask covers all GREASE values
|
||||||
|
GreaseBitmask uint16 = 0x0F0F
|
||||||
|
|
||||||
|
// Constants used for marshalling
|
||||||
|
dashByte = byte(45)
|
||||||
|
commaByte = byte(44)
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseSegment to populate the corresponding ClientHello object or return an error
|
||||||
|
func (j *ClientHello) parseSegment(segment []byte) error {
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(segment) < recordLayerHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have "Content Type: Handshake (22)"
|
||||||
|
contType := uint8(segment[0])
|
||||||
|
if contType != contentType {
|
||||||
|
return &ParseError{errType: ContentTypeErr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if TLS record layer version is supported
|
||||||
|
tlsRecordVersion := uint16(segment[1])<<8 | uint16(segment[2])
|
||||||
|
if tlsRecordVersion&tlsVersionBitmask != 0x0300 && tlsRecordVersion != tls13 {
|
||||||
|
return &ParseError{VersionErr, 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the Handshake is as long as expected from the length field
|
||||||
|
segmentLen := uint16(segment[3])<<8 | uint16(segment[4])
|
||||||
|
if len(segment[recordLayerHeaderLen:]) < int(segmentLen) {
|
||||||
|
return &ParseError{LengthErr, 2}
|
||||||
|
}
|
||||||
|
// Keep the Handshake messege, ignore any additional following record types
|
||||||
|
hs := segment[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)]
|
||||||
|
|
||||||
|
err := j.parseHandshake(hs)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHandshake body
|
||||||
|
func (j *ClientHello) parseHandshake(hs []byte) error {
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 3}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have "Handshake Type: Client Hello (1)"
|
||||||
|
handshType := uint8(hs[0])
|
||||||
|
if handshType != handshakeType {
|
||||||
|
return &ParseError{errType: HandshakeTypeErr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if actual length of handshake matches (this is a great exclusion criterion for false positives,
|
||||||
|
// as these fields have to match the actual length of the rest of the segment)
|
||||||
|
handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])
|
||||||
|
if len(hs[4:]) != int(handshakeLen) {
|
||||||
|
return &ParseError{LengthErr, 4}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Client Hello version is supported
|
||||||
|
tlsVersion := uint16(hs[4])<<8 | uint16(hs[5])
|
||||||
|
if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 {
|
||||||
|
return &ParseError{VersionErr, 2}
|
||||||
|
}
|
||||||
|
j.Version = tlsVersion
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
sessionIDLen := uint8(hs[38])
|
||||||
|
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) {
|
||||||
|
return &ParseError{LengthErr, 5}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cipher Suites
|
||||||
|
cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):]
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(cs) < cipherSuiteHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 6}
|
||||||
|
}
|
||||||
|
|
||||||
|
csLen := uint16(cs[0])<<8 | uint16(cs[1])
|
||||||
|
numCiphers := int(csLen / 2)
|
||||||
|
cipherSuites := make([]uint16, 0, numCiphers)
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 7}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < numCiphers; i++ {
|
||||||
|
cipherSuite := uint16(cs[2+i<<1])<<8 | uint16(cs[3+i<<1])
|
||||||
|
cipherSuites = append(cipherSuites, cipherSuite)
|
||||||
|
}
|
||||||
|
j.CipherSuites = cipherSuites
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)])
|
||||||
|
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) {
|
||||||
|
return &ParseError{LengthErr, 8}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extensions
|
||||||
|
exs := cs[cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen):]
|
||||||
|
|
||||||
|
err := j.parseExtensions(exs)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseExtensions of the handshake
|
||||||
|
func (j *ClientHello) parseExtensions(exs []byte) error {
|
||||||
|
// Check for no extensions, this fields header is nonexistent if no body is used
|
||||||
|
if len(exs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(exs) < extensionsHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 9}
|
||||||
|
}
|
||||||
|
|
||||||
|
exsLen := uint16(exs[0])<<8 | uint16(exs[1])
|
||||||
|
exs = exs[extensionsHeaderLen:]
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(exs) < int(exsLen) {
|
||||||
|
return &ParseError{LengthErr, 10}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sni []byte
|
||||||
|
var extensions, ellipticCurves []uint16
|
||||||
|
var ellipticCurvePF []uint8
|
||||||
|
var versions []uint16
|
||||||
|
var signatureAlgorithms []uint16
|
||||||
|
for len(exs) > 0 {
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(exs) < extensionHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 11}
|
||||||
|
}
|
||||||
|
|
||||||
|
exType := uint16(exs[0])<<8 | uint16(exs[1])
|
||||||
|
exLen := uint16(exs[2])<<8 | uint16(exs[3])
|
||||||
|
// Ignore any GREASE extensions
|
||||||
|
extensions = append(extensions, exType)
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(exs) < extensionHeaderLen+int(exLen) {
|
||||||
|
return &ParseError{LengthErr, 12}
|
||||||
|
}
|
||||||
|
|
||||||
|
sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)]
|
||||||
|
|
||||||
|
switch exType {
|
||||||
|
case sniExtensionType: // Extensions: server_name
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(sex) < sniExtensionHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 13}
|
||||||
|
}
|
||||||
|
|
||||||
|
sniType := uint8(sex[2])
|
||||||
|
sniLen := uint16(sex[3])<<8 | uint16(sex[4])
|
||||||
|
sex = sex[sniExtensionHeaderLen:]
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(sex) != int(sniLen) {
|
||||||
|
return &ParseError{LengthErr, 14}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch sniType {
|
||||||
|
case sniNameDNSHostnameType:
|
||||||
|
sni = sex
|
||||||
|
default:
|
||||||
|
return &ParseError{errType: SNITypeErr}
|
||||||
|
}
|
||||||
|
case ecExtensionType: // Extensions: supported_groups
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(sex) < ecExtensionHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 15}
|
||||||
|
}
|
||||||
|
|
||||||
|
ecsLen := uint16(sex[0])<<8 | uint16(sex[1])
|
||||||
|
numCurves := int(ecsLen / 2)
|
||||||
|
ellipticCurves = make([]uint16, 0, numCurves)
|
||||||
|
sex = sex[ecExtensionHeaderLen:]
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(sex) != int(ecsLen) {
|
||||||
|
return &ParseError{LengthErr, 16}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < numCurves; i++ {
|
||||||
|
ecType := uint16(sex[i*2])<<8 | uint16(sex[1+i*2])
|
||||||
|
ellipticCurves = append(ellipticCurves, ecType)
|
||||||
|
}
|
||||||
|
|
||||||
|
case ecpfExtensionType: // Extensions: ec_point_formats
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(sex) < ecpfExtensionHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 17}
|
||||||
|
}
|
||||||
|
|
||||||
|
ecpfsLen := uint8(sex[0])
|
||||||
|
numPF := int(ecpfsLen)
|
||||||
|
ellipticCurvePF = make([]uint8, numPF)
|
||||||
|
sex = sex[ecpfExtensionHeaderLen:]
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(sex) != numPF {
|
||||||
|
return &ParseError{LengthErr, 18}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < numPF; i++ {
|
||||||
|
ellipticCurvePF[i] = uint8(sex[i])
|
||||||
|
}
|
||||||
|
case versionExtensionType:
|
||||||
|
if len(sex) < versionExtensionHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 19}
|
||||||
|
}
|
||||||
|
versionsLen := int(sex[0])
|
||||||
|
for i := 0; i < versionsLen; i += 2 {
|
||||||
|
versions = append(versions, binary.BigEndian.Uint16(sex[1:][i:]))
|
||||||
|
}
|
||||||
|
case signatureAlgorithmsExtensionType:
|
||||||
|
if len(sex) < signatureAlgorithmsExtensionHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 20}
|
||||||
|
}
|
||||||
|
ssaLen := binary.BigEndian.Uint16(sex)
|
||||||
|
for i := 0; i < int(ssaLen); i += 2 {
|
||||||
|
signatureAlgorithms = append(signatureAlgorithms, binary.BigEndian.Uint16(sex[2:][i:]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exs = exs[4+exLen:]
|
||||||
|
}
|
||||||
|
j.ServerName = string(sni)
|
||||||
|
j.Extensions = extensions
|
||||||
|
j.EllipticCurves = ellipticCurves
|
||||||
|
j.EllipticCurvePF = ellipticCurvePF
|
||||||
|
j.Versions = versions
|
||||||
|
j.SignatureAlgorithms = signatureAlgorithms
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// marshalJA3 into a byte string
|
||||||
|
func (j *ClientHello) marshalJA3() {
|
||||||
|
// An uint16 can contain numbers with up to 5 digits and an uint8 can contain numbers with up to 3 digits, but we
|
||||||
|
// also need a byte for each separating character, except at the end.
|
||||||
|
byteStringLen := 6*(1+len(j.CipherSuites)+len(j.Extensions)+len(j.EllipticCurves)) + 4*len(j.EllipticCurvePF) - 1
|
||||||
|
byteString := make([]byte, 0, byteStringLen)
|
||||||
|
|
||||||
|
// Version
|
||||||
|
byteString = strconv.AppendUint(byteString, uint64(j.Version), 10)
|
||||||
|
byteString = append(byteString, commaByte)
|
||||||
|
|
||||||
|
// Cipher Suites
|
||||||
|
if len(j.CipherSuites) != 0 {
|
||||||
|
for _, val := range j.CipherSuites {
|
||||||
|
if val&GreaseBitmask != 0x0A0A {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byteString = strconv.AppendUint(byteString, uint64(val), 10)
|
||||||
|
byteString = append(byteString, dashByte)
|
||||||
|
}
|
||||||
|
// Replace last dash with a comma
|
||||||
|
byteString[len(byteString)-1] = commaByte
|
||||||
|
} else {
|
||||||
|
byteString = append(byteString, commaByte)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extensions
|
||||||
|
if len(j.Extensions) != 0 {
|
||||||
|
for _, val := range j.Extensions {
|
||||||
|
if val&GreaseBitmask != 0x0A0A {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byteString = strconv.AppendUint(byteString, uint64(val), 10)
|
||||||
|
byteString = append(byteString, dashByte)
|
||||||
|
}
|
||||||
|
// Replace last dash with a comma
|
||||||
|
byteString[len(byteString)-1] = commaByte
|
||||||
|
} else {
|
||||||
|
byteString = append(byteString, commaByte)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elliptic curves
|
||||||
|
if len(j.EllipticCurves) != 0 {
|
||||||
|
for _, val := range j.EllipticCurves {
|
||||||
|
if val&GreaseBitmask != 0x0A0A {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byteString = strconv.AppendUint(byteString, uint64(val), 10)
|
||||||
|
byteString = append(byteString, dashByte)
|
||||||
|
}
|
||||||
|
// Replace last dash with a comma
|
||||||
|
byteString[len(byteString)-1] = commaByte
|
||||||
|
} else {
|
||||||
|
byteString = append(byteString, commaByte)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECPF
|
||||||
|
if len(j.EllipticCurvePF) != 0 {
|
||||||
|
for _, val := range j.EllipticCurvePF {
|
||||||
|
byteString = strconv.AppendUint(byteString, uint64(val), 10)
|
||||||
|
byteString = append(byteString, dashByte)
|
||||||
|
}
|
||||||
|
// Remove last dash
|
||||||
|
byteString = byteString[:len(byteString)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
j.ja3ByteString = byteString
|
||||||
|
}
|
|
@ -19,45 +19,44 @@ const (
|
||||||
|
|
||||||
// BitTorrent detects if the stream is a BitTorrent connection.
|
// BitTorrent detects if the stream is a BitTorrent connection.
|
||||||
// For the BitTorrent protocol specification, see https://www.bittorrent.org/beps/bep_0003.html
|
// For the BitTorrent protocol specification, see https://www.bittorrent.org/beps/bep_0003.html
|
||||||
func BitTorrent(_ context.Context, reader io.Reader) (*adapter.InboundContext, error) {
|
func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||||
var first byte
|
var first byte
|
||||||
err := binary.Read(reader, binary.BigEndian, &first)
|
err := binary.Read(reader, binary.BigEndian, &first)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if first != 19 {
|
if first != 19 {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
var protocol [19]byte
|
var protocol [19]byte
|
||||||
_, err = reader.Read(protocol[:])
|
_, err = reader.Read(protocol[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if string(protocol[:]) != "BitTorrent protocol" {
|
if string(protocol[:]) != "BitTorrent protocol" {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
return &adapter.InboundContext{
|
metadata.Protocol = C.ProtocolBitTorrent
|
||||||
Protocol: C.ProtocolBitTorrent,
|
return nil
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UTP detects if the packet is a uTP connection packet.
|
// UTP detects if the packet is a uTP connection packet.
|
||||||
// For the uTP protocol specification, see
|
// For the uTP protocol specification, see
|
||||||
// 1. https://www.bittorrent.org/beps/bep_0029.html
|
// 1. https://www.bittorrent.org/beps/bep_0029.html
|
||||||
// 2. https://github.com/bittorrent/libutp/blob/2b364cbb0650bdab64a5de2abb4518f9f228ec44/utp_internal.cpp#L112
|
// 2. https://github.com/bittorrent/libutp/blob/2b364cbb0650bdab64a5de2abb4518f9f228ec44/utp_internal.cpp#L112
|
||||||
func UTP(_ context.Context, packet []byte) (*adapter.InboundContext, error) {
|
func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
// A valid uTP packet must be at least 20 bytes long.
|
// A valid uTP packet must be at least 20 bytes long.
|
||||||
if len(packet) < 20 {
|
if len(packet) < 20 {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
version := packet[0] & 0x0F
|
version := packet[0] & 0x0F
|
||||||
ty := packet[0] >> 4
|
ty := packet[0] >> 4
|
||||||
if version != 1 || ty > 4 {
|
if version != 1 || ty > 4 {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the extensions
|
// Validate the extensions
|
||||||
|
@ -66,36 +65,35 @@ func UTP(_ context.Context, packet []byte) (*adapter.InboundContext, error) {
|
||||||
for extension != 0 {
|
for extension != 0 {
|
||||||
err := binary.Read(reader, binary.BigEndian, &extension)
|
err := binary.Read(reader, binary.BigEndian, &extension)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var length byte
|
var length byte
|
||||||
err = binary.Read(reader, binary.BigEndian, &length)
|
err = binary.Read(reader, binary.BigEndian, &length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = reader.Seek(int64(length), io.SeekCurrent)
|
_, err = reader.Seek(int64(length), io.SeekCurrent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
metadata.Protocol = C.ProtocolBitTorrent
|
||||||
return &adapter.InboundContext{
|
return nil
|
||||||
Protocol: C.ProtocolBitTorrent,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UDPTracker detects if the packet is a UDP Tracker Protocol packet.
|
// UDPTracker detects if the packet is a UDP Tracker Protocol packet.
|
||||||
// For the UDP Tracker Protocol specification, see https://www.bittorrent.org/beps/bep_0015.html
|
// For the UDP Tracker Protocol specification, see https://www.bittorrent.org/beps/bep_0015.html
|
||||||
func UDPTracker(_ context.Context, packet []byte) (*adapter.InboundContext, error) {
|
func UDPTracker(_ context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
if len(packet) < trackerConnectMinSize {
|
if len(packet) < trackerConnectMinSize {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
if binary.BigEndian.Uint64(packet[:8]) != trackerProtocolID {
|
if binary.BigEndian.Uint64(packet[:8]) != trackerProtocolID {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
if binary.BigEndian.Uint32(packet[8:12]) != trackerConnectFlag {
|
if binary.BigEndian.Uint32(packet[8:12]) != trackerConnectFlag {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolBitTorrent}, nil
|
metadata.Protocol = C.ProtocolBitTorrent
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/sniff"
|
"github.com/sagernet/sing-box/common/sniff"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
|
||||||
|
@ -24,7 +25,8 @@ func TestSniffBittorrent(t *testing.T) {
|
||||||
for _, pkt := range packets {
|
for _, pkt := range packets {
|
||||||
pkt, err := hex.DecodeString(pkt)
|
pkt, err := hex.DecodeString(pkt)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
metadata, err := sniff.BitTorrent(context.TODO(), bytes.NewReader(pkt))
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
||||||
}
|
}
|
||||||
|
@ -43,8 +45,8 @@ func TestSniffUTP(t *testing.T) {
|
||||||
for _, pkt := range packets {
|
for _, pkt := range packets {
|
||||||
pkt, err := hex.DecodeString(pkt)
|
pkt, err := hex.DecodeString(pkt)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
var metadata adapter.InboundContext
|
||||||
metadata, err := sniff.UTP(context.TODO(), pkt)
|
err = sniff.UTP(context.TODO(), &metadata, pkt)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
||||||
}
|
}
|
||||||
|
@ -63,7 +65,8 @@ func TestSniffUDPTracker(t *testing.T) {
|
||||||
pkt, err := hex.DecodeString(pkt)
|
pkt, err := hex.DecodeString(pkt)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
metadata, err := sniff.UDPTracker(context.TODO(), pkt)
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.UDPTracker(context.TODO(), &metadata, pkt)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,18 +17,17 @@ import (
|
||||||
mDNS "github.com/miekg/dns"
|
mDNS "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
|
func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||||
var length uint16
|
var length uint16
|
||||||
err := binary.Read(reader, binary.BigEndian, &length)
|
err := binary.Read(reader, binary.BigEndian, &length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
buffer := buf.NewSize(int(length))
|
buffer := buf.NewSize(int(length))
|
||||||
defer buffer.Release()
|
defer buffer.Release()
|
||||||
|
|
||||||
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)
|
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)
|
||||||
var readTask task.Group
|
var readTask task.Group
|
||||||
readTask.Append0(func(ctx context.Context) error {
|
readTask.Append0(func(ctx context.Context) error {
|
||||||
|
@ -37,19 +36,20 @@ func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.
|
||||||
err = readTask.Run(readCtx)
|
err = readTask.Run(readCtx)
|
||||||
cancel()
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
return DomainNameQuery(readCtx, buffer.Bytes())
|
return DomainNameQuery(readCtx, metadata, buffer.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func DomainNameQuery(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
|
func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
var msg mDNS.Msg
|
var msg mDNS.Msg
|
||||||
err := msg.Unpack(packet)
|
err := msg.Unpack(packet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
|
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolDNS}, nil
|
metadata.Protocol = C.ProtocolDNS
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,24 +8,25 @@ import (
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DTLSRecord(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
|
func DTLSRecord(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
const fixedHeaderSize = 13
|
const fixedHeaderSize = 13
|
||||||
if len(packet) < fixedHeaderSize {
|
if len(packet) < fixedHeaderSize {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
contentType := packet[0]
|
contentType := packet[0]
|
||||||
switch contentType {
|
switch contentType {
|
||||||
case 20, 21, 22, 23, 25:
|
case 20, 21, 22, 23, 25:
|
||||||
default:
|
default:
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
versionMajor := packet[1]
|
versionMajor := packet[1]
|
||||||
if versionMajor != 0xfe {
|
if versionMajor != 0xfe {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
versionMinor := packet[2]
|
versionMinor := packet[2]
|
||||||
if versionMinor != 0xff && versionMinor != 0xfd {
|
if versionMinor != 0xff && versionMinor != 0xfd {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolDTLS}, nil
|
metadata.Protocol = C.ProtocolDTLS
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/sniff"
|
"github.com/sagernet/sing-box/common/sniff"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
|
||||||
|
@ -15,7 +16,8 @@ func TestSniffDTLSClientHello(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
packet, err := hex.DecodeString("16fefd0000000000000000007e010000720000000000000072fefd668a43523798e064bd806d0c87660de9c611a59bbdfc3892c4e072d94f2cafc40000000cc02bc02fc00ac014c02cc0300100003c000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100000e000900060008000700010000170000")
|
packet, err := hex.DecodeString("16fefd0000000000000000007e010000720000000000000072fefd668a43523798e064bd806d0c87660de9c611a59bbdfc3892c4e072d94f2cafc40000000cc02bc02fc00ac014c02cc0300100003c000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100000e000900060008000700010000170000")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
metadata, err := sniff.DTLSRecord(context.Background(), packet)
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.DTLSRecord(context.Background(), &metadata, packet)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
|
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
|
||||||
}
|
}
|
||||||
|
@ -24,7 +26,8 @@ func TestSniffDTLSClientApplicationData(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
packet, err := hex.DecodeString("17fefd000100000000000100440001000000000001a4f682b77ecadd10f3f3a2f78d90566212366ff8209fd77314f5a49352f9bb9bd12f4daba0b4736ae29e46b9714d3b424b3e6d0234736619b5aa0d3f")
|
packet, err := hex.DecodeString("17fefd000100000000000100440001000000000001a4f682b77ecadd10f3f3a2f78d90566212366ff8209fd77314f5a49352f9bb9bd12f4daba0b4736ae29e46b9714d3b424b3e6d0234736619b5aa0d3f")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
metadata, err := sniff.DTLSRecord(context.Background(), packet)
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.DTLSRecord(context.Background(), &metadata, packet)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
|
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,12 @@ import (
|
||||||
"github.com/sagernet/sing/protocol/http"
|
"github.com/sagernet/sing/protocol/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HTTPHost(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
|
func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||||
request, err := http.ReadRequest(std_bufio.NewReader(reader))
|
request, err := http.ReadRequest(std_bufio.NewReader(reader))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolHTTP, Domain: M.ParseSocksaddr(request.Host).AddrString()}, nil
|
metadata.Protocol = C.ProtocolHTTP
|
||||||
|
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/sniff"
|
"github.com/sagernet/sing-box/common/sniff"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -13,7 +14,8 @@ import (
|
||||||
func TestSniffHTTP1(t *testing.T) {
|
func TestSniffHTTP1(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n"
|
pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n"
|
||||||
metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt))
|
var metadata adapter.InboundContext
|
||||||
|
err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, metadata.Domain, "www.google.com")
|
require.Equal(t, metadata.Domain, "www.google.com")
|
||||||
}
|
}
|
||||||
|
@ -21,7 +23,8 @@ func TestSniffHTTP1(t *testing.T) {
|
||||||
func TestSniffHTTP1WithPort(t *testing.T) {
|
func TestSniffHTTP1WithPort(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n"
|
pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n"
|
||||||
metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt))
|
var metadata adapter.InboundContext
|
||||||
|
err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, metadata.Domain, "www.gov.cn")
|
require.Equal(t, metadata.Domain, "www.gov.cn")
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,95 +5,99 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/ja3"
|
||||||
"github.com/sagernet/sing-box/common/sniff/internal/qtls"
|
"github.com/sagernet/sing-box/common/sniff/internal/qtls"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
"golang.org/x/crypto/hkdf"
|
"golang.org/x/crypto/hkdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
|
var ErrClientHelloFragmented = E.New("need more packet for chromium QUIC connection")
|
||||||
reader := bytes.NewReader(packet)
|
|
||||||
|
|
||||||
|
func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
|
reader := bytes.NewReader(packet)
|
||||||
typeByte, err := reader.ReadByte()
|
typeByte, err := reader.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if typeByte&0x40 == 0 {
|
if typeByte&0x40 == 0 {
|
||||||
return nil, E.New("bad type byte")
|
return E.New("bad type byte")
|
||||||
}
|
}
|
||||||
var versionNumber uint32
|
var versionNumber uint32
|
||||||
err = binary.Read(reader, binary.BigEndian, &versionNumber)
|
err = binary.Read(reader, binary.BigEndian, &versionNumber)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if versionNumber != qtls.VersionDraft29 && versionNumber != qtls.Version1 && versionNumber != qtls.Version2 {
|
if versionNumber != qtls.VersionDraft29 && versionNumber != qtls.Version1 && versionNumber != qtls.Version2 {
|
||||||
return nil, E.New("bad version")
|
return E.New("bad version")
|
||||||
}
|
}
|
||||||
packetType := (typeByte & 0x30) >> 4
|
packetType := (typeByte & 0x30) >> 4
|
||||||
if packetType == 0 && versionNumber == qtls.Version2 || packetType == 2 && versionNumber != qtls.Version2 || packetType > 2 {
|
if packetType == 0 && versionNumber == qtls.Version2 || packetType == 2 && versionNumber != qtls.Version2 || packetType > 2 {
|
||||||
return nil, E.New("bad packet type")
|
return E.New("bad packet type")
|
||||||
}
|
}
|
||||||
|
|
||||||
destConnIDLen, err := reader.ReadByte()
|
destConnIDLen, err := reader.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if destConnIDLen == 0 || destConnIDLen > 20 {
|
if destConnIDLen == 0 || destConnIDLen > 20 {
|
||||||
return nil, E.New("bad destination connection id length")
|
return E.New("bad destination connection id length")
|
||||||
}
|
}
|
||||||
|
|
||||||
destConnID := make([]byte, destConnIDLen)
|
destConnID := make([]byte, destConnIDLen)
|
||||||
_, err = io.ReadFull(reader, destConnID)
|
_, err = io.ReadFull(reader, destConnID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
srcConnIDLen, err := reader.ReadByte()
|
srcConnIDLen, err := reader.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.CopyN(io.Discard, reader, int64(srcConnIDLen))
|
_, err = io.CopyN(io.Discard, reader, int64(srcConnIDLen))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenLen, err := qtls.ReadUvarint(reader)
|
tokenLen, err := qtls.ReadUvarint(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.CopyN(io.Discard, reader, int64(tokenLen))
|
_, err = io.CopyN(io.Discard, reader, int64(tokenLen))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
packetLen, err := qtls.ReadUvarint(reader)
|
packetLen, err := qtls.ReadUvarint(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hdrLen := int(reader.Size()) - reader.Len()
|
hdrLen := int(reader.Size()) - reader.Len()
|
||||||
if hdrLen+int(packetLen) > len(packet) {
|
if hdrLen+int(packetLen) > len(packet) {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.CopyN(io.Discard, reader, 4)
|
_, err = io.CopyN(io.Discard, reader, 4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pnBytes := make([]byte, aes.BlockSize)
|
pnBytes := make([]byte, aes.BlockSize)
|
||||||
_, err = io.ReadFull(reader, pnBytes)
|
_, err = io.ReadFull(reader, pnBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var salt []byte
|
var salt []byte
|
||||||
|
@ -117,7 +121,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
||||||
hpKey := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, hkdfHeaderProtectionLabel, 16)
|
hpKey := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, hkdfHeaderProtectionLabel, 16)
|
||||||
block, err := aes.NewCipher(hpKey)
|
block, err := aes.NewCipher(hpKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
mask := make([]byte, aes.BlockSize)
|
mask := make([]byte, aes.BlockSize)
|
||||||
block.Encrypt(mask, pnBytes)
|
block.Encrypt(mask, pnBytes)
|
||||||
|
@ -129,7 +133,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
||||||
}
|
}
|
||||||
packetNumberLength := newPacket[0]&0x3 + 1
|
packetNumberLength := newPacket[0]&0x3 + 1
|
||||||
if hdrLen+int(packetNumberLength) > int(packetLen)+hdrLen {
|
if hdrLen+int(packetNumberLength) > int(packetLen)+hdrLen {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
var packetNumber uint32
|
var packetNumber uint32
|
||||||
switch packetNumberLength {
|
switch packetNumberLength {
|
||||||
|
@ -142,7 +146,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
||||||
case 4:
|
case 4:
|
||||||
packetNumber = binary.BigEndian.Uint32(newPacket[hdrLen:])
|
packetNumber = binary.BigEndian.Uint32(newPacket[hdrLen:])
|
||||||
default:
|
default:
|
||||||
return nil, E.New("bad packet number length")
|
return E.New("bad packet number length")
|
||||||
}
|
}
|
||||||
extHdrLen := hdrLen + int(packetNumberLength)
|
extHdrLen := hdrLen + int(packetNumberLength)
|
||||||
copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:])
|
copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:])
|
||||||
|
@ -166,138 +170,208 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
||||||
binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber))
|
binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber))
|
||||||
decrypted, err := cipher.Open(newPacket[extHdrLen:extHdrLen], nonce, data, newPacket[:extHdrLen])
|
decrypted, err := cipher.Open(newPacket[extHdrLen:extHdrLen], nonce, data, newPacket[:extHdrLen])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
var frameType byte
|
var frameType byte
|
||||||
var frameLen uint64
|
var fragments []qCryptoFragment
|
||||||
var fragments []struct {
|
|
||||||
offset uint64
|
|
||||||
length uint64
|
|
||||||
payload []byte
|
|
||||||
}
|
|
||||||
decryptedReader := bytes.NewReader(decrypted)
|
decryptedReader := bytes.NewReader(decrypted)
|
||||||
|
const (
|
||||||
|
frameTypePadding = 0x00
|
||||||
|
frameTypePing = 0x01
|
||||||
|
frameTypeAck = 0x02
|
||||||
|
frameTypeAck2 = 0x03
|
||||||
|
frameTypeCrypto = 0x06
|
||||||
|
frameTypeConnectionClose = 0x1c
|
||||||
|
)
|
||||||
|
var frameTypeList []uint8
|
||||||
for {
|
for {
|
||||||
frameType, err = decryptedReader.ReadByte()
|
frameType, err = decryptedReader.ReadByte()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
frameTypeList = append(frameTypeList, frameType)
|
||||||
switch frameType {
|
switch frameType {
|
||||||
case 0x00: // PADDING
|
case frameTypePadding:
|
||||||
continue
|
continue
|
||||||
case 0x01: // PING
|
case frameTypePing:
|
||||||
continue
|
continue
|
||||||
case 0x02, 0x03: // ACK
|
case frameTypeAck, frameTypeAck2:
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged
|
_, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ACK Delay
|
_, err = qtls.ReadUvarint(decryptedReader) // ACK Delay
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count
|
ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // First ACK Range
|
_, err = qtls.ReadUvarint(decryptedReader) // First ACK Range
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
for i := 0; i < int(ackRangeCount); i++ {
|
for i := 0; i < int(ackRangeCount); i++ {
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Gap
|
_, err = qtls.ReadUvarint(decryptedReader) // Gap
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length
|
_, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if frameType == 0x03 {
|
if frameType == 0x03 {
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count
|
_, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count
|
_, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count
|
_, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 0x06: // CRYPTO
|
case frameTypeCrypto:
|
||||||
var offset uint64
|
var offset uint64
|
||||||
offset, err = qtls.ReadUvarint(decryptedReader)
|
offset, err = qtls.ReadUvarint(decryptedReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
|
return err
|
||||||
}
|
}
|
||||||
var length uint64
|
var length uint64
|
||||||
length, err = qtls.ReadUvarint(decryptedReader)
|
length, err = qtls.ReadUvarint(decryptedReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
|
return err
|
||||||
}
|
}
|
||||||
index := len(decrypted) - decryptedReader.Len()
|
index := len(decrypted) - decryptedReader.Len()
|
||||||
fragments = append(fragments, struct {
|
fragments = append(fragments, qCryptoFragment{offset, length, decrypted[index : index+int(length)]})
|
||||||
offset uint64
|
|
||||||
length uint64
|
|
||||||
payload []byte
|
|
||||||
}{offset, length, decrypted[index : index+int(length)]})
|
|
||||||
frameLen += length
|
|
||||||
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent)
|
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
case 0x1c: // CONNECTION_CLOSE
|
case frameTypeConnectionClose:
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Error Code
|
_, err = qtls.ReadUvarint(decryptedReader) // Error Code
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Frame Type
|
_, err = qtls.ReadUvarint(decryptedReader) // Frame Type
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
var length uint64
|
var length uint64
|
||||||
length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length
|
length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase
|
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tlsHdr := make([]byte, 5)
|
if metadata.SniffContext != nil {
|
||||||
tlsHdr[0] = 0x16
|
fragments = append(fragments, metadata.SniffContext.([]qCryptoFragment)...)
|
||||||
binary.BigEndian.PutUint16(tlsHdr[1:], uint16(0x0303))
|
metadata.SniffContext = nil
|
||||||
binary.BigEndian.PutUint16(tlsHdr[3:], uint16(frameLen))
|
}
|
||||||
|
var frameLen uint64
|
||||||
|
for _, fragment := range fragments {
|
||||||
|
frameLen += fragment.length
|
||||||
|
}
|
||||||
|
buffer := buf.NewSize(5 + int(frameLen))
|
||||||
|
defer buffer.Release()
|
||||||
|
buffer.WriteByte(0x16)
|
||||||
|
binary.Write(buffer, binary.BigEndian, uint16(0x0303))
|
||||||
|
binary.Write(buffer, binary.BigEndian, uint16(frameLen))
|
||||||
var index uint64
|
var index uint64
|
||||||
var length int
|
var length int
|
||||||
var readers []io.Reader
|
|
||||||
readers = append(readers, bytes.NewReader(tlsHdr))
|
|
||||||
find:
|
find:
|
||||||
for {
|
for {
|
||||||
for _, fragment := range fragments {
|
for _, fragment := range fragments {
|
||||||
if fragment.offset == index {
|
if fragment.offset == index {
|
||||||
readers = append(readers, bytes.NewReader(fragment.payload))
|
buffer.Write(fragment.payload)
|
||||||
index = fragment.offset + fragment.length
|
index = fragment.offset + fragment.length
|
||||||
length++
|
length++
|
||||||
continue find
|
continue find
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if length == len(fragments) {
|
break
|
||||||
break
|
|
||||||
}
|
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, E.New("bad fragments")
|
|
||||||
}
|
|
||||||
metadata, err := TLSClientHello(ctx, io.MultiReader(readers...))
|
|
||||||
if err != nil {
|
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
|
|
||||||
}
|
}
|
||||||
metadata.Protocol = C.ProtocolQUIC
|
metadata.Protocol = C.ProtocolQUIC
|
||||||
return metadata, nil
|
fingerprint, err := ja3.Compute(buffer.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
metadata.Protocol = C.ProtocolQUIC
|
||||||
|
metadata.Client = C.ClientChromium
|
||||||
|
metadata.SniffContext = fragments
|
||||||
|
return ErrClientHelloFragmented
|
||||||
|
}
|
||||||
|
metadata.Domain = fingerprint.ServerName
|
||||||
|
for metadata.Client == "" {
|
||||||
|
if len(frameTypeList) == 1 {
|
||||||
|
metadata.Client = C.ClientFirefox
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if frameTypeList[0] == frameTypeCrypto && isZero(frameTypeList[1:]) {
|
||||||
|
if len(fingerprint.Versions) == 2 && fingerprint.Versions[0]&ja3.GreaseBitmask == 0x0A0A &&
|
||||||
|
len(fingerprint.EllipticCurves) == 5 && fingerprint.EllipticCurves[0]&ja3.GreaseBitmask == 0x0A0A {
|
||||||
|
metadata.Client = C.ClientSafari
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if len(fingerprint.CipherSuites) == 1 && fingerprint.CipherSuites[0] == tls.TLS_AES_256_GCM_SHA384 &&
|
||||||
|
len(fingerprint.EllipticCurves) == 1 && fingerprint.EllipticCurves[0] == uint16(tls.X25519) &&
|
||||||
|
len(fingerprint.SignatureAlgorithms) == 1 && fingerprint.SignatureAlgorithms[0] == uint16(tls.ECDSAWithP256AndSHA256) {
|
||||||
|
metadata.Client = C.ClientSafari
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if frameTypeList[len(frameTypeList)-1] == frameTypeCrypto && isZero(frameTypeList[:len(frameTypeList)-1]) {
|
||||||
|
metadata.Client = C.ClientQUICGo
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
|
||||||
|
if maybeUQUIC(fingerprint) {
|
||||||
|
metadata.Client = C.ClientQUICGo
|
||||||
|
} else {
|
||||||
|
metadata.Client = C.ClientChromium
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.Client = C.ClientUnknown
|
||||||
|
//nolint:staticcheck
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isZero(slices []uint8) bool {
|
||||||
|
for _, slice := range slices {
|
||||||
|
if slice != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func count(slices []uint8, value uint8) int {
|
||||||
|
var times int
|
||||||
|
for _, slice := range slices {
|
||||||
|
if slice == value {
|
||||||
|
times++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return times
|
||||||
|
}
|
||||||
|
|
||||||
|
type qCryptoFragment struct {
|
||||||
|
offset uint64
|
||||||
|
length uint64
|
||||||
|
payload []byte
|
||||||
}
|
}
|
||||||
|
|
24
common/sniff/quic_blacklist.go
Normal file
24
common/sniff/quic_blacklist.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package sniff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/ja3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior
|
||||||
|
// The cronet without this behavior does not have version 115
|
||||||
|
var uQUICChrome115 = &ja3.ClientHello{
|
||||||
|
Version: tls.VersionTLS12,
|
||||||
|
CipherSuites: []uint16{4865, 4866, 4867},
|
||||||
|
Extensions: []uint16{0, 10, 13, 16, 27, 43, 45, 51, 57, 17513},
|
||||||
|
EllipticCurves: []uint16{29, 23, 24},
|
||||||
|
SignatureAlgorithms: []uint16{1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513},
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeUQUIC(fingerprint *ja3.ClientHello) bool {
|
||||||
|
if uQUICChrome115.Equals(fingerprint, true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -5,31 +5,69 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/sniff"
|
"github.com/sagernet/sing-box/common/sniff"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSniffQUICv1(t *testing.T) {
|
func TestSniffQUICChromium(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
pkt, err := hex.DecodeString("cc0000000108d2dc7bad02241f5003796e71004215a71bfcb05159416c724be418537389acdd9a4047306283dcb4d7a9cad5cc06322042d204da67a8dbaa328ab476bb428b48fd001501863afd203f8d4ef085629d664f1a734a65969a47e4a63d4e01a21f18c1d90db0c027180906dc135f9ae421bb8617314c8d54c175fef3d3383d310d0916ebcbd6eed9329befbbb109d8fd4af1d2cf9d6adce8e6c1260a7f8256e273e326da0aa7cc148d76e7a08489dc9d52ade89c027cbc3491ada46417c2c04e2ca768e9a7dd6aa00c594e48b678927325da796817693499bb727050cb3baf3d3291a397c3a8d868e8ec7b8f7295e347455c9dadbe2252ae917ac793d958c7fb8a3d2cdb34e3891eb4286f18617556ff7216dd60256aa5b1d11ff4753459fc5f9dedf11d483a26a0835dc6cd50e1c1f54f86e8f1e502821183cd874f6447a74e818bf3445c7795acf4559d1c1fac474911d2ead5c8d23e4aa4f67afb66efe305a30a0b5d825679b31ddc186cbea936535795c7e8c378c87b8c5adc065154d15bae8f85ac8fec2da40c3aa623b682a065440831555011d7647cde44446a0fb4cf5892f2c088ae1920643094be72e3c499fe8d265caf939e8ab607a5b9317917d2a32a812e8a0e6a2f84721bbb5984ffd242838f705d13f4cfb249bc6a5c80d58ac2595edf56648ec3fe21d787573c253a79805252d6d81e26d367d4ff29ef66b5fe8992086af7bada8cad10b82a7c0dc406c5b6d0c5ec3c583e767f759ce08cad6c3c8f91e5a8")
|
pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
metadata, err := sniff.QUICClientHello(context.Background(), pkt)
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
|
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||||
|
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
|
||||||
|
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, metadata.Domain, "cloudflare-quic.com")
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, metadata.Domain, "google.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSniffQUICFragment(t *testing.T) {
|
func TestSniffUQUICChrome115(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
pkt, err := hex.DecodeString("cc00000001082e3d5d1b64040c55000044d0ccea69e773f6631c1d18b04ae9ee75fcfc34ef74fa62533c93534338a86f101a05d70e0697fb483063fa85db1c59ccfbda5c35234931d8524d8aac37eaaad649470a67794cd754b23c98695238b8363452333bc8c4858376b4166e001da2006e35cf98a91e11a56419b2786775284942d0f7163982f7c248867d12dd374957481dbc564013ff785e1916195eef671f725908f761099d992d69231336ba81d9e25fe2fa3a6eff4318a6ccf10176fc841a1b315f7b35c5b292266fc869d76ca533e7d14e86d82db2e22eacd350977e47d2e012d8a5891c5aaf2a0f4c2b2dae897c161e5b68cbb4dee952472bdc1e21504b8f02534ec4366ce3f8bf86efc78e0232778fbd554457567112abdcafcf6d4d8fcf35083c25d9495679614aba21696e338c62b585046cc55ba8c09c844361d889a47c3ea703b4e23545a9ab2c0bb369693a9ddfb5daffa85cf80fdd6ad66738664e5b0a551729b4955cff7255afcb04dee88c2f072c9de7400947a1bd9327ac5d012a33000ada021d4c03d249fb017d6ac9200b2f9436beab8183ddfbe2d8aee31ffb7df9e1cc181c1af80c39a89965d18ed12da8e3ebe2ae1fbe4b348f83ba19e3e3d1c9b22bcf03ab6ad9b30fe180623faa291ebad83bcd71d7b57f2f5e2f3b8e81d24fb70b2f2159239e8f21ffafef2747aba47d97ab4081e603c018b10678cf99cab1fb42156a14486fa435153979d7279fd22cd40af7088bfc7eff41af2f4b3c0c8864d0040d74dff427f7bffdb8c278474ea00311326cf4925471a8cf596cb92119f19e0f789490ba9cb77b98015a987d93e0324cf1a38b55109f00c3e6ddc5180fb107bf468323afec9bb49fd6a86418569789d66cafe3b8253c2aebb3af3782c1c54dd560487d031d28e6a6e23e159581bb1d47efc4da3fe1d169f9ffb0ca9ba61af0a38a92fde5bc5e6ec026e8378a6315a7b95abf1d2da790a391306ce74d0baf8e2ce648ca74c487f2c0a76a28a80cdf5bd34316eb607684fe7e6d9e83824a00e07660d0b90e3cddd61ebf10748263474afa88c300549e64ce2e90560bb1a12dee7e9484f729a8a4ee7c5651adb5194b3b3ae38e501567c7dbf36e7bb37a2c20b74655f47f2d9af18e52e9d4c9c9eee8e63745779b8f0b06f3a09d846ba62eb978ad77c85de1ee2fee3fbb4c2d283c73e1ccba56a4658e48a2665d200f7f9342f8e84c2ba490094a4f94feec89e42d2f654f564c2beb2997bafa1fc2c68ad8e160b63587d49abc31b834878d52acfb05fb73d0e059b206162e3c90b40c4bc08407ffcb3c08431895b691a3fea923f1f3b48db75d3e6b91fd319ffe4d486e0e14bd5c6affc838dee63d9e0b80f169b5e6c02c7321dcb20deb2b8e707b60e345a308d505bbf26a93d8f18b39d62632e9a77cbe48b3b32eb8819d6311a49820d40f5acbf0273c91c36b2269a03e72ee64df3dfb10ddefe73c64ef60870b2b77bd99dea655f5fe791b538a929a14d99f6d69685d72431ea5f0f4b27a044f2f575ab474fcc3857895934de1ca2581798eaef2c17fe5aaf2e6add97fa32997c7026f15c1b1ad0e6043ae506027a7c0242546fdc851cca39a204e56879f2cef838be8ec66e0f2292f8c862e06f810eb9b80c7a467ce6e90155206352c7f82b1173ba3b98d35bb72c259a60db20dd1a43fe6d7aef0265e6eaa5caafd9b64b448ff745a2046acbdb65cf2a5007809808a4828dc99097feedc734c236260c584")
|
pkt, err := hex.DecodeString("cb0000000108181e17c387120abc000044d0705b6a3ef9ee37a8d3949a7d393ed078243c2ee2c3627fad1c3f107c117f4f071131ad61848068fcbbe5c65803c147f7f8ec5e2cd77b77beea23ba779d936dccac540f8396400e3190ea35cc2942af4171a04cb14272491920f90124959f44e80143678c0b52f5d31af319aaa589db2f940f004562724d0af40f737e1bb0002a071e6a1dbc9f52c64f070806a5010abed0298053634d9c9126bd7949ae5087998ade762c0ad06691d99c0875a38c601fc1ee77bfc3b8c11381829f2c9bdd022f4499c43ff1d6aee1a0d296861461dda217d22c568b276016ef3929e59d2f7d7ddf7809920fb7dc805641608949f3f8466ab3d37149aac501f0b107d808f3add4acfc657e4a82e2b88e97a6c74a00c419548760ab3414ba13915c78a1ca79dceee8d59fbe299f20b671ac44823218368b2a026baa55170cf549519ac21dbb6d31d248bd339438a4e663bcdca1fe3ae3f045a5dc19b122e9db9d7af9757076666dda4e9ace1c67def77fa14786f0cab3ebf7a270ea6e2b37838318c95779f80c3b8471948d0046c3614b3a13477c939a39a7855d85d13522a45ae0765739cd5eedef87237e824a929983ace27640c6495dbf5a72fa0b96893dc5d28f3988249a57bdb458d460b4a57043de3da750a76b6e5d2259247ca27cd864ea18f0d09aa62ab6eb7c014fb43179b2a1963d170b756cce83eeaebff78a828d025c811848e16ff862a8080d093478cd2208c8ab0803178325bc0d9d6bb25e62fa50c4ad15cf80916da6578796932036c72e43eb480d1e423ed812ac75a97722f8416529b82ba8ee2219c535012282bb17066bd53e78b87a71abdb7ebdb2a7c2766ff8397962e87d0f85485b64b4ee81cc84f99c47f33f2b0872716441992773f59186e38d32dbf5609a6fda94cb928cd25f5a7a3ab736b5a4236b6d5409ab18892c6a4d3480fc2350abfdf0bab1cedb55bdf0760fdb703e6688f4de596254eed4ed3e67eb03d0717b8e15b31e735214e588c87ae36bc6c310e1894b4c15143e4ccf287b2dbc707a946bf9671ae3c574f9486b2c82eec784bba4cbc76113cbe0f97ac8c13cfa38f2925ab9d06887a612ce48280a91d7e074e6caf898d88e2bbf71360899abf48a03f9a70cf2891199f2d63b116f4871af0ebb4f4906792f66cc21d1609f189138532875c129a68c73e7bcd3b5d8100beac1d8ac4b20d94a59ac8df5a5af58a9acb20413eadf97189f5f19ff889155f0c4d37514ec184eb6903967ff38a41fc087abb0f2cad3761d6e3f95f92a09a72f5c065b16e188088b87460241f27ecdb1bc6ece92c8d36b2d68b58d0fb4d4b3c928c579ade8ae5a995833aadd297c30a37f7bc35440fc97070e1b198e0fac00157452177d16d2803b4239997452b4ad3a951173bdec47a033fd7f8a7942accaa9aaa905b3c5a2175e7c3e07c48bf25331727fd69cd1e64d74d8c9d4a6f8f4491adb7bc911505cb19877083d8f21a12475e313fccf57877ff3556318e81ed9145dd9427f2b65275440893035f417481f721c69215af8ae103530cd0a1d35bf2cb5a27628f8d44d7c6f5ec12ce79d0a8333e0eb48771115d0a191304e46b8db19bbe5c40f1c346dde98e76ff5e21ff38d2c34e60cb07766ed529dd6d2cbacd7fbf1ed8a0e6e40decad0ca5021e91552be87c156d3ae2fffef41c65b14ba6d488f2c3227a1ab11ffce0e2dc47723a69da27a67a7f26e1cb13a7103af9b87a8db8e18ea")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
metadata, err := sniff.QUICClientHello(context.Background(), pkt)
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, metadata.Domain, "cloudflare-quic.com")
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
|
require.Equal(t, metadata.Client, C.ClientQUICGo)
|
||||||
|
require.Equal(t, metadata.Domain, "www.google.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSniffQUICFirefox(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
pkt, err := hex.DecodeString("c8000000010867f174d7ebfe1b0803cd9c20004286de068f7963cf1736349ee6ebe0ddcd3e4cd0041a51ced3f7ce9eea1fb595458e74bdb4b792b16449bd8cae71419862c4fcbe766eaec7d1af65cd298e1dd46f8bd94a77ab4ca28c54b8e9773de3f02d7cb2463c9f7dcacfb311f024b0266ec6ab7bfb615b4148333fb4d4ece7c4cd90029ca30c2cbae2216b428499ec873fa125797e71c5a5da85087760ad37ca610020f71b76e82651c47576e20bf33cf676cb2d400b8c09d3c8cb4e21c47d2b21f6b68732bef30c8cefd5c723fc23eb29e6f7f65a5e52aad9055c1fb3d8b1811f0380b38d7e2eee8eb37dd5bd5d4ca4b66540175d916289d88a9df7c161964d713999c5057d27edb298ef5164352568b0d4bac3c15d90456e8fd460e41b81d0ec1b1e94b87d3333cc6908b018e0914ae1f214d73e75398da3d55a0106161d3a75897b4eb66e98c59010fae75f0d367d38be48c3a5c58bc8a30773c3fff50690ac9d487822f85d4f5713d626baa92d36e858dd21259cf814bce0b90d18da88a1ade40113e5a088cdb304a2558879152a8cf15c1839e056378aa41acba6fcb9974dee54bd50b5d4eb2c475654e06c0ec06b7f18f4462c808684843a1071041b9bfb2688324e0120144944416e30e83eedbbbcbc275b1f53762d3db18f0998ce54f0e1c512946b4098f07781d49264fa148f4c8220a3b02e73d7f15554aa370aafeff73cb75c52c494edf90f0261abfdd32a4d670f729de50266162687aa8efe14b8506f313b058b02aaaab5825428f5f4510b8e49451fdcb7b5a4af4b59c831afcb89fb4f64dba78e3b38387e87e9e8cdaa1f3b700a87c7d442388863b8950296e5773b38f308d62f52548c0bbf308e40540747cca5bf99b1345bc0d70b8f0e69a83b85a8d69f795b87f93e2bfccf52b529afea4ff6fd456957000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
|
||||||
|
require.NoError(t, err)
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
|
require.Equal(t, metadata.Client, C.ClientFirefox)
|
||||||
|
require.Equal(t, metadata.Domain, "www.google.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSniffQUICSafari(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
pkt, err := hex.DecodeString("c70000000108e4e75af2e223198a0000449ef2d83cb4473a62765eba67424cd4a5817315cbf55a9e8daaca360904b0bae60b1629cfeba11e2dfbbf5ea4c588cb134e31af36fd7a409fb0fcc0187e9b56037ac37964ed20a8c1ca19fd6cfd53398324b3d0c71537294f769db208fa998b6811234a4a7eb3b5eceb457ae92e3a2d98f7c110702db8064b5c29fa3298eb1d0529fd445a84a5fd6ff8709be90f8af4f94998d8a8f2953bb05ad08c80668eca784c6aec959114e68e5b827e7c41c79f2277c716a967e7fcc8d1b77442e6cb18329dbedb34b473516b468cba5fc20659e655fbe37f36408289b9a475fcee091bd82828d3be00367e9e5cec9423bb97854abdada1d7562a3777756eb3bddef826ddc1ef46137cb01bb504a54d410d9bcb74cd5f959050c84edf343fa6a49708c228a758ee7adbbadf260b2f1984911489712e2cb364a3d6520badba4b7e539b9c163eeddfd96c0abb0de151e47496bb9750be76ee17ccdb61d35d2c6795174037d6f9d282c3f36c4d9a90b64f3b6ddd0cf4d9ed8e6f7805e25928fa04b087e63ae02761df30720cc01dfc32b64c575c8a66ef82e9a17400ff80cd8609b93ba16d668f4aa734e71c4a5d145f14ee1151bec970214e0ff83fc3e1e85d8694f2975f9155c57c18b7b69bb6a36832a9435f1f4b346a7be188f3a75f9ad2cc6ad0a3d26d6fa7d4c1179bd49bd5989d15ba43ff602890107db96484695086627356750d7b2b3b714ba65d564654e8f60ac10f5b6d3bfb507e8eaa31bab1da2d676195046d165c7f8b32829c9f9b68d97b2af7ac04a1369357e4b65de2b2f24eaf27cc8d95e05db001adebe726f927a94e43e62ce671e6e306e16f05aafcbe6c49080e80286d7939f375023d110a5ad9069364ae928ca480454a9dcddd61bc48b7efeb716a5bd6c7cd39c486ceb20c738af6abf22ba1ddd8b4a3b781fc2f251173409e1aadccbd7514e97106d0ebfc3af6e59445f74cd733a1ba99b10fce3fb4e9f7c88f5e25b567f5ba2b8dabacd375e7faf7634bfa178cbe51aee63032c5126b196ea47b02385fc3062a000fb7e4b4d0d12e74579f8830ede20d10829496032b2cc56743287f9a9b4d5091877a82fea44deb2cffac8a379f78a151d99e28cbc74d732c083bf06d50584e3f18f254e71a48d6ababaf6fff6f425e9be001510dfbe6a32a27792c00ada036b62ddb90c706d7b882c76a7072f5dd11c69a1f49d4ba183cb0b57545419fa27b9b9706098848935ae9c9e8fbe9fac165d1339128b991a73d20e7795e8d6a8c6adfbf20bf13ada43f2aef3ba78c14697910507132623f721387dce60c4707225b84d9782d469a5d9eaa099f35d6a590ef142ddef766495cf3337815ceef5ff2b3ed352637e72b5c23a2a8ff7d7440236a19b981d47f8e519a0431ebfbc0b78d8a36798b4c060c0c6793499f1e2e818862560a5b501c8d02ba1517be1941da2af5b174e0189c62978d878eb0f9c9db3a9221c28fb94645cf6e85ff2eea8c65ba3083a7382b131b83102dd67aa5453ad7375a4eb8c69fc479fbd29dab8924f801d253f2c997120b705c6e5217fb74702e2f1038917dd5fb0eeb7ae1bf7a668fc7d50c034b4cd5a057a8482e6bc9c921297f44e76967265623a167cd9883eb6e64bc77856dc333bd605d7df3bed0e5cecb5a99fe8b62873d58530f")
|
||||||
|
require.NoError(t, err)
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
|
require.Equal(t, metadata.Client, C.ClientSafari)
|
||||||
|
require.Equal(t, metadata.Domain, "www.google.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuzzSniffQUIC(f *testing.F) {
|
func FuzzSniffQUIC(f *testing.F) {
|
||||||
f.Fuzz(func(t *testing.T, data []byte) {
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
sniff.QUICClientHello(context.Background(), data)
|
var metadata adapter.InboundContext
|
||||||
|
err := sniff.QUICClientHello(context.Background(), &metadata, data)
|
||||||
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
StreamSniffer = func(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error)
|
StreamSniffer = func(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error
|
||||||
PacketSniffer = func(ctx context.Context, packet []byte) (*adapter.InboundContext, error)
|
PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
|
||||||
)
|
)
|
||||||
|
|
||||||
func Skip(metadata adapter.InboundContext) bool {
|
func Skip(metadata adapter.InboundContext) bool {
|
||||||
|
@ -34,7 +34,7 @@ func Skip(metadata adapter.InboundContext) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) (*adapter.InboundContext, error) {
|
func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error {
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
timeout = C.ReadPayloadTimeout
|
timeout = C.ReadPayloadTimeout
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout
|
||||||
for i := 0; ; i++ {
|
for i := 0; ; i++ {
|
||||||
err := conn.SetReadDeadline(deadline)
|
err := conn.SetReadDeadline(deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "set read deadline")
|
return E.Cause(err, "set read deadline")
|
||||||
}
|
}
|
||||||
_, err = buffer.ReadOnceFrom(conn)
|
_, err = buffer.ReadOnceFrom(conn)
|
||||||
_ = conn.SetReadDeadline(time.Time{})
|
_ = conn.SetReadDeadline(time.Time{})
|
||||||
|
@ -51,29 +51,28 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return nil, E.Cause(err, "read payload")
|
return E.Cause(err, "read payload")
|
||||||
}
|
}
|
||||||
errors = nil
|
errors = nil
|
||||||
var metadata *adapter.InboundContext
|
|
||||||
for _, sniffer := range sniffers {
|
for _, sniffer := range sniffers {
|
||||||
metadata, err = sniffer(ctx, bytes.NewReader(buffer.Bytes()))
|
err = sniffer(ctx, metadata, bytes.NewReader(buffer.Bytes()))
|
||||||
if metadata != nil {
|
if err == nil {
|
||||||
return metadata, nil
|
return nil
|
||||||
}
|
}
|
||||||
errors = append(errors, err)
|
errors = append(errors, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, E.Errors(errors...)
|
return E.Errors(errors...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PeekPacket(ctx context.Context, packet []byte, sniffers ...PacketSniffer) (*adapter.InboundContext, error) {
|
func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error {
|
||||||
var errors []error
|
var errors []error
|
||||||
for _, sniffer := range sniffers {
|
for _, sniffer := range sniffers {
|
||||||
metadata, err := sniffer(ctx, packet)
|
err := sniffer(ctx, metadata, packet)
|
||||||
if metadata != nil {
|
if err == nil {
|
||||||
return metadata, nil
|
return nil
|
||||||
}
|
}
|
||||||
errors = append(errors, err)
|
errors = append(errors, err)
|
||||||
}
|
}
|
||||||
return nil, E.Errors(errors...)
|
return E.Errors(errors...)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,16 +9,17 @@ import (
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
func STUNMessage(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
|
func STUNMessage(_ context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
pLen := len(packet)
|
pLen := len(packet)
|
||||||
if pLen < 20 {
|
if pLen < 20 {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
if binary.BigEndian.Uint32(packet[4:8]) != 0x2112A442 {
|
if binary.BigEndian.Uint32(packet[4:8]) != 0x2112A442 {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
if len(packet) < 20+int(binary.BigEndian.Uint16(packet[2:4])) {
|
if len(packet) < 20+int(binary.BigEndian.Uint16(packet[2:4])) {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolSTUN}, nil
|
metadata.Protocol = C.ProtocolSTUN
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/sniff"
|
"github.com/sagernet/sing-box/common/sniff"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
|
||||||
|
@ -15,14 +16,16 @@ func TestSniffSTUN(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
packet, err := hex.DecodeString("000100002112a44224b1a025d0c180c484341306")
|
packet, err := hex.DecodeString("000100002112a44224b1a025d0c180c484341306")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
metadata, err := sniff.STUNMessage(context.Background(), packet)
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.STUNMessage(context.Background(), &metadata, packet)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, metadata.Protocol, C.ProtocolSTUN)
|
require.Equal(t, metadata.Protocol, C.ProtocolSTUN)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuzzSniffSTUN(f *testing.F) {
|
func FuzzSniffSTUN(f *testing.F) {
|
||||||
f.Fuzz(func(t *testing.T, data []byte) {
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
if _, err := sniff.STUNMessage(context.Background(), data); err == nil {
|
var metadata adapter.InboundContext
|
||||||
|
if err := sniff.STUNMessage(context.Background(), &metadata, data); err == nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TLSClientHello(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
|
func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||||
var clientHello *tls.ClientHelloInfo
|
var clientHello *tls.ClientHelloInfo
|
||||||
err := tls.Server(bufio.NewReadOnlyConn(reader), &tls.Config{
|
err := tls.Server(bufio.NewReadOnlyConn(reader), &tls.Config{
|
||||||
GetConfigForClient: func(argHello *tls.ClientHelloInfo) (*tls.Config, error) {
|
GetConfigForClient: func(argHello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||||
|
@ -19,7 +19,9 @@ func TLSClientHello(ctx context.Context, reader io.Reader) (*adapter.InboundCont
|
||||||
},
|
},
|
||||||
}).HandshakeContext(ctx)
|
}).HandshakeContext(ctx)
|
||||||
if clientHello != nil {
|
if clientHello != nil {
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolTLS, Domain: clientHello.ServerName}, nil
|
metadata.Protocol = C.ProtocolTLS
|
||||||
|
metadata.Domain = clientHello.ServerName
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,3 +9,11 @@ const (
|
||||||
ProtocolBitTorrent = "bittorrent"
|
ProtocolBitTorrent = "bittorrent"
|
||||||
ProtocolDTLS = "dtls"
|
ProtocolDTLS = "dtls"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClientChromium = "chromium"
|
||||||
|
ClientSafari = "safari"
|
||||||
|
ClientFirefox = "firefox"
|
||||||
|
ClientQUICGo = "quic-go"
|
||||||
|
ClientUnknown = "unknown"
|
||||||
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ icon: material/alert-decagram
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.10.0"
|
!!! quote "Changes in sing-box 1.10.0"
|
||||||
|
|
||||||
|
:material-plus: [client](#client)
|
||||||
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
|
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
|
||||||
:material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source)
|
:material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source)
|
||||||
|
|
||||||
|
@ -40,6 +41,12 @@ icon: material/alert-decagram
|
||||||
"http",
|
"http",
|
||||||
"quic"
|
"quic"
|
||||||
],
|
],
|
||||||
|
"client": [
|
||||||
|
"chromium",
|
||||||
|
"safari",
|
||||||
|
"firefox",
|
||||||
|
"quic-go"
|
||||||
|
],
|
||||||
"domain": [
|
"domain": [
|
||||||
"test.com"
|
"test.com"
|
||||||
],
|
],
|
||||||
|
@ -166,7 +173,13 @@ Username, see each inbound for details.
|
||||||
|
|
||||||
#### protocol
|
#### protocol
|
||||||
|
|
||||||
Sniffed protocol, see [Sniff](/configuration/route/sniff/) for details.
|
Sniffed protocol, see [Protocol Sniff](/configuration/route/sniff/) for details.
|
||||||
|
|
||||||
|
#### client
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.10.0"
|
||||||
|
|
||||||
|
Sniffed client type, see [Protocol Sniff](/configuration/route/sniff/) for details.
|
||||||
|
|
||||||
#### network
|
#### network
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,9 @@ icon: material/alert-decagram
|
||||||
|
|
||||||
!!! quote "sing-box 1.10.0 中的更改"
|
!!! quote "sing-box 1.10.0 中的更改"
|
||||||
|
|
||||||
|
:material-plus: [client](#client)
|
||||||
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
|
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
|
||||||
:material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source)
|
:material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source)
|
||||||
|
|
||||||
!!! quote "sing-box 1.8.0 中的更改"
|
!!! quote "sing-box 1.8.0 中的更改"
|
||||||
|
|
||||||
|
@ -40,6 +41,12 @@ icon: material/alert-decagram
|
||||||
"http",
|
"http",
|
||||||
"quic"
|
"quic"
|
||||||
],
|
],
|
||||||
|
"client": [
|
||||||
|
"chromium",
|
||||||
|
"safari",
|
||||||
|
"firefox",
|
||||||
|
"quic-go"
|
||||||
|
],
|
||||||
"domain": [
|
"domain": [
|
||||||
"test.com"
|
"test.com"
|
||||||
],
|
],
|
||||||
|
@ -166,6 +173,12 @@ icon: material/alert-decagram
|
||||||
|
|
||||||
探测到的协议, 参阅 [协议探测](/zh/configuration/route/sniff/)。
|
探测到的协议, 参阅 [协议探测](/zh/configuration/route/sniff/)。
|
||||||
|
|
||||||
|
#### client
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.10.0 起"
|
||||||
|
|
||||||
|
探测到的客户端类型, 参阅 [协议探测](/zh/configuration/route/sniff/)。
|
||||||
|
|
||||||
#### network
|
#### network
|
||||||
|
|
||||||
`tcp` 或 `udp`。
|
`tcp` 或 `udp`。
|
||||||
|
|
|
@ -4,19 +4,28 @@ icon: material/new-box
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.10.0"
|
!!! quote "Changes in sing-box 1.10.0"
|
||||||
|
|
||||||
:material-plus: BitTorrent support
|
:material-plus: QUIC client type detect support for QUIC
|
||||||
|
:material-plus: Chromium support for QUIC
|
||||||
|
:material-plus: BitTorrent support
|
||||||
:material-plus: DTLS support
|
:material-plus: DTLS support
|
||||||
|
|
||||||
If enabled in the inbound, the protocol and domain name (if present) of by the connection can be sniffed.
|
If enabled in the inbound, the protocol and domain name (if present) of by the connection can be sniffed.
|
||||||
|
|
||||||
#### Supported Protocols
|
#### Supported Protocols
|
||||||
|
|
||||||
| Network | Protocol | Domain Name |
|
| Network | Protocol | Domain Name | Client |
|
||||||
|:-------:|:------------:|:-----------:|
|
|:-------:|:------------:|:-----------:|:----------------:|
|
||||||
| TCP | `http` | Host |
|
| TCP | `http` | Host | / |
|
||||||
| TCP | `tls` | Server Name |
|
| TCP | `tls` | Server Name | / |
|
||||||
| UDP | `quic` | Server Name |
|
| UDP | `quic` | Server Name | QUIC Client Type |
|
||||||
| UDP | `stun` | / |
|
| UDP | `stun` | / | / |
|
||||||
| TCP/UDP | `dns` | / |
|
| TCP/UDP | `dns` | / | / |
|
||||||
| TCP/UDP | `bittorrent` | / |
|
| TCP/UDP | `bittorrent` | / | / |
|
||||||
| UDP | `dtls` | / |
|
| UDP | `dtls` | / | / |
|
||||||
|
|
||||||
|
| QUIC Client | Type |
|
||||||
|
|:------------------------:|:----------:|
|
||||||
|
| Chromium/Cronet | `chrimium` |
|
||||||
|
| Safari/Apple Network API | `safari` |
|
||||||
|
| Firefox / uquic firefox | `firefox` |
|
||||||
|
| quic-go / uquic chrome | `quic-go` |
|
|
@ -4,19 +4,28 @@ icon: material/new-box
|
||||||
|
|
||||||
!!! quote "sing-box 1.10.0 中的更改"
|
!!! quote "sing-box 1.10.0 中的更改"
|
||||||
|
|
||||||
:material-plus: BitTorrent 支持
|
:material-plus: QUIC 的 客户端类型探测支持
|
||||||
|
:material-plus: QUIC 的 Chromium 支持
|
||||||
|
:material-plus: BitTorrent 支持
|
||||||
:material-plus: DTLS 支持
|
:material-plus: DTLS 支持
|
||||||
|
|
||||||
如果在入站中启用,则可以嗅探连接的协议和域名(如果存在)。
|
如果在入站中启用,则可以嗅探连接的协议和域名(如果存在)。
|
||||||
|
|
||||||
#### 支持的协议
|
#### 支持的协议
|
||||||
|
|
||||||
| 网络 | 协议 | 域名 |
|
| 网络 | 协议 | 域名 | 客户端 |
|
||||||
|:-------:|:------------:|:-----------:|
|
|:-------:|:------------:|:-----------:|:----------:|
|
||||||
| TCP | `http` | Host |
|
| TCP | `http` | Host | / |
|
||||||
| TCP | `tls` | Server Name |
|
| TCP | `tls` | Server Name | / |
|
||||||
| UDP | `quic` | Server Name |
|
| UDP | `quic` | Server Name | QUIC 客户端类型 |
|
||||||
| UDP | `stun` | / |
|
| UDP | `stun` | / | / |
|
||||||
| TCP/UDP | `dns` | / |
|
| TCP/UDP | `dns` | / | / |
|
||||||
| TCP/UDP | `bittorrent` | / |
|
| TCP/UDP | `bittorrent` | / | / |
|
||||||
| UDP | `dtls` | / |
|
| UDP | `dtls` | / | / |
|
||||||
|
|
||||||
|
| QUIC 客户端 | 类型 |
|
||||||
|
|:------------------------:|:----------:|
|
||||||
|
| Chromium/Cronet | `chrimium` |
|
||||||
|
| Safari/Apple Network API | `safari` |
|
||||||
|
| Firefox / uquic firefox | `firefox` |
|
||||||
|
| quic-go / uquic chrome | `quic-go` |
|
2
go.mod
2
go.mod
|
@ -45,6 +45,7 @@ require (
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||||
golang.org/x/crypto v0.25.0
|
golang.org/x/crypto v0.25.0
|
||||||
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||||
golang.org/x/net v0.27.0
|
golang.org/x/net v0.27.0
|
||||||
golang.org/x/sys v0.25.0
|
golang.org/x/sys v0.25.0
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
|
@ -89,7 +90,6 @@ require (
|
||||||
github.com/vishvananda/netns v0.0.4 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
|
||||||
golang.org/x/mod v0.19.0 // indirect
|
golang.org/x/mod v0.19.0 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/text v0.18.0 // indirect
|
||||||
|
|
|
@ -70,6 +70,7 @@ type _DefaultRule struct {
|
||||||
Network Listable[string] `json:"network,omitempty"`
|
Network Listable[string] `json:"network,omitempty"`
|
||||||
AuthUser Listable[string] `json:"auth_user,omitempty"`
|
AuthUser Listable[string] `json:"auth_user,omitempty"`
|
||||||
Protocol Listable[string] `json:"protocol,omitempty"`
|
Protocol Listable[string] `json:"protocol,omitempty"`
|
||||||
|
Client Listable[string] `json:"client,omitempty"`
|
||||||
Domain Listable[string] `json:"domain,omitempty"`
|
Domain Listable[string] `json:"domain,omitempty"`
|
||||||
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
|
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
|
||||||
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
|
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
|
||||||
|
|
139
route/router.go
139
route/router.go
|
@ -854,8 +854,9 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
|
||||||
|
|
||||||
if metadata.InboundOptions.SniffEnabled && !sniff.Skip(metadata) {
|
if metadata.InboundOptions.SniffEnabled && !sniff.Skip(metadata) {
|
||||||
buffer := buf.NewPacket()
|
buffer := buf.NewPacket()
|
||||||
sniffMetadata, err := sniff.PeekStream(
|
err := sniff.PeekStream(
|
||||||
ctx,
|
ctx,
|
||||||
|
&metadata,
|
||||||
conn,
|
conn,
|
||||||
buffer,
|
buffer,
|
||||||
time.Duration(metadata.InboundOptions.SniffTimeout),
|
time.Duration(metadata.InboundOptions.SniffTimeout),
|
||||||
|
@ -864,9 +865,7 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
|
||||||
sniff.HTTPHost,
|
sniff.HTTPHost,
|
||||||
sniff.BitTorrent,
|
sniff.BitTorrent,
|
||||||
)
|
)
|
||||||
if sniffMetadata != nil {
|
if err == nil {
|
||||||
metadata.Protocol = sniffMetadata.Protocol
|
|
||||||
metadata.Domain = sniffMetadata.Domain
|
|
||||||
if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) {
|
if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) {
|
||||||
metadata.Destination = M.Socksaddr{
|
metadata.Destination = M.Socksaddr{
|
||||||
Fqdn: metadata.Domain,
|
Fqdn: metadata.Domain,
|
||||||
|
@ -878,8 +877,6 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
|
||||||
} else {
|
} else {
|
||||||
r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol)
|
r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol)
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
|
||||||
r.logger.TraceContext(ctx, "sniffed no protocol: ", err)
|
|
||||||
}
|
}
|
||||||
if !buffer.IsEmpty() {
|
if !buffer.IsEmpty() {
|
||||||
conn = bufio.NewCachedConn(conn, buffer)
|
conn = bufio.NewCachedConn(conn, buffer)
|
||||||
|
@ -980,65 +977,87 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
if metadata.InboundOptions.SniffEnabled || metadata.Destination.Addr.IsUnspecified() {
|
if metadata.InboundOptions.SniffEnabled || metadata.Destination.Addr.IsUnspecified() {
|
||||||
var (
|
var bufferList []*buf.Buffer
|
||||||
buffer = buf.NewPacket()
|
for {
|
||||||
destination M.Socksaddr
|
var (
|
||||||
done = make(chan struct{})
|
buffer = buf.NewPacket()
|
||||||
err error
|
destination M.Socksaddr
|
||||||
)
|
done = make(chan struct{})
|
||||||
go func() {
|
err error
|
||||||
sniffTimeout := C.ReadPayloadTimeout
|
)
|
||||||
if metadata.InboundOptions.SniffTimeout > 0 {
|
go func() {
|
||||||
sniffTimeout = time.Duration(metadata.InboundOptions.SniffTimeout)
|
sniffTimeout := C.ReadPayloadTimeout
|
||||||
|
if metadata.InboundOptions.SniffTimeout > 0 {
|
||||||
|
sniffTimeout = time.Duration(metadata.InboundOptions.SniffTimeout)
|
||||||
|
}
|
||||||
|
conn.SetReadDeadline(time.Now().Add(sniffTimeout))
|
||||||
|
destination, err = conn.ReadPacket(buffer)
|
||||||
|
conn.SetReadDeadline(time.Time{})
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-ctx.Done():
|
||||||
|
conn.Close()
|
||||||
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
conn.SetReadDeadline(time.Now().Add(sniffTimeout))
|
if err != nil {
|
||||||
destination, err = conn.ReadPacket(buffer)
|
buffer.Release()
|
||||||
conn.SetReadDeadline(time.Time{})
|
if !errors.Is(err, os.ErrDeadlineExceeded) {
|
||||||
close(done)
|
return err
|
||||||
}()
|
}
|
||||||
select {
|
} else {
|
||||||
case <-done:
|
if metadata.Destination.Addr.IsUnspecified() {
|
||||||
case <-ctx.Done():
|
metadata.Destination = destination
|
||||||
conn.Close()
|
}
|
||||||
return ctx.Err()
|
if metadata.InboundOptions.SniffEnabled {
|
||||||
}
|
if len(bufferList) > 0 {
|
||||||
if err != nil {
|
err = sniff.PeekPacket(
|
||||||
buffer.Release()
|
ctx,
|
||||||
if !errors.Is(err, os.ErrDeadlineExceeded) {
|
&metadata,
|
||||||
return err
|
buffer.Bytes(),
|
||||||
}
|
sniff.QUICClientHello,
|
||||||
} else {
|
)
|
||||||
if metadata.Destination.Addr.IsUnspecified() {
|
} else {
|
||||||
metadata.Destination = destination
|
err = sniff.PeekPacket(
|
||||||
}
|
ctx, &metadata,
|
||||||
if metadata.InboundOptions.SniffEnabled {
|
buffer.Bytes(),
|
||||||
sniffMetadata, _ := sniff.PeekPacket(
|
sniff.DomainNameQuery,
|
||||||
ctx,
|
sniff.QUICClientHello,
|
||||||
buffer.Bytes(),
|
sniff.STUNMessage,
|
||||||
sniff.DomainNameQuery,
|
sniff.UTP,
|
||||||
sniff.QUICClientHello,
|
sniff.UDPTracker,
|
||||||
sniff.STUNMessage,
|
sniff.DTLSRecord)
|
||||||
sniff.UTP,
|
}
|
||||||
sniff.UDPTracker,
|
if E.IsMulti(err, sniff.ErrClientHelloFragmented) && len(bufferList) == 0 {
|
||||||
sniff.DTLSRecord,
|
bufferList = append(bufferList, buffer)
|
||||||
)
|
r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello")
|
||||||
if sniffMetadata != nil {
|
continue
|
||||||
metadata.Protocol = sniffMetadata.Protocol
|
}
|
||||||
metadata.Domain = sniffMetadata.Domain
|
if metadata.Protocol != "" {
|
||||||
if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) {
|
if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) {
|
||||||
metadata.Destination = M.Socksaddr{
|
metadata.Destination = M.Socksaddr{
|
||||||
Fqdn: metadata.Domain,
|
Fqdn: metadata.Domain,
|
||||||
Port: metadata.Destination.Port,
|
Port: metadata.Destination.Port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if metadata.Domain != "" && metadata.Client != "" {
|
||||||
|
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client)
|
||||||
|
} else if metadata.Domain != "" {
|
||||||
|
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain)
|
||||||
|
} else if metadata.Client != "" {
|
||||||
|
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", client: ", metadata.Client)
|
||||||
|
} else {
|
||||||
|
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if metadata.Domain != "" {
|
|
||||||
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain)
|
|
||||||
} else {
|
|
||||||
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
conn = bufio.NewCachedPacketConn(conn, buffer, destination)
|
||||||
}
|
}
|
||||||
conn = bufio.NewCachedPacketConn(conn, buffer, destination)
|
for _, cachedBuffer := range common.Reverse(bufferList) {
|
||||||
|
conn = bufio.NewCachedPacketConn(conn, cachedBuffer, destination)
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if r.dnsReverseMapping != nil && metadata.Domain == "" {
|
if r.dnsReverseMapping != nil && metadata.Domain == "" {
|
||||||
|
|
|
@ -79,6 +79,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
|
||||||
rule.items = append(rule.items, item)
|
rule.items = append(rule.items, item)
|
||||||
rule.allItems = append(rule.allItems, item)
|
rule.allItems = append(rule.allItems, item)
|
||||||
}
|
}
|
||||||
|
if len(options.Client) > 0 {
|
||||||
|
item := NewClientItem(options.Client)
|
||||||
|
rule.items = append(rule.items, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 {
|
if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 {
|
||||||
item := NewDomainItem(options.Domain, options.DomainSuffix)
|
item := NewDomainItem(options.Domain, options.DomainSuffix)
|
||||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||||
|
|
37
route/rule_item_client.go
Normal file
37
route/rule_item_client.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ RuleItem = (*ClientItem)(nil)
|
||||||
|
|
||||||
|
type ClientItem struct {
|
||||||
|
clients []string
|
||||||
|
clientMap map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClientItem(clients []string) *ClientItem {
|
||||||
|
clientMap := make(map[string]bool)
|
||||||
|
for _, client := range clients {
|
||||||
|
clientMap[client] = true
|
||||||
|
}
|
||||||
|
return &ClientItem{
|
||||||
|
clients: clients,
|
||||||
|
clientMap: clientMap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClientItem) Match(metadata *adapter.InboundContext) bool {
|
||||||
|
return r.clientMap[metadata.Client]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClientItem) String() string {
|
||||||
|
if len(r.clients) == 1 {
|
||||||
|
return F.ToString("client=", r.clients[0])
|
||||||
|
}
|
||||||
|
return F.ToString("client=[", strings.Join(r.clients, " "), "]")
|
||||||
|
}
|
Loading…
Reference in a new issue