Feature: add gRPC Transport for vmess/trojan (#1287)
Co-authored-by: eMeab <32988354+eMeab@users.noreply.github.com> Co-authored-by: Dreamacro <8615343+Dreamacro@users.noreply.github.com>
This commit is contained in:
226
component/gun/gun.go
Normal file
226
component/gun/gun.go
Normal file
@ -0,0 +1,226 @@
|
||||
// Modified from: https://github.com/Qv2ray/gun-lite
|
||||
// License: MIT
|
||||
|
||||
package gun
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/pool"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidLength = errors.New("invalid length")
|
||||
)
|
||||
|
||||
var (
|
||||
defaultHeader = http.Header{
|
||||
"content-type": []string{"application/grpc"},
|
||||
"user-agent": []string{"grpc-go/1.36.0"},
|
||||
}
|
||||
)
|
||||
|
||||
type DialFn = func(network, addr string) (net.Conn, error)
|
||||
|
||||
type Conn struct {
|
||||
response *http.Response
|
||||
request *http.Request
|
||||
client *http.Client
|
||||
writer *io.PipeWriter
|
||||
once sync.Once
|
||||
close *atomic.Bool
|
||||
err error
|
||||
|
||||
buf []byte
|
||||
offset int
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ServiceName string
|
||||
Host string
|
||||
}
|
||||
|
||||
func (g *Conn) initRequest() {
|
||||
response, err := g.client.Do(g.request)
|
||||
if err != nil {
|
||||
g.err = err
|
||||
g.writer.Close()
|
||||
return
|
||||
}
|
||||
|
||||
if !g.close.Load() {
|
||||
g.response = response
|
||||
} else {
|
||||
response.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Conn) Read(b []byte) (n int, err error) {
|
||||
g.once.Do(g.initRequest)
|
||||
if g.err != nil {
|
||||
return 0, g.err
|
||||
}
|
||||
|
||||
if g.buf != nil {
|
||||
n = copy(b, g.buf[g.offset:])
|
||||
g.offset += n
|
||||
if g.offset == len(g.buf) {
|
||||
g.offset = 0
|
||||
g.buf = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, 5)
|
||||
_, err = io.ReadFull(g.response.Body, buf)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
grpcPayloadLen := binary.BigEndian.Uint32(buf[1:])
|
||||
if grpcPayloadLen > pool.RelayBufferSize {
|
||||
return 0, ErrInvalidLength
|
||||
}
|
||||
|
||||
buf = pool.Get(int(grpcPayloadLen))
|
||||
_, err = io.ReadFull(g.response.Body, buf)
|
||||
if err != nil {
|
||||
pool.Put(buf)
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
protobufPayloadLen, protobufLengthLen := decodeUleb128(buf[1:])
|
||||
if protobufLengthLen == 0 {
|
||||
pool.Put(buf)
|
||||
return 0, ErrInvalidLength
|
||||
}
|
||||
if grpcPayloadLen != uint32(protobufPayloadLen)+uint32(protobufLengthLen)+1 {
|
||||
pool.Put(buf)
|
||||
return 0, ErrInvalidLength
|
||||
}
|
||||
|
||||
if len(b) >= int(grpcPayloadLen)-1-int(protobufLengthLen) {
|
||||
n = copy(b, buf[1+protobufLengthLen:])
|
||||
pool.Put(buf)
|
||||
return
|
||||
}
|
||||
n = copy(b, buf[1+protobufLengthLen:])
|
||||
g.offset = n + 1 + int(protobufLengthLen)
|
||||
g.buf = buf
|
||||
return
|
||||
}
|
||||
|
||||
func (g *Conn) Write(b []byte) (n int, err error) {
|
||||
protobufHeader := appendUleb128([]byte{0x0A}, uint64(len(b)))
|
||||
grpcHeader := make([]byte, 5)
|
||||
grpcPayloadLen := uint32(len(protobufHeader) + len(b))
|
||||
binary.BigEndian.PutUint32(grpcHeader[1:5], grpcPayloadLen)
|
||||
|
||||
buffers := net.Buffers{grpcHeader, protobufHeader, b}
|
||||
_, err = buffers.WriteTo(g.writer)
|
||||
if err == io.ErrClosedPipe && g.err != nil {
|
||||
err = g.err
|
||||
}
|
||||
|
||||
return len(b), err
|
||||
}
|
||||
|
||||
func (g *Conn) Close() error {
|
||||
g.close.Store(true)
|
||||
if r := g.response; r != nil {
|
||||
r.Body.Close()
|
||||
}
|
||||
|
||||
return g.writer.Close()
|
||||
}
|
||||
|
||||
func (g *Conn) LocalAddr() net.Addr { return &net.TCPAddr{IP: net.IPv4zero, Port: 0} }
|
||||
func (g *Conn) RemoteAddr() net.Addr { return &net.TCPAddr{IP: net.IPv4zero, Port: 0} }
|
||||
func (g *Conn) SetDeadline(t time.Time) error { return nil }
|
||||
func (g *Conn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (g *Conn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config) *http2.Transport {
|
||||
dialFunc := func(network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||
pconn, err := dialFn(network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cn := tls.Client(pconn, cfg)
|
||||
if err := cn.Handshake(); err != nil {
|
||||
pconn.Close()
|
||||
return nil, err
|
||||
}
|
||||
state := cn.ConnectionState()
|
||||
if p := state.NegotiatedProtocol; p != http2.NextProtoTLS {
|
||||
cn.Close()
|
||||
return nil, errors.New("http2: unexpected ALPN protocol " + p + "; want q" + http2.NextProtoTLS)
|
||||
}
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
return &http2.Transport{
|
||||
DialTLS: dialFunc,
|
||||
TLSClientConfig: tlsConfig,
|
||||
AllowHTTP: false,
|
||||
DisableCompression: true,
|
||||
ReadIdleTimeout: 0,
|
||||
PingTimeout: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func StreamGunWithTransport(transport *http2.Transport, cfg *Config) (net.Conn, error) {
|
||||
serviceName := "GunService"
|
||||
if cfg.ServiceName != "" {
|
||||
serviceName = cfg.ServiceName
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
request := &http.Request{
|
||||
Method: http.MethodPost,
|
||||
Body: reader,
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: cfg.Host,
|
||||
Path: fmt.Sprintf("/%s/Tun", serviceName),
|
||||
},
|
||||
Proto: "HTTP/2",
|
||||
ProtoMajor: 2,
|
||||
ProtoMinor: 0,
|
||||
Header: defaultHeader,
|
||||
}
|
||||
|
||||
conn := &Conn{
|
||||
request: request,
|
||||
client: client,
|
||||
writer: writer,
|
||||
close: atomic.NewBool(false),
|
||||
}
|
||||
|
||||
go conn.once.Do(conn.initRequest)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config) (net.Conn, error) {
|
||||
dialFn := func(network, addr string) (net.Conn, error) {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
transport := NewHTTP2Client(dialFn, tlsConfig)
|
||||
return StreamGunWithTransport(transport, cfg)
|
||||
}
|
Reference in New Issue
Block a user