From afac5fe52d9c74eb354754ab0bc8029d1fd9dd13 Mon Sep 17 00:00:00 2001 From: "rui.zheng" Date: Wed, 2 Aug 2017 18:17:16 +0800 Subject: [PATCH] add HTTP2 tunnel support --- gost/examples/bench/cli.go | 15 +- gost/examples/bench/srv.go | 21 +- gost/examples/http2/{srv.go => http2.go} | 27 +-- gost/http.go | 2 +- gost/http2.go | 244 ++++++++++++++++++++++- gost/tls.go | 64 ++++++ 6 files changed, 353 insertions(+), 20 deletions(-) rename gost/examples/http2/{srv.go => http2.go} (95%) diff --git a/gost/examples/bench/cli.go b/gost/examples/bench/cli.go index ec488c4..47948f1 100644 --- a/gost/examples/bench/cli.go +++ b/gost/examples/bench/cli.go @@ -139,12 +139,21 @@ func main() { }, */ - // http+quic + /* + // http+quic + gost.Node{ + Addr: "localhost:6121", + Client: &gost.Client{ + Connector: gost.HTTPConnector(url.UserPassword("admin", "123456")), + Transporter: gost.QUICTransporter(nil), + }, + }, + */ gost.Node{ - Addr: "localhost:6121", + Addr: "localhost:8443", Client: &gost.Client{ Connector: gost.HTTPConnector(url.UserPassword("admin", "123456")), - Transporter: gost.QUICTransporter(nil), + Transporter: gost.H2Transporter(), }, }, ) diff --git a/gost/examples/bench/srv.go b/gost/examples/bench/srv.go index fe977bf..0306f77 100644 --- a/gost/examples/bench/srv.go +++ b/gost/examples/bench/srv.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ginuerzh/gost/gost" + "golang.org/x/net/http2" ) var ( @@ -19,6 +20,7 @@ func init() { flag.BoolVar(&quiet, "q", false, "quiet mode") flag.BoolVar(&gost.Debug, "d", false, "debug mode") + flag.BoolVar(&http2.VerboseLogs, "v", false, "HTTP2 verbose logs") flag.Parse() if quiet { @@ -40,6 +42,7 @@ func main() { // go tcpRedirectServer() go sshTunnelServer() go http2Server() + go http2TunnelServer() go quicServer() go shadowUDPServer() select {} @@ -230,6 +233,19 @@ func http2Server() { log.Fatal(s.Serve(ln, h)) } +func http2TunnelServer() { + s := &gost.Server{} + ln, err := gost.H2Listener(":8443", tlsConfig()) // HTTP2 h2 mode + // ln, err := gost.H2Listener(":8443", nil) // HTTP2 h2c mode + if err != nil { + log.Fatal(err) + } + h := gost.HTTPHandler( + gost.UsersHandlerOption(url.UserPassword("admin", "123456")), + ) + log.Fatal(s.Serve(ln, h)) +} + func quicServer() { s := &gost.Server{} ln, err := gost.QUICListener("localhost:6121", &gost.QUICConfig{TLSConfig: tlsConfig()}) @@ -320,5 +336,8 @@ func tlsConfig() *tls.Config { if err != nil { panic(err) } - return &tls.Config{Certificates: []tls.Certificate{cert}} + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + PreferServerCipherSuites: true, + } } diff --git a/gost/examples/http2/srv.go b/gost/examples/http2/http2.go similarity index 95% rename from gost/examples/http2/srv.go rename to gost/examples/http2/http2.go index 16e69aa..951bf77 100644 --- a/gost/examples/http2/srv.go +++ b/gost/examples/http2/http2.go @@ -45,8 +45,17 @@ func main() { func http2Server() { s := &gost.Server{} - var err error - var ln gost.Listener + ln, err := gost.TCPListener(laddr) + if err != nil { + log.Fatal(err) + } + + var users []*url.Userinfo + if user != "" || passwd != "" { + users = append(users, url.UserPassword(user, passwd)) + } + + var tlsConfig *tls.Config if tlsEnabled { cert, er := tls.LoadX509KeyPair(certFile, keyFile) if er != nil { @@ -56,20 +65,12 @@ func http2Server() { panic(er) } } - ln, err = gost.TLSListener(laddr, &tls.Config{Certificates: []tls.Certificate{cert}}) // HTTP2 h2 mode - } else { - ln, err = gost.TCPListener(laddr) - } - if err != nil { - log.Fatal(err) - } - - var users []*url.Userinfo - if user != "" || passwd != "" { - users = append(users, url.UserPassword(user, passwd)) + tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}} } h := gost.HTTP2Handler( gost.UsersHandlerOption(users...), + gost.AddrHandlerOption(laddr), + gost.TLSConfigHandlerOption(tlsConfig), ) log.Fatal(s.Serve(ln, h)) } diff --git a/gost/http.go b/gost/http.go index 9a51c8e..df41f22 100644 --- a/gost/http.go +++ b/gost/http.go @@ -89,7 +89,7 @@ func (h *httpHandler) Handle(conn net.Conn) { req, err := http.ReadRequest(bufio.NewReader(conn)) if err != nil { - log.Log("[http]", err) + log.Logf("[http] %s - %s : %s", conn.RemoteAddr(), conn.LocalAddr(), err) return } diff --git a/gost/http2.go b/gost/http2.go index eadb896..8cd6f02 100644 --- a/gost/http2.go +++ b/gost/http2.go @@ -148,7 +148,99 @@ func (tr *http2Transporter) Multiplex() bool { return true } +type h2Transporter struct { + clients map[string]*http.Client + clientMutex sync.Mutex + tlsConfig *tls.Config +} + +func H2Transporter() Transporter { + return &h2Transporter{ + clients: make(map[string]*http.Client), + tlsConfig: &tls.Config{InsecureSkipVerify: true}, + } +} + +func (tr *h2Transporter) Dial(addr string, options ...DialOption) (net.Conn, error) { + opts := &DialOptions{} + for _, option := range options { + option(opts) + } + + tr.clientMutex.Lock() + client, ok := tr.clients[addr] + if !ok { + transport := http2.Transport{ + TLSClientConfig: tr.tlsConfig, + DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { + conn, err := opts.Chain.Dial(addr) + if err != nil { + return nil, err + } + if cfg == nil { + return conn, nil + } + return wrapTLSClient(conn, cfg) + }, + } + client = &http.Client{Transport: &transport} + tr.clients[addr] = client + } + tr.clientMutex.Unlock() + + pr, pw := io.Pipe() + req := &http.Request{ + Method: http.MethodConnect, + URL: &url.URL{Scheme: "https", Host: addr}, + Header: make(http.Header), + Proto: "HTTP/2.0", + ProtoMajor: 2, + ProtoMinor: 0, + Body: pr, + Host: addr, + ContentLength: -1, + } + if Debug { + dump, _ := httputil.DumpRequest(req, false) + log.Log("[http2]", string(dump)) + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + if Debug { + dump, _ := httputil.DumpResponse(resp, false) + log.Log("[http2]", string(dump)) + } + + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, errors.New(resp.Status) + } + conn := &http2Conn{ + r: resp.Body, + w: pw, + closed: make(chan struct{}), + } + conn.remoteAddr, _ = net.ResolveTCPAddr("tcp", addr) + conn.localAddr = &net.TCPAddr{IP: net.IPv4zero, Port: 0} + return conn, nil +} + +func (tr *h2Transporter) Handshake(conn net.Conn, options ...HandshakeOption) (net.Conn, error) { + opts := &HandshakeOptions{} + for _, option := range options { + option(opts) + } + return conn, nil +} + +func (tr *h2Transporter) Multiplex() bool { + return true +} + type http2Handler struct { + base *http.Server server *http2.Server options *HandlerOptions } @@ -156,18 +248,31 @@ type http2Handler struct { // HTTP2Handler creates a server Handler for HTTP2 proxy server. func HTTP2Handler(opts ...HandlerOption) Handler { h := &http2Handler{ - server: new(http2.Server), options: new(HandlerOptions), } for _, opt := range opts { opt(h.options) } + + h.base = &http.Server{ + Addr: h.options.Addr, + TLSConfig: h.options.TLSConfig, + Handler: http.HandlerFunc(h.handleFunc), + } + h.server = new(http2.Server) + if err := http2.ConfigureServer(h.base, h.server); err != nil { + log.Log("[http2]", err) + } return h } func (h *http2Handler) Handle(conn net.Conn) { defer conn.Close() + if h.options.TLSConfig != nil { + conn = tls.Server(conn, h.options.TLSConfig) + } + if tc, ok := conn.(*tls.Conn); ok { // NOTE: HTTP2 server will check the TLS version, // so we must ensure that the TLS connection is handshake completed. @@ -178,7 +283,8 @@ func (h *http2Handler) Handle(conn net.Conn) { } opt := http2.ServeConnOpts{ - Handler: http.HandlerFunc(h.handleFunc), + BaseConfig: h.base, + Handler: http.HandlerFunc(h.handleFunc), } h.server.ServeConn(conn, &opt) } @@ -292,6 +398,132 @@ func (h *http2Handler) handleFunc(w http.ResponseWriter, r *http.Request) { log.Logf("[http2] %s >-< %s", r.RemoteAddr, target) } +type h2Listener struct { + net.Listener + server *http2.Server + tlsConfig *tls.Config + connChan chan net.Conn + errChan chan error +} + +func H2Listener(addr string, config *tls.Config) (Listener, error) { + ln, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + l := &h2Listener{ + Listener: ln, + server: &http2.Server{ + MaxConcurrentStreams: 1000, + PermitProhibitedCipherSuites: true, + }, + tlsConfig: config, + connChan: make(chan net.Conn, 1024), + errChan: make(chan error, 1), + } + go l.listenLoop() + + return l, nil +} + +func H2CListener(addr string) (Listener, error) { + return H2Listener(addr, nil) +} + +func (l *h2Listener) listenLoop() { + for { + conn, err := l.Listener.Accept() + if err != nil { + log.Log("[http2] accept:", err) + l.errChan <- err + close(l.errChan) + return + } + go l.handleLoop(conn) + } +} + +func (l *h2Listener) handleLoop(conn net.Conn) { + if l.tlsConfig != nil { + conn = tls.Server(conn, l.tlsConfig) + } + + if tc, ok := conn.(*tls.Conn); ok { + // NOTE: HTTP2 server will check the TLS version, + // so we must ensure that the TLS connection is handshake completed. + if err := tc.Handshake(); err != nil { + log.Logf("[http2] %s - %s : %s", conn.RemoteAddr(), conn.LocalAddr(), err) + return + } + } + + opt := http2.ServeConnOpts{ + Handler: http.HandlerFunc(l.handleFunc), + } + l.server.ServeConn(conn, &opt) +} + +func (l *h2Listener) handleFunc(w http.ResponseWriter, r *http.Request) { + log.Logf("[http2] %s %s - %s %s", r.Method, r.RemoteAddr, r.Host, r.Proto) + if Debug { + dump, _ := httputil.DumpRequest(r, false) + log.Log("[http2]", string(dump)) + } + w.Header().Set("Proxy-Agent", "gost/"+Version) + conn, err := l.upgrade(w, r) + if err != nil { + log.Logf("[http2] %s %s - %s %s", r.Method, r.RemoteAddr, r.Host, r.Proto) + return + } + select { + case l.connChan <- conn: + default: + conn.Close() + log.Logf("[http2] %s - %s: connection queue is full", conn.RemoteAddr(), conn.LocalAddr()) + } + + <-conn.closed // wait for streaming +} + +func (l *h2Listener) upgrade(w http.ResponseWriter, r *http.Request) (*http2Conn, error) { + if r.Method != http.MethodConnect { + w.WriteHeader(http.StatusMethodNotAllowed) + return nil, errors.New("Method not allowed") + } + w.WriteHeader(http.StatusOK) + if fw, ok := w.(http.Flusher); ok { + fw.Flush() // write header to client + } + + remoteAddr, _ := net.ResolveTCPAddr("tcp", r.RemoteAddr) + if remoteAddr == nil { + remoteAddr = &net.TCPAddr{ + IP: net.IPv4zero, + Port: 0, + } + } + conn := &http2Conn{ + r: r.Body, + w: flushWriter{w}, + localAddr: l.Listener.Addr(), + remoteAddr: remoteAddr, + closed: make(chan struct{}), + } + return conn, nil +} + +func (l *h2Listener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.connChan: + case err, ok = <-l.errChan: + if !ok { + err = errors.New("accpet on closed listener") + } + } + return +} + type http2Session struct { conn net.Conn clientConn *http2.ClientConn @@ -383,6 +615,7 @@ type http2Conn struct { w io.Writer remoteAddr net.Addr localAddr net.Addr + closed chan struct{} } func (c *http2Conn) Read(b []byte) (n int, err error) { @@ -394,6 +627,12 @@ func (c *http2Conn) Write(b []byte) (n int, err error) { } func (c *http2Conn) Close() (err error) { + select { + case <-c.closed: + return + default: + close(c.closed) + } if rc, ok := c.r.(io.Closer); ok { err = rc.Close() } @@ -470,6 +709,7 @@ func (fw flushWriter) Write(p []byte) (n int, err error) { if r := recover(); r != nil { if s, ok := r.(string); ok { err = errors.New(s) + log.Log("[http2]", err) return } err = r.(error) diff --git a/gost/tls.go b/gost/tls.go index 0140d5b..613e8a4 100644 --- a/gost/tls.go +++ b/gost/tls.go @@ -2,7 +2,9 @@ package gost import ( "crypto/tls" + "crypto/x509" "net" + "time" ) type tlsTransporter struct { @@ -38,3 +40,65 @@ func TLSListener(addr string, config *tls.Config) (Listener, error) { } return &tlsListener{ln}, nil } + +// Wrap a net.Conn into a client tls connection, performing any +// additional verification as needed. +// +// As of go 1.3, crypto/tls only supports either doing no certificate +// verification, or doing full verification including of the peer's +// DNS name. For consul, we want to validate that the certificate is +// signed by a known CA, but because consul doesn't use DNS names for +// node names, we don't verify the certificate DNS names. Since go 1.3 +// no longer supports this mode of operation, we have to do it +// manually. +// +// This code is taken from consul: +// https://github.com/hashicorp/consul/blob/master/tlsutil/config.go +func wrapTLSClient(conn net.Conn, tlsConfig *tls.Config) (net.Conn, error) { + var err error + var tlsConn *tls.Conn + + tlsConn = tls.Client(conn, tlsConfig) + + // If crypto/tls is doing verification, there's no need to do our own. + if tlsConfig.InsecureSkipVerify == false { + return tlsConn, nil + } + + // Similarly if we use host's CA, we can do full handshake + if tlsConfig.RootCAs == nil { + return tlsConn, nil + } + + // Otherwise perform handshake, but don't verify the domain + // + // The following is lightly-modified from the doFullHandshake + // method in https://golang.org/src/crypto/tls/handshake_client.go + if err = tlsConn.Handshake(); err != nil { + tlsConn.Close() + return nil, err + } + + opts := x509.VerifyOptions{ + Roots: tlsConfig.RootCAs, + CurrentTime: time.Now(), + DNSName: "", + Intermediates: x509.NewCertPool(), + } + + certs := tlsConn.ConnectionState().PeerCertificates + for i, cert := range certs { + if i == 0 { + continue + } + opts.Intermediates.AddCert(cert) + } + + _, err = certs[0].Verify(opts) + if err != nil { + tlsConn.Close() + return nil, err + } + + return tlsConn, err +}