diff --git a/adapters/outbound/vmess.go b/adapters/outbound/vmess.go index 7999093a..db1e2039 100644 --- a/adapters/outbound/vmess.go +++ b/adapters/outbound/vmess.go @@ -32,6 +32,7 @@ type VmessOption struct { UDP bool `proxy:"udp,omitempty"` Network string `proxy:"network,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` + HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` WSPath string `proxy:"ws-path,omitempty"` WSHeaders map[string]string `proxy:"ws-headers,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` @@ -44,6 +45,11 @@ type HTTPOptions struct { Headers map[string][]string `proxy:"headers,omitempty"` } +type HTTP2Options struct { + Host []string `proxy:"host,omitempty"` + Path string `proxy:"path,omitempty"` +} + func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { var err error switch v.option.Network { @@ -99,6 +105,30 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { } c = vmess.StreamHTTPConn(c, httpOpts) + case "h2": + host, _, _ := net.SplitHostPort(v.addr) + tlsOpts := vmess.TLSConfig{ + Host: host, + SkipCertVerify: v.option.SkipCertVerify, + SessionCache: getClientSessionCache(), + NextProtos: []string{"h2"}, + } + + if v.option.ServerName != "" { + tlsOpts.Host = v.option.ServerName + } + + c, err = vmess.StreamTLSConn(c, &tlsOpts) + if err != nil { + return nil, err + } + + h2Opts := &vmess.H2Config{ + Hosts: v.option.HTTP2Opts.Host, + Path: v.option.HTTP2Opts.Path, + } + + c, err = vmess.StreamH2Conn(c, h2Opts) default: // handle TLS if v.option.TLS { @@ -171,6 +201,9 @@ func NewVmess(option VmessOption) (*Vmess, error) { if err != nil { return nil, err } + if option.Network == "h2" && !option.TLS { + return nil, fmt.Errorf("TLS must be true with h2 network") + } return &Vmess{ Base: &Base{ diff --git a/component/vmess/h2.go b/component/vmess/h2.go new file mode 100644 index 00000000..b814ae9b --- /dev/null +++ b/component/vmess/h2.go @@ -0,0 +1,111 @@ +package vmess + +import ( + "io" + "math/rand" + "net" + "net/http" + "net/url" + + "golang.org/x/net/http2" +) + +type h2Conn struct { + net.Conn + *http2.ClientConn + pwriter *io.PipeWriter + res *http.Response + cfg *H2Config +} + +type H2Config struct { + Hosts []string + Path string +} + +func (hc *h2Conn) establishConn() error { + preader, pwriter := io.Pipe() + + host := hc.cfg.Hosts[rand.Intn(len(hc.cfg.Hosts))] + path := hc.cfg.Path + // TODO: connect use VMess Host instead of H2 Host + req := http.Request{ + Method: "PUT", + Host: host, + URL: &url.URL{ + Scheme: "https", + Host: host, + Path: path, + }, + Proto: "HTTP/2", + ProtoMajor: 2, + ProtoMinor: 0, + Body: preader, + Header: map[string][]string{ + "Accept-Encoding": {"identity"}, + }, + } + + res, err := hc.ClientConn.RoundTrip(&req) + if err != nil { + return err + } + + hc.pwriter = pwriter + hc.res = res + + return nil +} + +// Read implements net.Conn.Read() +func (hc *h2Conn) Read(b []byte) (int, error) { + if hc.res != nil && !hc.res.Close { + n, err := hc.res.Body.Read(b) + return n, err + } + + if err := hc.establishConn(); err != nil { + return 0, err + } + return hc.res.Body.Read(b) +} + +// Write implements io.Writer. +func (hc *h2Conn) Write(b []byte) (int, error) { + if hc.pwriter != nil { + return hc.pwriter.Write(b) + } + + if err := hc.establishConn(); err != nil { + return 0, err + } + return hc.pwriter.Write(b) +} + +func (hc *h2Conn) Close() error { + if err := hc.pwriter.Close(); err != nil { + return err + } + if err := hc.ClientConn.Shutdown(hc.res.Request.Context()); err != nil { + return err + } + if err := hc.Conn.Close(); err != nil { + return err + } + return nil +} + +func StreamH2Conn(conn net.Conn, cfg *H2Config) (net.Conn, error) { + transport := &http2.Transport{} + + cconn, err := transport.NewClientConn(conn) + if err != nil { + return nil, err + } + + return &h2Conn{ + Conn: conn, + ClientConn: cconn, + cfg: cfg, + }, nil +} diff --git a/component/vmess/tls.go b/component/vmess/tls.go index 8ed19777..b003a753 100644 --- a/component/vmess/tls.go +++ b/component/vmess/tls.go @@ -9,6 +9,7 @@ type TLSConfig struct { Host string SkipCertVerify bool SessionCache tls.ClientSessionCache + NextProtos []string } func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) { @@ -16,6 +17,7 @@ func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) { ServerName: cfg.Host, InsecureSkipVerify: cfg.SkipCertVerify, ClientSessionCache: cfg.SessionCache, + NextProtos: cfg.NextProtos, } tlsConn := tls.Client(conn, tlsConfig) diff --git a/go.sum b/go.sum index 8611148b..9bded5ce 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,7 @@ golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=