From dbc26775c90f9dc1b0b134c662a7ff60759cffed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 2 Nov 2021 19:36:55 +0800 Subject: [PATCH] Add quic sniffer --- app/dispatcher/default.go | 31 ++-- app/dispatcher/sniffer.go | 15 +- common/buf/buffer.go | 34 +++- common/protocol/quic/sniff.go | 210 ++++++++++++++++++++++++ common/protocol/quic/sniff_test.go | 18 ++ go.mod | 2 +- infra/conf/cfgcommon/sniffer/sniffer.go | 2 + 7 files changed, 285 insertions(+), 27 deletions(-) create mode 100644 common/protocol/quic/sniff.go create mode 100644 common/protocol/quic/sniff_test.go diff --git a/app/dispatcher/default.go b/app/dispatcher/default.go index 0e4807126..074114b66 100644 --- a/app/dispatcher/default.go +++ b/app/dispatcher/default.go @@ -174,6 +174,9 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran } func shouldOverride(result SniffResult, domainOverride []string) bool { + if result.Domain() == "" { + return false + } protocolString := result.Protocol() if resComp, ok := result.(SnifferResultComposite); ok { protocolString = resComp.ProtocolForDomainResult() @@ -207,32 +210,16 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin content = new(session.Content) 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 shouldOverride(result, sniffingRequest.OverrideDestinationForProtocol) { - domain := result.Domain() - newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx)) - destination.Address = net.ParseAddress(domain) - 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() } @@ -245,10 +232,11 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin d.routedDispatch(ctx, outbound, destination) }() } + return inbound, 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() @@ -274,7 +262,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 } @@ -293,6 +281,7 @@ func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool) (Sni } return contentResult, contentErr } + func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination) { var handler outbound.Handler diff --git a/app/dispatcher/sniffer.go b/app/dispatcher/sniffer.go index 101e1e46c..8fe61049a 100644 --- a/app/dispatcher/sniffer.go +++ b/app/dispatcher/sniffer.go @@ -4,8 +4,10 @@ import ( "context" "github.com/v2fly/v2ray-core/v4/common" + "github.com/v2fly/v2ray-core/v4/common/net" "github.com/v2fly/v2ray-core/v4/common/protocol/bittorrent" "github.com/v2fly/v2ray-core/v4/common/protocol/http" + "github.com/v2fly/v2ray-core/v4/common/protocol/quic" "github.com/v2fly/v2ray-core/v4/common/protocol/tls" ) @@ -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 { @@ -31,9 +34,10 @@ 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 quic.SniffQUIC(b) }, false, net.Network_UDP}, + {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false, net.Network_UDP}, }, } if sniffer, err := newFakeDNSSniffer(ctx); err == nil { @@ -49,13 +53,16 @@ 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 { continue } + if si.network != network { + continue + } result, err := s(c, payload) if err == common.ErrNoClue { pendingSniffer = append(pendingSniffer, si) diff --git a/common/buf/buffer.go b/common/buf/buffer.go index fa1de3cfe..38cec4655 100644 --- a/common/buf/buffer.go +++ b/common/buf/buffer.go @@ -20,6 +20,7 @@ type Buffer struct { v []byte start int32 end int32 + out bool } // New creates a Buffer with 0 length and 2K capacity. @@ -29,6 +30,15 @@ func New() *Buffer { } } +// As creates a Buffer with an existed bytearray +func As(data []byte) *Buffer { + return &Buffer{ + v: data, + end: int32(len(data)), + out: true, + } +} + // StackNew creates a new Buffer object on stack. // This method is for buffers that is released in the same function. func StackNew() Buffer { @@ -39,7 +49,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.out { return } @@ -173,6 +183,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 { diff --git a/common/protocol/quic/sniff.go b/common/protocol/quic/sniff.go new file mode 100644 index 000000000..58f306dc2 --- /dev/null +++ b/common/protocol/quic/sniff.go @@ -0,0 +1,210 @@ +package quic + +import ( + "crypto" + "crypto/aes" + "crypto/tls" + "encoding/binary" + "io" + + "github.com/v2fly/v2ray-core/v4/common/errors" + + "github.com/lucas-clemente/quic-go/quicvarint" + "github.com/marten-seemann/qtls-go1-17" + "golang.org/x/crypto/hkdf" + + "github.com/v2fly/v2ray-core/v4/common" + "github.com/v2fly/v2ray-core/v4/common/buf" + ptls "github.com/v2fly/v2ray-core/v4/common/protocol/tls" +) + +type SniffHeader struct { + domain string +} + +func (s SniffHeader) Protocol() string { + return "quic" +} + +func (s SniffHeader) Domain() string { + return s.domain +} + +const ( + versionDraft29 uint32 = 0xff00001d + version1 uint32 = 0x1 +) + +var ( + quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99} + quicSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} + initialSuite = &qtls.CipherSuiteTLS13{ + ID: tls.TLS_AES_128_GCM_SHA256, + KeyLen: 16, + AEAD: qtls.AEADAESGCMTLS13, + Hash: crypto.SHA256, + } + errNotQuic = errors.New("not quic") + errNotQuicInitial = errors.New("not initial packet") +) + +func SniffQUIC(b []byte) (*SniffHeader, error) { + buffer := buf.As(b) + typeByte, err := buffer.ReadByte() + if err != nil { + return nil, errNotQuic + } + isLongHeader := typeByte&0x80 > 0 + if !isLongHeader || typeByte&0x40 == 0 { + return nil, errNotQuicInitial + } + + vb, err := buffer.ReadBytes(4) + if err != nil { + return nil, errNotQuic + } + + versionNumber := binary.BigEndian.Uint32(vb) + + if versionNumber != 0 && typeByte&0x40 == 0 { + return nil, errNotQuic + } else if versionNumber != versionDraft29 && versionNumber != version1 { + return nil, errNotQuic + } + + if (typeByte&0x30)>>4 != 0x0 { + return nil, errNotQuicInitial + } + + var destConnID []byte + if l, err := buffer.ReadByte(); err != nil { + return nil, errNotQuic + } else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil { + return nil, errNotQuic + } + + if l, err := buffer.ReadByte(); err != nil { + return nil, errNotQuic + } else if common.Error2(buffer.ReadBytes(int32(l))) != nil { + return nil, errNotQuic + } + + tokenLen, err := quicvarint.Read(buffer) + if err != nil || tokenLen > uint64(len(b)) { + return nil, errNotQuic + } + + if _, err = buffer.ReadBytes(int32(tokenLen)); err != nil { + return nil, errNotQuic + } + + packetLen, err := quicvarint.Read(buffer) + if err != nil { + return nil, errNotQuic + } + + hdrLen := len(b) - int(buffer.Len()) + + origPNBytes := make([]byte, 4) + copy(origPNBytes, b[hdrLen:hdrLen+4]) + + var salt []byte + if versionNumber == version1 { + salt = quicSalt + } else { + salt = quicSaltOld + } + initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt) + secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size()) + hpKey := hkdfExpandLabel(initialSuite.Hash, secret, []byte{}, "quic hp", initialSuite.KeyLen) + block, err := aes.NewCipher(hpKey) + if err != nil { + return nil, err + } + + cache := buf.New() + defer cache.Release() + + mask := cache.Extend(int32(block.BlockSize())) + block.Encrypt(mask, b[hdrLen+4:hdrLen+4+16]) + b[0] ^= mask[0] & 0xf + for i := range b[hdrLen : hdrLen+4] { + b[hdrLen+i] ^= mask[i+1] + } + packetNumberLength := b[0]&0x3 + 1 + if packetNumberLength != 1 { + return nil, errNotQuicInitial + } + var packetNumber uint32 + { + n, err := buffer.ReadByte() + if err != nil { + return nil, err + } + packetNumber = uint32(n) + } + + if packetNumber != 0 { + return nil, errNotQuicInitial + } + + extHdrLen := hdrLen + int(packetNumberLength) + copy(b[extHdrLen:hdrLen+4], origPNBytes[packetNumberLength:]) + data := b[extHdrLen : int(packetLen)+hdrLen] + + key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16) + iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12) + cipher := qtls.AEADAESGCMTLS13(key, iv) + nonce := cache.Extend(int32(cipher.NonceSize())) + binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber)) + decrypted, err := cipher.Open(b[extHdrLen:extHdrLen], nonce, data, b[:extHdrLen]) + if err != nil { + return nil, err + } + buffer = buf.As(decrypted) + frameType, err := buffer.ReadByte() + if err != nil { + return nil, io.ErrUnexpectedEOF + } + if frameType != 0x6 { + // not crypto frame + return &SniffHeader{domain: ""}, nil + } + if common.Error2(quicvarint.Read(buffer)) != nil { + return nil, io.ErrUnexpectedEOF + } + dataLen, err := quicvarint.Read(buffer) + if err != nil { + return nil, io.ErrUnexpectedEOF + } + if dataLen > uint64(buffer.Len()) { + return nil, io.ErrUnexpectedEOF + } + frameData, err := buffer.ReadBytes(int32(dataLen)) + common.Must(err) + tlsHdr := &ptls.SniffHeader{} + err = ptls.ReadClientHello(frameData, tlsHdr) + if err != nil { + return nil, err + } + + return &SniffHeader{domain: tlsHdr.Domain()}, nil +} + +func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte { + b := make([]byte, 3, 3+6+len(label)+1+len(context)) + binary.BigEndian.PutUint16(b, uint16(length)) + b[2] = uint8(6 + len(label)) + b = append(b, []byte("tls13 ")...) + b = append(b, []byte(label)...) + b = b[:3+6+len(label)+1] + b[3+6+len(label)] = uint8(len(context)) + b = append(b, context...) + + out := make([]byte, length) + n, err := hkdf.Expand(hash.New, secret, b).Read(out) + if err != nil || n != length { + panic("quic: HKDF-Expand-Label invocation failed unexpectedly") + } + return out +} diff --git a/common/protocol/quic/sniff_test.go b/common/protocol/quic/sniff_test.go new file mode 100644 index 000000000..f33cc4394 --- /dev/null +++ b/common/protocol/quic/sniff_test.go @@ -0,0 +1,18 @@ +package quic_test + +import ( + "encoding/hex" + "testing" + + "github.com/v2fly/v2ray-core/v4/common" + "github.com/v2fly/v2ray-core/v4/common/protocol/quic" +) + +func TestSniffQUIC(t *testing.T) { + pkt, err := hex.DecodeString("cd0000000108f1fb7bcc78aa5e7203a8f86400421531fe825b19541876db6c55c38890cd73149d267a084afee6087304095417a3033df6a81bbb71d8512e7a3e16df1e277cae5df3182cb214b8fe982ba3fdffbaa9ffec474547d55945f0fddbeadfb0b5243890b2fa3da45169e2bd34ec04b2e29382f48d612b28432a559757504d158e9e505407a77dd34f4b60b8d3b555ee85aacd6648686802f4de25e7216b19e54c5f78e8a5963380c742d861306db4c16e4f7fc94957aa50b9578a0b61f1e406b2ad5f0cd3cd271c4d99476409797b0c3cb3efec256118912d4b7e4fd79d9cb9016b6e5eaa4f5e57b637b217755daf8968a4092bed0ed5413f5d04904b3a61e4064f9211b2629e5b52a89c7b19f37a713e41e27743ea6dfa736dfa1bb0a4b2bc8c8dc632c6ce963493a20c550e6fdb2475213665e9a85cfc394da9cec0cf41f0c8abed3fc83be5245b2b5aa5e825d29349f721d30774ef5bf965b540f3d8d98febe20956b1fc8fa047e10e7d2f921c9c6622389e02322e80621a1cf5264e245b7276966eb02932584e3f7038bd36aa908766ad3fb98344025dec18670d6db43a1c5daac00937fce7b7c7d61ff4e6efd01a2bdee0ee183108b926393df4f3d74bbcbb015f240e7e346b7d01c41111a401225ce3b095ab4623a5836169bf9599eeca79d1d2e9b2202b5960a09211e978058d6fc0484eff3e91ce4649a5e3ba15b906d334cf66e28d9ff575406e1ae1ac2febafd72870b6f5d58fc5fb949cb1f40feb7c1d9ce5e71b") + common.Must(err) + quicHdr, err := quic.SniffQUIC(pkt) + if err != nil || quicHdr.Domain() != "www.google.com" { + t.Error("failed") + } +} diff --git a/go.mod b/go.mod index d6376f62d..d31b97bab 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/gorilla/websocket v1.4.2 github.com/jhump/protoreflect v1.10.1 github.com/lucas-clemente/quic-go v0.24.0 + github.com/marten-seemann/qtls-go1-17 v0.1.0 github.com/miekg/dns v1.1.43 github.com/pelletier/go-toml v1.9.4 github.com/pires/go-proxyproto v0.6.1 @@ -44,7 +45,6 @@ require ( github.com/leodido/go-urn v1.2.1 // indirect github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect github.com/marten-seemann/qtls-go1-16 v0.1.4 // indirect - github.com/marten-seemann/qtls-go1-17 v0.1.0 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/onsi/ginkgo v1.16.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/infra/conf/cfgcommon/sniffer/sniffer.go b/infra/conf/cfgcommon/sniffer/sniffer.go index 84654cc54..7f4557da0 100644 --- a/infra/conf/cfgcommon/sniffer/sniffer.go +++ b/infra/conf/cfgcommon/sniffer/sniffer.go @@ -25,6 +25,8 @@ func (c *SniffingConfig) Build() (*proxyman.SniffingConfig, error) { p = append(p, "http") case "tls", "https", "ssl": p = append(p, "tls") + case "quic": + p = append(p, "quic") case "fakedns": p = append(p, "fakedns") case "fakedns+others":