Skip to content

Commit

Permalink
Quic sniffer (#1074)
Browse files Browse the repository at this point in the history
* Add quic sniffer

* Fix quic sniffer

* Add uTP sniffer

* rename buf pool membership status to unmanaged

* rename buf type adaptor into FromBytes

Co-authored-by: 世界 <i@sekai.icu>
Co-authored-by: Shelikhoo <xiaokangwang@outlook.com>
  • Loading branch information
3 people authored May 23, 2022
1 parent f046feb commit 3f64f32
Show file tree
Hide file tree
Showing 10 changed files with 404 additions and 54 deletions.
56 changes: 12 additions & 44 deletions app/dispatcher/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ func (d *DefaultDispatcher) getLink(ctx context.Context, network net.Network, sn

func (d *DefaultDispatcher) shouldOverride(ctx context.Context, result SniffResult, request session.SniffingRequest, destination net.Destination) bool {
domain := result.Domain()
if domain == "" {
return false
}
for _, d := range request.ExcludeForDomain {
if strings.ToLower(domain) == d {
return false
Expand Down Expand Up @@ -295,33 +298,15 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin

sniffingRequest := content.SniffingRequest
inbound, outbound := d.getLink(ctx, destination.Network, sniffingRequest)
switch {
case !sniffingRequest.Enabled:
go d.routedDispatch(ctx, outbound, destination)
case destination.Network != net.Network_TCP:
// Only metadata sniff will be used for non tcp connection
result, err := sniffer(ctx, nil, true)
if err == nil {
content.Protocol = result.Protocol()
if d.shouldOverride(ctx, result, sniffingRequest, destination) {
domain := result.Domain()
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
destination.Address = net.ParseAddress(domain)
if sniffingRequest.RouteOnly && result.Protocol() != "fakedns" {
ob.RouteTarget = destination
} else {
ob.Target = destination
}
}
}
if !sniffingRequest.Enabled {
go d.routedDispatch(ctx, outbound, destination)
default:
} else {
go func() {
cReader := &cachedReader{
reader: outbound.Reader.(*pipe.Reader),
}
outbound.Reader = cReader
result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly)
result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly, destination.Network)
if err == nil {
content.Protocol = result.Protocol()
}
Expand Down Expand Up @@ -356,33 +341,15 @@ func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.De
ctx = session.ContextWithContent(ctx, content)
}
sniffingRequest := content.SniffingRequest
switch {
case !sniffingRequest.Enabled:
if !sniffingRequest.Enabled {
go d.routedDispatch(ctx, outbound, destination)
case destination.Network != net.Network_TCP:
// Only metadata sniff will be used for non tcp connection
result, err := sniffer(ctx, nil, true)
if err == nil {
content.Protocol = result.Protocol()
if d.shouldOverride(ctx, result, sniffingRequest, destination) {
domain := result.Domain()
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
destination.Address = net.ParseAddress(domain)
if sniffingRequest.RouteOnly && result.Protocol() != "fakedns" {
ob.RouteTarget = destination
} else {
ob.Target = destination
}
}
}
go d.routedDispatch(ctx, outbound, destination)
default:
} else {
go func() {
cReader := &cachedReader{
reader: outbound.Reader.(*pipe.Reader),
}
outbound.Reader = cReader
result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly)
result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly, destination.Network)
if err == nil {
content.Protocol = result.Protocol()
}
Expand All @@ -399,10 +366,11 @@ func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.De
d.routedDispatch(ctx, outbound, destination)
}()
}

return nil
}

func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool) (SniffResult, error) {
func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, network net.Network) (SniffResult, error) {
payload := buf.New()
defer payload.Release()

Expand All @@ -428,7 +396,7 @@ func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool) (Sni

cReader.Cache(payload)
if !payload.IsEmpty() {
result, err := sniffer.Sniff(ctx, payload.Bytes())
result, err := sniffer.Sniff(ctx, payload.Bytes(), network)
if err != common.ErrNoClue {
return result, err
}
Expand Down
15 changes: 10 additions & 5 deletions app/dispatcher/sniffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package dispatcher

import (
"context"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol/quic"

"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/protocol/bittorrent"
Expand All @@ -22,6 +24,7 @@ type protocolSnifferWithMetadata struct {
// for both TCP and UDP connections
// It will not be shown as a traffic type for routing unless there is no other successful sniffing.
metadataSniffer bool
network net.Network
}

type Sniffer struct {
Expand All @@ -31,9 +34,11 @@ type Sniffer struct {
func NewSniffer(ctx context.Context) *Sniffer {
ret := &Sniffer{
sniffer: []protocolSnifferWithMetadata{
{func(c context.Context, b []byte) (SniffResult, error) { return http.SniffHTTP(b) }, false},
{func(c context.Context, b []byte) (SniffResult, error) { return tls.SniffTLS(b) }, false},
{func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false},
{func(c context.Context, b []byte) (SniffResult, error) { return http.SniffHTTP(b) }, false, net.Network_TCP},
{func(c context.Context, b []byte) (SniffResult, error) { return tls.SniffTLS(b) }, false, net.Network_TCP},
{func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false, net.Network_TCP},
{func(c context.Context, b []byte) (SniffResult, error) { return quic.SniffQUIC(b) }, false, net.Network_UDP},
{func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffUTP(b) }, false, net.Network_UDP},
},
}
if sniffer, err := newFakeDNSSniffer(ctx); err == nil {
Expand All @@ -49,11 +54,11 @@ func NewSniffer(ctx context.Context) *Sniffer {

var errUnknownContent = newError("unknown content")

func (s *Sniffer) Sniff(c context.Context, payload []byte) (SniffResult, error) {
func (s *Sniffer) Sniff(c context.Context, payload []byte, network net.Network) (SniffResult, error) {
var pendingSniffer []protocolSnifferWithMetadata
for _, si := range s.sniffer {
s := si.protocolSniffer
if si.metadataSniffer {
if si.metadataSniffer || si.network != network {
continue
}
result, err := s(c, payload)
Expand Down
43 changes: 38 additions & 5 deletions common/buf/buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ var pool = bytespool.GetPool(Size)
// the buffer into an internal buffer pool, in order to recreate a buffer more
// quickly.
type Buffer struct {
v []byte
start int32
end int32
UDP *net.Destination
v []byte
start int32
end int32
unmanaged bool
UDP *net.Destination
}

// New creates a Buffer with 0 length and 8K capacity.
Expand All @@ -38,6 +39,7 @@ func New() *Buffer {
}
}

// NewExisted creates a managed, standard size Buffer with an existed bytearray
func NewExisted(b []byte) *Buffer {
if cap(b) < Size {
panic("Invalid buffer")
Expand All @@ -54,6 +56,15 @@ func NewExisted(b []byte) *Buffer {
}
}

// FromBytes creates a Buffer with an existed bytearray
func FromBytes(b []byte) *Buffer {
return &Buffer{
v: b,
end: int32(len(b)),
unmanaged: true,
}
}

// StackNew creates a new Buffer object on stack.
// This method is for buffers that is released in the same function.
func StackNew() Buffer {
Expand All @@ -71,7 +82,7 @@ func StackNew() Buffer {

// Release recycles the buffer into an internal buffer pool.
func (b *Buffer) Release() {
if b == nil || b.v == nil {
if b == nil || b.v == nil || b.unmanaged {
return
}

Expand Down Expand Up @@ -212,6 +223,28 @@ func (b *Buffer) WriteString(s string) (int, error) {
return b.Write([]byte(s))
}

// ReadByte implements io.ByteReader
func (b *Buffer) ReadByte() (byte, error) {
if b.start == b.end {
return 0, io.EOF
}

nb := b.v[b.start]
b.start++
return nb, nil
}

// ReadBytes implements bufio.Reader.ReadBytes
func (b *Buffer) ReadBytes(length int32) ([]byte, error) {
if b.end-b.start < length {
return nil, io.EOF
}

nb := b.v[b.start : b.start+length]
b.start += length
return nb, nil
}

// Read implements io.Reader.Read().
func (b *Buffer) Read(data []byte) (int, error) {
if b.Len() == 0 {
Expand Down
59 changes: 59 additions & 0 deletions common/protocol/bittorrent/bittorrent.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package bittorrent

import (
"encoding/binary"
"errors"
"math"
"time"

"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common"
)

Expand All @@ -29,3 +33,58 @@ func SniffBittorrent(b []byte) (*SniffHeader, error) {

return nil, errNotBittorrent
}

func SniffUTP(b []byte) (*SniffHeader, error) {
if len(b) < 20 {
return nil, common.ErrNoClue
}

buffer := buf.FromBytes(b)

var typeAndVersion uint8

if binary.Read(buffer, binary.BigEndian, &typeAndVersion) != nil {
return nil, common.ErrNoClue
} else if b[0]>>4&0xF > 4 || b[0]&0xF != 1 {
return nil, errNotBittorrent
}

var extension uint8

if binary.Read(buffer, binary.BigEndian, &extension) != nil {
return nil, common.ErrNoClue
} else if extension != 0 && extension != 1 {
return nil, errNotBittorrent
}

for extension != 0 {
if extension != 1 {
return nil, errNotBittorrent
}
if binary.Read(buffer, binary.BigEndian, &extension) != nil {
return nil, common.ErrNoClue
}

var length uint8
if err := binary.Read(buffer, binary.BigEndian, &length); err != nil {
return nil, common.ErrNoClue
}
if common.Error2(buffer.ReadBytes(int32(length))) != nil {
return nil, common.ErrNoClue
}
}

if common.Error2(buffer.ReadBytes(2)) != nil {
return nil, common.ErrNoClue
}

var timestamp uint32
if err := binary.Read(buffer, binary.BigEndian, &timestamp); err != nil {
return nil, common.ErrNoClue
}
if math.Abs(float64(time.Now().UnixMicro()-int64(timestamp))) > float64(24*time.Hour) {
return nil, errNotBittorrent
}

return &SniffHeader{}, nil
}
19 changes: 19 additions & 0 deletions common/protocol/quic/qtls_go116.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//go:build go1.16 && !go1.17
// +build go1.16,!go1.17

package quic

import (
"crypto/cipher"

"github.com/marten-seemann/qtls-go1-16"
)

type (
// A CipherSuiteTLS13 is a cipher suite for TLS 1.3
CipherSuiteTLS13 = qtls.CipherSuiteTLS13
)

func AEADAESGCMTLS13(key, fixedNonce []byte) cipher.AEAD {
return qtls.AEADAESGCMTLS13(key, fixedNonce)
}
19 changes: 19 additions & 0 deletions common/protocol/quic/qtls_go117.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//go:build go1.17 && !go1.18
// +build go1.17,!go1.18

package quic

import (
"crypto/cipher"

"github.com/marten-seemann/qtls-go1-17"
)

type (
// A CipherSuiteTLS13 is a cipher suite for TLS 1.3
CipherSuiteTLS13 = qtls.CipherSuiteTLS13
)

func AEADAESGCMTLS13(key, fixedNonce []byte) cipher.AEAD {
return qtls.AEADAESGCMTLS13(key, fixedNonce)
}
19 changes: 19 additions & 0 deletions common/protocol/quic/qtls_go118.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//go:build go1.18
// +build go1.18

package quic

import (
"crypto/cipher"

"github.com/marten-seemann/qtls-go1-18"
)

type (
// A CipherSuiteTLS13 is a cipher suite for TLS 1.3
CipherSuiteTLS13 = qtls.CipherSuiteTLS13
)

func AEADAESGCMTLS13(key, fixedNonce []byte) cipher.AEAD {
return qtls.AEADAESGCMTLS13(key, fixedNonce)
}
Loading

0 comments on commit 3f64f32

Please sign in to comment.