From 346c2c27e58c7c6a78f11de5522c69393f76efd0 Mon Sep 17 00:00:00 2001 From: "rui.zheng" Date: Mon, 24 Jul 2017 20:28:25 +0800 Subject: [PATCH] add HTTP2 proxy support --- gost/cli/cli.go | 73 +++++--- gost/client.go | 6 + gost/gost.go | 4 + gost/http.go | 36 ++-- gost/http2.go | 444 ++++++++++++++++++++++++++++++++++++++++++++++++ gost/kcp.go | 6 +- gost/srv/srv.go | 64 ++++--- gost/ssh.go | 1 + gost/tls.go | 12 +- gost/ws.go | 8 + 10 files changed, 581 insertions(+), 73 deletions(-) create mode 100644 gost/http2.go create mode 100644 gost/ssh.go diff --git a/gost/cli/cli.go b/gost/cli/cli.go index ce92618..2100974 100644 --- a/gost/cli/cli.go +++ b/gost/cli/cli.go @@ -2,9 +2,13 @@ package main import ( "bufio" + "crypto/tls" "log" "net/http" "net/http/httputil" + "net/url" + + "time" "github.com/ginuerzh/gost/gost" ) @@ -85,36 +89,55 @@ func main() { }, */ - // http+kcp + // http2 gost.Node{ - Addr: "127.0.0.1:8388", + Addr: "127.0.0.1:1443", Client: gost.NewClient( - gost.HTTPConnector(nil), - gost.KCPTransporter(nil), + gost.HTTP2Connector(url.UserPassword("admin", "123456")), + gost.HTTP2Transporter( + nil, + &tls.Config{InsecureSkipVerify: true}, + time.Second*60, + ), ), }, + + /* + // http+kcp + gost.Node{ + Addr: "127.0.0.1:8388", + Client: gost.NewClient( + gost.HTTPConnector(nil), + gost.KCPTransporter(nil), + ), + }, + */ ) - conn, err := chain.Dial("localhost:10000") - if err != nil { - log.Fatal(err) - } - //conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true}) - req, err := http.NewRequest(http.MethodGet, "http://localhost:10000/pkg", nil) - if err != nil { - log.Fatal(err) - } - if err := req.Write(conn); err != nil { - log.Fatal(err) - } - resp, err := http.ReadResponse(bufio.NewReader(conn), req) - if err != nil { - log.Fatal(err) - } - defer resp.Body.Close() + for i := 0; i < 10; i++ { + conn, err := chain.Dial("localhost:10000") + if err != nil { + log.Fatal(err) + } + //conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true}) + req, err := http.NewRequest(http.MethodGet, "http://localhost:10000/pkg", nil) + if err != nil { + log.Fatal(err) + } + if err := req.Write(conn); err != nil { + log.Fatal(err) + } + resp, err := http.ReadResponse(bufio.NewReader(conn), req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() - rb, _ := httputil.DumpRequest(req, true) - log.Println(string(rb)) - rb, _ = httputil.DumpResponse(resp, true) - log.Println(string(rb)) + rb, _ := httputil.DumpRequest(req, true) + log.Println(string(rb)) + rb, _ = httputil.DumpResponse(resp, true) + log.Println(string(rb)) + + time.Sleep(100 * time.Millisecond) + } } diff --git a/gost/client.go b/gost/client.go index abc7867..9f458ba 100644 --- a/gost/client.go +++ b/gost/client.go @@ -63,6 +63,8 @@ type Connector interface { type Transporter interface { Dial(addr string) (net.Conn, error) Handshake(conn net.Conn) (net.Conn, error) + // Indicate that the Transporter supports multiplex + Multiplex() bool } type tcpTransporter struct { @@ -80,3 +82,7 @@ func (tr *tcpTransporter) Dial(addr string) (net.Conn, error) { func (tr *tcpTransporter) Handshake(conn net.Conn) (net.Conn, error) { return conn, nil } + +func (tr *tcpTransporter) Multiplex() bool { + return false +} diff --git a/gost/gost.go b/gost/gost.go index 373b5f2..1a9b7da 100644 --- a/gost/gost.go +++ b/gost/gost.go @@ -28,6 +28,10 @@ var ( ReadTimeout = 30 * time.Second // WriteTimeout is the timeout for writing. WriteTimeout = 60 * time.Second + // PingTimeout is the timeout for pinging. + PingTimeout = 30 * time.Second + // PingRetries is the reties of ping. + PingRetries = 3 // default udp node TTL in second for udp port forwarding. defaultTTL = 60 ) diff --git a/gost/http.go b/gost/http.go index 7fc76a2..959a302 100644 --- a/gost/http.go +++ b/gost/http.go @@ -110,21 +110,8 @@ func (h *httpHandler) Handle(conn net.Conn) { return } - valid := false - u, p, _ := h.basicProxyAuth(req.Header.Get("Proxy-Authorization")) - users := h.options.Users - for _, user := range users { - username := user.Username() - password, _ := user.Password() - if (u == username && p == password) || - (u == username && password == "") || - (username == "" && p == password) { - valid = true - break - } - } - - if len(users) > 0 && !valid { + u, p, _ := basicProxyAuth(req.Header.Get("Proxy-Authorization")) + if !authenticate(u, p, h.options.Users...) { log.Logf("[http] %s <- %s : proxy authentication required", conn.RemoteAddr(), req.Host) resp := "HTTP/1.1 407 Proxy Authentication Required\r\n" + "Proxy-Authenticate: Basic realm=\"gost\"\r\n" + @@ -182,7 +169,7 @@ func (h *httpHandler) Handle(conn net.Conn) { log.Logf("[http] %s >-< %s", conn.RemoteAddr(), req.Host) } -func (h *httpHandler) basicProxyAuth(proxyAuth string) (username, password string, ok bool) { +func basicProxyAuth(proxyAuth string) (username, password string, ok bool) { if proxyAuth == "" { return } @@ -202,3 +189,20 @@ func (h *httpHandler) basicProxyAuth(proxyAuth string) (username, password strin return cs[:s], cs[s+1:], true } + +func authenticate(username, password string, users ...*url.Userinfo) bool { + if len(users) == 0 { + return true + } + + for _, user := range users { + u := user.Username() + p, _ := user.Password() + if (u == username && p == password) || + (u == username && p == "") || + (u == "" && p == password) { + return true + } + } + return false +} diff --git a/gost/http2.go b/gost/http2.go new file mode 100644 index 0000000..ad09d6e --- /dev/null +++ b/gost/http2.go @@ -0,0 +1,444 @@ +package gost + +import ( + "bufio" + "context" + "crypto/tls" + "encoding/base64" + "errors" + "io" + "io/ioutil" + "net" + "net/http" + "net/http/httputil" + "net/url" + "sync" + "time" + + "github.com/go-log/log" + "golang.org/x/net/http2" +) + +type http2Connector struct { + User *url.Userinfo +} + +// HTTP2Connector creates a Connector for HTTP2 proxy client. +// It accepts an optional auth info for HTTP Basic Authentication. +func HTTP2Connector(user *url.Userinfo) Connector { + return &http2Connector{User: user} +} + +func (c *http2Connector) Connect(conn net.Conn, addr string) (net.Conn, error) { + cc, ok := conn.(*http2DummyConn) + if !ok { + return nil, errors.New("conn must be a conn wrapper") + } + + pr, pw := io.Pipe() + u := &url.URL{ + Host: addr, + } + req, err := http.NewRequest("CONNECT", u.String(), ioutil.NopCloser(pr)) + if err != nil { + log.Logf("[http2] %s - %s : %s", cc.raddr, addr, err) + return nil, err + } + if c.User != nil { + req.Header.Set("Proxy-Authorization", + "Basic "+base64.StdEncoding.EncodeToString([]byte(c.User.String()))) + } + req.ProtoMajor = 2 + req.ProtoMinor = 0 + + if Debug { + dump, _ := httputil.DumpRequest(req, false) + log.Log("[http2]", string(dump)) + } + resp, err := cc.conn.RoundTrip(req) + if err != nil { + log.Logf("[http2] %s - %s : %s", cc.raddr, addr, err) + 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) + } + hc := &http2Conn{r: resp.Body, w: pw} + hc.remoteAddr, _ = net.ResolveTCPAddr("tcp", cc.raddr) + return hc, nil +} + +type http2Transporter struct { + tlsConfig *tls.Config + tr *http2.Transport + chain *Chain + conns map[string]*http2.ClientConn + connMutex sync.Mutex + pingInterval time.Duration +} + +// HTTP2Transporter creates a Transporter that is used by HTTP2 proxy client. +// +// Optional chain is a proxy chain that can be used to establish a connection with the HTTP2 server. +// +// Optional config is a TLS config for TLS handshake, if is nil, will use h2c mode. +// +// Optional ping is the ping interval, if is zero, ping will not be enabled. +func HTTP2Transporter(chain *Chain, config *tls.Config, ping time.Duration) Transporter { + return &http2Transporter{ + tlsConfig: config, + tr: new(http2.Transport), + chain: chain, + pingInterval: ping, + conns: make(map[string]*http2.ClientConn), + } +} + +func (tr *http2Transporter) Dial(addr string) (net.Conn, error) { + tr.connMutex.Lock() + conn, ok := tr.conns[addr] + + if !ok { + cc, err := tr.chain.Dial(addr) + if err != nil { + tr.connMutex.Unlock() + return nil, err + } + + if tr.tlsConfig != nil { + tc := tls.Client(cc, tr.tlsConfig) + if err := tc.Handshake(); err != nil { + tr.connMutex.Unlock() + return nil, err + } + cc = tc + } + conn, err = tr.tr.NewClientConn(cc) + if err != nil { + tr.connMutex.Unlock() + return nil, err + } + tr.conns[addr] = conn + go tr.ping(tr.pingInterval, addr, conn) + } + tr.connMutex.Unlock() + + if !conn.CanTakeNewRequest() { + tr.connMutex.Lock() + delete(tr.conns, addr) // TODO: we could re-connect to the addr automatically. + tr.connMutex.Unlock() + return nil, errors.New("connection is dead") + } + + return &http2DummyConn{ + raddr: addr, + conn: conn, + }, nil +} + +func (tr *http2Transporter) ping(interval time.Duration, addr string, conn *http2.ClientConn) { + if interval <= 0 { + return + } + log.Log("[http2] ping is enabled, interval:", interval) + + baseCtx := context.Background() + t := time.NewTicker(interval) + retries := PingRetries + for { + select { + case <-t.C: + if !conn.CanTakeNewRequest() { + return + } + ctx, cancel := context.WithTimeout(baseCtx, PingTimeout) + if err := conn.Ping(ctx); err != nil { + log.Logf("[http2] ping: %s", err) + if retries > 0 { + retries-- + log.Log("[http2] retry ping") + cancel() + continue + } + + // connection is dead, remove it. + tr.connMutex.Lock() + delete(tr.conns, addr) + tr.connMutex.Unlock() + + cancel() + return + } + + cancel() + retries = PingRetries + } + } +} + +func (tr *http2Transporter) Handshake(conn net.Conn) (net.Conn, error) { + return conn, nil +} + +func (tr *http2Transporter) Multiplex() bool { + return true +} + +type http2Handler struct { + server *http2.Server + options *HandlerOptions +} + +// HTTP2Handler creates a server Handler for HTTP2 proxy server. +func HTTP2Handler(opts ...HandlerOption) Handler { + h := &http2Handler{ + server: new(http2.Server), + options: &HandlerOptions{ + Chain: new(Chain), + }, + } + for _, opt := range opts { + opt(h.options) + } + return h +} + +func (h *http2Handler) Handle(conn net.Conn) { + defer conn.Close() + + 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(h.handleFunc), + } + h.server.ServeConn(conn, &opt) +} + +func (h *http2Handler) handleFunc(w http.ResponseWriter, r *http.Request) { + target := r.Header.Get("Gost-Target") // compitable with old version + if target == "" { + target = r.Host + } + + log.Logf("[http2] %s %s - %s %s", r.Method, r.RemoteAddr, target, r.Proto) + if Debug { + dump, _ := httputil.DumpRequest(r, false) + log.Log("[http2]", string(dump)) + } + + w.Header().Set("Proxy-Agent", "gost/"+Version) + + //! if !s.Base.Node.Can("tcp", target) { + //! glog.Errorf("Unauthorized to tcp connect to %s", target) + //! return + //! } + + u, p, _ := basicProxyAuth(r.Header.Get("Proxy-Authorization")) + if !authenticate(u, p, h.options.Users...) { + log.Logf("[http2] %s <- %s : proxy authentication required", r.RemoteAddr, target) + w.Header().Set("Proxy-Authenticate", "Basic realm=\"gost\"") + w.WriteHeader(http.StatusProxyAuthRequired) + return + } + + r.Header.Del("Proxy-Authorization") + r.Header.Del("Proxy-Connection") + + cc, err := h.options.Chain.Dial(target) + if err != nil { + log.Logf("[http2] %s -> %s : %s", r.RemoteAddr, target, err) + w.WriteHeader(http.StatusServiceUnavailable) + return + } + defer cc.Close() + + log.Logf("[http2] %s <-> %s", r.RemoteAddr, target) + + if r.Method == http.MethodConnect { + w.WriteHeader(http.StatusOK) + if fw, ok := w.(http.Flusher); ok { + fw.Flush() + } + + // compatible with HTTP1.x + if hj, ok := w.(http.Hijacker); ok && r.ProtoMajor == 1 { + // we take over the underly connection + conn, _, err := hj.Hijack() + if err != nil { + log.Logf("[http2] %s -> %s : %s", r.RemoteAddr, target, err) + w.WriteHeader(http.StatusInternalServerError) + return + } + defer conn.Close() + + log.Logf("[http2] %s -> %s : downgrade to HTTP/1.1", r.RemoteAddr, target) + transport(conn, cc) + log.Logf("[http2] %s >-< %s", r.RemoteAddr, target) + return + } + + errc := make(chan error, 2) + go func() { + _, err := io.Copy(cc, r.Body) + errc <- err + }() + go func() { + _, err := io.Copy(flushWriter{w}, cc) + errc <- err + }() + + select { + case <-errc: + // glog.V(LWARNING).Infoln("exit", err) + } + log.Logf("[http2] %s >-< %s", r.RemoteAddr, target) + return + } + + if err = r.Write(cc); err != nil { + log.Logf("[http2] %s -> %s : %s", r.RemoteAddr, target, err) + return + } + + resp, err := http.ReadResponse(bufio.NewReader(cc), r) + if err != nil { + log.Logf("[http2] %s -> %s : %s", r.RemoteAddr, target, err) + return + } + defer resp.Body.Close() + + for k, v := range resp.Header { + for _, vv := range v { + w.Header().Add(k, vv) + } + } + w.WriteHeader(resp.StatusCode) + if _, err := io.Copy(flushWriter{w}, resp.Body); err != nil { + log.Logf("[http2] %s <- %s : %s", r.RemoteAddr, target, err) + } + log.Logf("[http2] %s >-< %s", r.RemoteAddr, target) +} + +// HTTP2 connection, wrapped up just like a net.Conn +type http2Conn struct { + r io.Reader + w io.Writer + remoteAddr net.Addr + localAddr net.Addr +} + +func (c *http2Conn) Read(b []byte) (n int, err error) { + return c.r.Read(b) +} + +func (c *http2Conn) Write(b []byte) (n int, err error) { + return c.w.Write(b) +} + +func (c *http2Conn) Close() (err error) { + if rc, ok := c.r.(io.Closer); ok { + err = rc.Close() + } + if w, ok := c.w.(io.Closer); ok { + err = w.Close() + } + return +} + +func (c *http2Conn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *http2Conn) RemoteAddr() net.Addr { + return c.remoteAddr +} + +func (c *http2Conn) SetDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *http2Conn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *http2Conn) SetWriteDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +// Dummy HTTP2 connection. +type http2DummyConn struct { + raddr string + conn *http2.ClientConn +} + +func (c *http2DummyConn) Read(b []byte) (n int, err error) { + return 0, &net.OpError{Op: "read", Net: "http2", Source: nil, Addr: nil, Err: errors.New("read not supported")} +} + +func (c *http2DummyConn) Write(b []byte) (n int, err error) { + return 0, &net.OpError{Op: "write", Net: "http2", Source: nil, Addr: nil, Err: errors.New("write not supported")} +} + +func (c *http2DummyConn) Close() error { + return nil +} + +func (c *http2DummyConn) LocalAddr() net.Addr { + return nil +} + +func (c *http2DummyConn) RemoteAddr() net.Addr { + return nil +} + +func (c *http2DummyConn) SetDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *http2DummyConn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *http2DummyConn) SetWriteDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +type flushWriter struct { + w io.Writer +} + +func (fw flushWriter) Write(p []byte) (n int, err error) { + defer func() { + if r := recover(); r != nil { + if s, ok := r.(string); ok { + err = errors.New(s) + return + } + err = r.(error) + } + }() + + n, err = fw.w.Write(p) + if err != nil { + // log.Log("flush writer:", err) + return + } + if f, ok := fw.w.(http.Flusher); ok { + f.Flush() + } + return +} diff --git a/gost/kcp.go b/gost/kcp.go index 15cf8a0..95d3ad4 100644 --- a/gost/kcp.go +++ b/gost/kcp.go @@ -196,7 +196,7 @@ func (tr *kcpTransporter) Dial(addr string) (conn net.Conn, err error) { if err != nil { tr.sessionMutex.Lock() session.Close() - delete(tr.sessions, addr) + delete(tr.sessions, addr) // TODO: we could obtain a new session automatically. tr.sessionMutex.Unlock() } return @@ -245,6 +245,10 @@ func (tr *kcpTransporter) Handshake(conn net.Conn) (net.Conn, error) { return conn, nil } +func (tr *kcpTransporter) Multiplex() bool { + return true +} + type kcpListener struct { config *KCPConfig ln *kcp.Listener diff --git a/gost/srv/srv.go b/gost/srv/srv.go index 4d467ea..77185a1 100644 --- a/gost/srv/srv.go +++ b/gost/srv/srv.go @@ -15,17 +15,18 @@ func init() { } func main() { - go httpServer() - go socks5Server() - go tlsServer() - go shadowServer() - go wsServer() - go wssServer() - go kcpServer() - go tcpForwardServer() - go rtcpForwardServer() + // go httpServer() + // go socks5Server() + // go tlsServer() + // go shadowServer() + // go wsServer() + // go wssServer() + // go kcpServer() + // go tcpForwardServer() + // go rtcpForwardServer() // go rudpForwardServer() - go tcpRedirectServer() + // go tcpRedirectServer() + go http2Server() select {} } @@ -43,15 +44,10 @@ func httpServer() { } func socks5Server() { - cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem") - if err != nil { - log.Fatal(err) - } - s := &gost.Server{} s.Handle(gost.SOCKS5Handler( gost.UsersHandlerOption(url.UserPassword("admin", "123456")), - gost.TLSConfigHandlerOption(&tls.Config{Certificates: []tls.Certificate{cert}}), + gost.TLSConfigHandlerOption(tlsConfig()), )) ln, err := gost.TCPListener(":1080") if err != nil { @@ -77,11 +73,7 @@ func tlsServer() { s.Handle(gost.HTTPHandler( gost.UsersHandlerOption(url.UserPassword("admin", "123456")), )) - cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem") - if err != nil { - log.Fatal(err) - } - ln, err := gost.TLSListener(":1443", &tls.Config{Certificates: []tls.Certificate{cert}}) + ln, err := gost.TLSListener(":1443", tlsConfig()) if err != nil { log.Fatal(err) } @@ -105,12 +97,7 @@ func wssServer() { s.Handle(gost.HTTPHandler( gost.UsersHandlerOption(url.UserPassword("admin", "123456")), )) - - cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem") - if err != nil { - log.Fatal(err) - } - ln, err := gost.WSSListener(":8443", &gost.WSOptions{TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}}) + ln, err := gost.WSSListener(":8443", &gost.WSOptions{TLSConfig: tlsConfig()}) if err != nil { log.Fatal(err) } @@ -194,3 +181,26 @@ func tcpRedirectServer() { } log.Fatal(s.Serve(ln)) } + +func http2Server() { + // http2.VerboseLogs = true + + s := &gost.Server{} + s.Handle(gost.HTTP2Handler( + gost.UsersHandlerOption(url.UserPassword("admin", "123456")), + )) + ln, err := gost.TLSListener(":1443", tlsConfig()) + // ln, err := gost.TCPListener(":1443") + if err != nil { + log.Fatal(err) + } + log.Fatal(s.Serve(ln)) +} + +func tlsConfig() *tls.Config { + cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem") + if err != nil { + panic(err) + } + return &tls.Config{Certificates: []tls.Certificate{cert}} +} diff --git a/gost/ssh.go b/gost/ssh.go new file mode 100644 index 0000000..71be68f --- /dev/null +++ b/gost/ssh.go @@ -0,0 +1 @@ +package gost diff --git a/gost/tls.go b/gost/tls.go index 79309fe..9a9bc54 100644 --- a/gost/tls.go +++ b/gost/tls.go @@ -6,13 +6,13 @@ import ( ) type tlsTransporter struct { - TLSClientConfig *tls.Config + tlsConfig *tls.Config } // TLSTransporter creates a Transporter that is used by TLS proxy client. // It accepts a TLS config for TLS handshake. func TLSTransporter(cfg *tls.Config) Transporter { - return &tlsTransporter{TLSClientConfig: cfg} + return &tlsTransporter{tlsConfig: cfg} } func (tr *tlsTransporter) Dial(addr string) (net.Conn, error) { @@ -20,7 +20,11 @@ func (tr *tlsTransporter) Dial(addr string) (net.Conn, error) { } func (tr *tlsTransporter) Handshake(conn net.Conn) (net.Conn, error) { - return tls.Client(conn, tr.TLSClientConfig), nil + return tls.Client(conn, tr.tlsConfig), nil +} + +func (tr *tlsTransporter) Multiplex() bool { + return false } type tlsListener struct { @@ -33,5 +37,5 @@ func TLSListener(addr string, config *tls.Config) (Listener, error) { if err != nil { return nil, err } - return &tlsListener{Listener: ln}, nil + return &tlsListener{ln}, nil } diff --git a/gost/ws.go b/gost/ws.go index 0045575..9f9d615 100644 --- a/gost/ws.go +++ b/gost/ws.go @@ -120,6 +120,10 @@ func (tr *wsTransporter) Handshake(conn net.Conn) (net.Conn, error) { return websocketClientConn(url.String(), conn, tr.options) } +func (tr *wsTransporter) Multiplex() bool { + return false +} + type wssTransporter struct { addr string options *WSOptions @@ -142,6 +146,10 @@ func (tr *wssTransporter) Handshake(conn net.Conn) (net.Conn, error) { return websocketClientConn(url.String(), conn, tr.options) } +func (tr *wssTransporter) Multiplex() bool { + return false +} + type wsListener struct { addr net.Addr upgrader *websocket.Upgrader