From 9397cb53519e8b059d7536a0c1e9dc58cda2820b Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Mon, 14 Mar 2022 20:27:14 +0800 Subject: [PATCH] initial commit --- README.md | 1 + connector/http2/conn.go | 54 ++ connector/http2/connector.go | 121 ++++ connector/http2/metadata.go | 31 + connector/relay/bind.go | 133 ++++ connector/relay/conn.go | 132 ++++ connector/relay/connector.go | 127 ++++ connector/relay/listener.go | 73 +++ connector/relay/metadata.go | 24 + connector/sni/conn.go | 128 ++++ connector/sni/connector.go | 46 ++ connector/sni/metadata.go | 24 + connector/ss/connector.go | 111 ++++ connector/ss/metadata.go | 27 + connector/ss/udp/connector.go | 95 +++ connector/ss/udp/metadata.go | 33 + connector/sshd/connector.go | 80 +++ dialer/ftcp/conn.go | 21 + dialer/ftcp/dialer.go | 51 ++ dialer/ftcp/metadata.go | 23 + dialer/grpc/conn.go | 92 +++ dialer/grpc/dialer.go | 113 ++++ dialer/grpc/metadata.go | 22 + dialer/http2/dialer.go | 101 +++ dialer/http2/h2/conn.go | 54 ++ dialer/http2/h2/dialer.go | 165 +++++ dialer/http2/h2/metadata.go | 22 + dialer/http2/metadata.go | 12 + dialer/http3/dialer.go | 107 ++++ dialer/http3/metadata.go | 52 ++ dialer/icmp/conn.go | 42 ++ dialer/icmp/dialer.go | 130 ++++ dialer/icmp/metadata.go | 29 + dialer/kcp/conn.go | 55 ++ dialer/kcp/dialer.go | 165 +++++ dialer/kcp/metadata.go | 39 ++ dialer/mtls/conn.go | 38 ++ dialer/mtls/dialer.go | 156 +++++ dialer/mtls/metadata.go | 42 ++ dialer/mws/conn.go | 38 ++ dialer/mws/dialer.go | 215 +++++++ dialer/mws/metadata.go | 87 +++ dialer/obfs/http/conn.go | 144 +++++ dialer/obfs/http/dialer.go | 68 +++ dialer/obfs/http/metadata.go | 29 + dialer/obfs/tls/conn.go | 266 ++++++++ dialer/obfs/tls/dialer.go | 67 ++ dialer/obfs/tls/metadata.go | 18 + dialer/pht/dialer.go | 114 ++++ dialer/pht/metadata.go | 52 ++ dialer/quic/conn.go | 42 ++ dialer/quic/dialer.go | 128 ++++ dialer/quic/metadata.go | 40 ++ dialer/ssh/conn.go | 31 + dialer/ssh/dialer.go | 165 +++++ dialer/ssh/metadata.go | 56 ++ dialer/sshd/conn.go | 31 + dialer/sshd/dialer.go | 160 +++++ dialer/sshd/metadata.go | 43 ++ dialer/ws/dialer.go | 132 ++++ dialer/ws/metadata.go | 66 ++ go.mod | 71 +++ go.sum | 744 +++++++++++++++++++++++ handler/dns/handler.go | 280 +++++++++ handler/dns/metadata.go | 41 ++ handler/http2/conn.go | 46 ++ handler/http2/handler.go | 343 +++++++++++ handler/http2/metadata.go | 47 ++ handler/redirect/handler.go | 112 ++++ handler/redirect/handler_linux.go | 40 ++ handler/redirect/handler_other.go | 15 + handler/redirect/metadata.go | 18 + handler/relay/bind.go | 193 ++++++ handler/relay/conn.go | 81 +++ handler/relay/connect.go | 91 +++ handler/relay/forward.go | 91 +++ handler/relay/handler.go | 147 +++++ handler/relay/metadata.go | 35 ++ handler/sni/conn.go | 20 + handler/sni/handler.go | 222 +++++++ handler/sni/metadata.go | 20 + handler/ss/handler.go | 119 ++++ handler/ss/metadata.go | 24 + handler/ss/udp/handler.go | 188 ++++++ handler/ss/udp/metadata.go | 32 + handler/sshd/handler.go | 245 ++++++++ handler/sshd/metadata.go | 12 + handler/tap/handler.go | 341 +++++++++++ handler/tap/metadata.go | 24 + handler/tun/handler.go | 391 ++++++++++++ handler/tun/metadata.go | 24 + internal/util/grpc/proto/gost.pb.go | 148 +++++ internal/util/grpc/proto/gost.proto | 10 + internal/util/grpc/proto/gost_grpc.pb.go | 133 ++++ internal/util/grpc/proto/protoc.sh | 3 + internal/util/http2/conn.go | 132 ++++ internal/util/icmp/conn.go | 285 +++++++++ internal/util/kcp/config.go | 115 ++++ internal/util/kcp/kcp.go | 34 ++ internal/util/mux/mux.go | 85 +++ internal/util/pht/client.go | 105 ++++ internal/util/pht/conn.go | 176 ++++++ internal/util/pht/server.go | 344 +++++++++++ internal/util/quic/conn.go | 90 +++ internal/util/relay/conn.go | 172 ++++++ internal/util/ss/conn.go | 96 +++ internal/util/ss/ss.go | 60 ++ internal/util/ssh/conn.go | 48 ++ internal/util/ssh/ssh.go | 75 +++ internal/util/sshd/conn.go | 118 ++++ internal/util/tap/config.go | 9 + internal/util/tap/conn.go | 61 ++ internal/util/tun/config.go | 19 + internal/util/tun/conn.go | 61 ++ internal/util/ws/ws.go | 56 ++ listener/dns/listener.go | 210 +++++++ listener/dns/metadata.go | 41 ++ listener/dns/server.go | 110 ++++ listener/ftcp/conn.go | 124 ++++ listener/ftcp/listener.go | 159 +++++ listener/ftcp/metadata.go | 22 + listener/grpc/listener.go | 100 +++ listener/grpc/metadata.go | 29 + listener/grpc/server.go | 124 ++++ listener/http2/conn.go | 54 ++ listener/http2/h2/conn.go | 89 +++ listener/http2/h2/listener.go | 178 ++++++ listener/http2/h2/metadata.go | 29 + listener/http2/listener.go | 120 ++++ listener/http2/metadata.go | 25 + listener/http3/listener.go | 81 +++ listener/http3/metadata.go | 51 ++ listener/icmp/conn.go | 21 + listener/icmp/listener.go | 147 +++++ listener/icmp/metadata.go | 41 ++ listener/kcp/listener.go | 179 ++++++ listener/kcp/metadata.go | 47 ++ listener/mtls/listener.go | 129 ++++ listener/mtls/metadata.go | 49 ++ listener/mws/listener.go | 194 ++++++ listener/mws/metadata.go | 85 +++ listener/obfs/http/conn.go | 145 +++++ listener/obfs/http/listener.go | 63 ++ listener/obfs/http/metadata.go | 26 + listener/obfs/tls/conn.go | 161 +++++ listener/obfs/tls/listener.go | 61 ++ listener/obfs/tls/metadata.go | 12 + listener/pht/listener.go | 96 +++ listener/pht/metadata.go | 51 ++ listener/quic/conn.go | 21 + listener/quic/listener.go | 151 +++++ listener/quic/metadata.go | 46 ++ listener/redirect/udp/conn.go | 42 ++ listener/redirect/udp/listener.go | 68 +++ listener/redirect/udp/listener_linux.go | 37 ++ listener/redirect/udp/listener_other.go | 16 + listener/redirect/udp/metadata.go | 36 ++ listener/ssh/listener.go | 148 +++++ listener/ssh/metadata.go | 68 +++ listener/sshd/listener.go | 199 ++++++ listener/sshd/metadata.go | 68 +++ listener/tap/listener.go | 96 +++ listener/tap/metadata.go | 44 ++ listener/tap/tap_darwin.go | 13 + listener/tap/tap_linux.go | 69 +++ listener/tap/tap_unix.go | 61 ++ listener/tap/tap_windows.go | 75 +++ listener/tun/listener.go | 96 +++ listener/tun/metadata.go | 63 ++ listener/tun/tun_darwin.go | 57 ++ listener/tun/tun_linux.go | 67 ++ listener/tun/tun_unix.go | 55 ++ listener/tun/tun_windows.go | 77 +++ listener/ws/listener.go | 149 +++++ listener/ws/metadata.go | 66 ++ 175 files changed, 16196 insertions(+) create mode 100644 README.md create mode 100644 connector/http2/conn.go create mode 100644 connector/http2/connector.go create mode 100644 connector/http2/metadata.go create mode 100644 connector/relay/bind.go create mode 100644 connector/relay/conn.go create mode 100644 connector/relay/connector.go create mode 100644 connector/relay/listener.go create mode 100644 connector/relay/metadata.go create mode 100644 connector/sni/conn.go create mode 100644 connector/sni/connector.go create mode 100644 connector/sni/metadata.go create mode 100644 connector/ss/connector.go create mode 100644 connector/ss/metadata.go create mode 100644 connector/ss/udp/connector.go create mode 100644 connector/ss/udp/metadata.go create mode 100644 connector/sshd/connector.go create mode 100644 dialer/ftcp/conn.go create mode 100644 dialer/ftcp/dialer.go create mode 100644 dialer/ftcp/metadata.go create mode 100644 dialer/grpc/conn.go create mode 100644 dialer/grpc/dialer.go create mode 100644 dialer/grpc/metadata.go create mode 100644 dialer/http2/dialer.go create mode 100644 dialer/http2/h2/conn.go create mode 100644 dialer/http2/h2/dialer.go create mode 100644 dialer/http2/h2/metadata.go create mode 100644 dialer/http2/metadata.go create mode 100644 dialer/http3/dialer.go create mode 100644 dialer/http3/metadata.go create mode 100644 dialer/icmp/conn.go create mode 100644 dialer/icmp/dialer.go create mode 100644 dialer/icmp/metadata.go create mode 100644 dialer/kcp/conn.go create mode 100644 dialer/kcp/dialer.go create mode 100644 dialer/kcp/metadata.go create mode 100644 dialer/mtls/conn.go create mode 100644 dialer/mtls/dialer.go create mode 100644 dialer/mtls/metadata.go create mode 100644 dialer/mws/conn.go create mode 100644 dialer/mws/dialer.go create mode 100644 dialer/mws/metadata.go create mode 100644 dialer/obfs/http/conn.go create mode 100644 dialer/obfs/http/dialer.go create mode 100644 dialer/obfs/http/metadata.go create mode 100644 dialer/obfs/tls/conn.go create mode 100644 dialer/obfs/tls/dialer.go create mode 100644 dialer/obfs/tls/metadata.go create mode 100644 dialer/pht/dialer.go create mode 100644 dialer/pht/metadata.go create mode 100644 dialer/quic/conn.go create mode 100644 dialer/quic/dialer.go create mode 100644 dialer/quic/metadata.go create mode 100644 dialer/ssh/conn.go create mode 100644 dialer/ssh/dialer.go create mode 100644 dialer/ssh/metadata.go create mode 100644 dialer/sshd/conn.go create mode 100644 dialer/sshd/dialer.go create mode 100644 dialer/sshd/metadata.go create mode 100644 dialer/ws/dialer.go create mode 100644 dialer/ws/metadata.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handler/dns/handler.go create mode 100644 handler/dns/metadata.go create mode 100644 handler/http2/conn.go create mode 100644 handler/http2/handler.go create mode 100644 handler/http2/metadata.go create mode 100644 handler/redirect/handler.go create mode 100644 handler/redirect/handler_linux.go create mode 100644 handler/redirect/handler_other.go create mode 100644 handler/redirect/metadata.go create mode 100644 handler/relay/bind.go create mode 100644 handler/relay/conn.go create mode 100644 handler/relay/connect.go create mode 100644 handler/relay/forward.go create mode 100644 handler/relay/handler.go create mode 100644 handler/relay/metadata.go create mode 100644 handler/sni/conn.go create mode 100644 handler/sni/handler.go create mode 100644 handler/sni/metadata.go create mode 100644 handler/ss/handler.go create mode 100644 handler/ss/metadata.go create mode 100644 handler/ss/udp/handler.go create mode 100644 handler/ss/udp/metadata.go create mode 100644 handler/sshd/handler.go create mode 100644 handler/sshd/metadata.go create mode 100644 handler/tap/handler.go create mode 100644 handler/tap/metadata.go create mode 100644 handler/tun/handler.go create mode 100644 handler/tun/metadata.go create mode 100644 internal/util/grpc/proto/gost.pb.go create mode 100644 internal/util/grpc/proto/gost.proto create mode 100644 internal/util/grpc/proto/gost_grpc.pb.go create mode 100755 internal/util/grpc/proto/protoc.sh create mode 100644 internal/util/http2/conn.go create mode 100644 internal/util/icmp/conn.go create mode 100644 internal/util/kcp/config.go create mode 100644 internal/util/kcp/kcp.go create mode 100644 internal/util/mux/mux.go create mode 100644 internal/util/pht/client.go create mode 100644 internal/util/pht/conn.go create mode 100644 internal/util/pht/server.go create mode 100644 internal/util/quic/conn.go create mode 100644 internal/util/relay/conn.go create mode 100644 internal/util/ss/conn.go create mode 100644 internal/util/ss/ss.go create mode 100644 internal/util/ssh/conn.go create mode 100644 internal/util/ssh/ssh.go create mode 100644 internal/util/sshd/conn.go create mode 100644 internal/util/tap/config.go create mode 100644 internal/util/tap/conn.go create mode 100644 internal/util/tun/config.go create mode 100644 internal/util/tun/conn.go create mode 100644 internal/util/ws/ws.go create mode 100644 listener/dns/listener.go create mode 100644 listener/dns/metadata.go create mode 100644 listener/dns/server.go create mode 100644 listener/ftcp/conn.go create mode 100644 listener/ftcp/listener.go create mode 100644 listener/ftcp/metadata.go create mode 100644 listener/grpc/listener.go create mode 100644 listener/grpc/metadata.go create mode 100644 listener/grpc/server.go create mode 100644 listener/http2/conn.go create mode 100644 listener/http2/h2/conn.go create mode 100644 listener/http2/h2/listener.go create mode 100644 listener/http2/h2/metadata.go create mode 100644 listener/http2/listener.go create mode 100644 listener/http2/metadata.go create mode 100644 listener/http3/listener.go create mode 100644 listener/http3/metadata.go create mode 100644 listener/icmp/conn.go create mode 100644 listener/icmp/listener.go create mode 100644 listener/icmp/metadata.go create mode 100644 listener/kcp/listener.go create mode 100644 listener/kcp/metadata.go create mode 100644 listener/mtls/listener.go create mode 100644 listener/mtls/metadata.go create mode 100644 listener/mws/listener.go create mode 100644 listener/mws/metadata.go create mode 100644 listener/obfs/http/conn.go create mode 100644 listener/obfs/http/listener.go create mode 100644 listener/obfs/http/metadata.go create mode 100644 listener/obfs/tls/conn.go create mode 100644 listener/obfs/tls/listener.go create mode 100644 listener/obfs/tls/metadata.go create mode 100644 listener/pht/listener.go create mode 100644 listener/pht/metadata.go create mode 100644 listener/quic/conn.go create mode 100644 listener/quic/listener.go create mode 100644 listener/quic/metadata.go create mode 100644 listener/redirect/udp/conn.go create mode 100644 listener/redirect/udp/listener.go create mode 100644 listener/redirect/udp/listener_linux.go create mode 100644 listener/redirect/udp/listener_other.go create mode 100644 listener/redirect/udp/metadata.go create mode 100644 listener/ssh/listener.go create mode 100644 listener/ssh/metadata.go create mode 100644 listener/sshd/listener.go create mode 100644 listener/sshd/metadata.go create mode 100644 listener/tap/listener.go create mode 100644 listener/tap/metadata.go create mode 100644 listener/tap/tap_darwin.go create mode 100644 listener/tap/tap_linux.go create mode 100644 listener/tap/tap_unix.go create mode 100644 listener/tap/tap_windows.go create mode 100644 listener/tun/listener.go create mode 100644 listener/tun/metadata.go create mode 100644 listener/tun/tun_darwin.go create mode 100644 listener/tun/tun_linux.go create mode 100644 listener/tun/tun_unix.go create mode 100644 listener/tun/tun_windows.go create mode 100644 listener/ws/listener.go create mode 100644 listener/ws/metadata.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..8da1f53 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# The extended (may be experimental) features outside the main gost tree. \ No newline at end of file diff --git a/connector/http2/conn.go b/connector/http2/conn.go new file mode 100644 index 0000000..186e775 --- /dev/null +++ b/connector/http2/conn.go @@ -0,0 +1,54 @@ +package http2 + +import ( + "errors" + "io" + "net" + "time" +) + +// 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 r, ok := c.r.(io.Closer); ok { + err = r.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")} +} diff --git a/connector/http2/connector.go b/connector/http2/connector.go new file mode 100644 index 0000000..807869d --- /dev/null +++ b/connector/http2/connector.go @@ -0,0 +1,121 @@ +package http2 + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/http/httputil" + "net/url" + "time" + + "github.com/go-gost/gost/v3/pkg/connector" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + http2_util "github.com/go-gost/x/internal/util/http2" +) + +func init() { + registry.ConnectorRegistry().Register("http2", NewConnector) +} + +type http2Connector struct { + md metadata + options connector.Options +} + +func NewConnector(opts ...connector.Option) connector.Connector { + options := connector.Options{} + for _, opt := range opts { + opt(&options) + } + + return &http2Connector{ + options: options, + } +} + +func (c *http2Connector) Init(md md.Metadata) (err error) { + return c.parseMetadata(md) +} + +func (c *http2Connector) Connect(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) { + log := c.options.Logger.WithFields(map[string]any{ + "local": conn.LocalAddr().String(), + "remote": conn.RemoteAddr().String(), + "network": network, + "address": address, + }) + log.Infof("connect %s/%s", address, network) + + cc, ok := conn.(*http2_util.ClientConn) + if !ok { + err := errors.New("wrong connection type") + log.Error(err) + return nil, err + } + + pr, pw := io.Pipe() + req := &http.Request{ + Method: http.MethodConnect, + URL: &url.URL{Scheme: "https", Host: conn.RemoteAddr().String()}, + Host: address, + ProtoMajor: 2, + ProtoMinor: 0, + Header: make(http.Header), + Body: pr, + // ContentLength: -1, + } + if c.md.UserAgent != "" { + req.Header.Set("User-Agent", c.md.UserAgent) + } + + if user := c.options.Auth; user != nil { + u := user.Username() + p, _ := user.Password() + req.Header.Set("Proxy-Authorization", + "Basic "+base64.StdEncoding.EncodeToString([]byte(u+":"+p))) + } + + if log.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpRequest(req, false) + log.Debug(string(dump)) + } + + if c.md.connectTimeout > 0 { + conn.SetDeadline(time.Now().Add(c.md.connectTimeout)) + defer conn.SetDeadline(time.Time{}) + } + + resp, err := cc.Client().Do(req.WithContext(ctx)) + if err != nil { + log.Error(err) + cc.Close() + return nil, err + } + + if log.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpResponse(resp, false) + log.Debug(string(dump)) + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + err = fmt.Errorf("%s", resp.Status) + log.Error(err) + return nil, err + } + + hc := &http2Conn{ + r: resp.Body, + w: pw, + localAddr: conn.RemoteAddr(), + } + + hc.remoteAddr, _ = net.ResolveTCPAddr(network, address) + + return hc, nil +} diff --git a/connector/http2/metadata.go b/connector/http2/metadata.go new file mode 100644 index 0000000..3937cb6 --- /dev/null +++ b/connector/http2/metadata.go @@ -0,0 +1,31 @@ +package http2 + +import ( + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultUserAgent = "Chrome/78.0.3904.106" +) + +type metadata struct { + connectTimeout time.Duration + UserAgent string +} + +func (c *http2Connector) parseMetadata(md mdata.Metadata) (err error) { + const ( + connectTimeout = "timeout" + userAgent = "userAgent" + ) + + c.md.connectTimeout = mdata.GetDuration(md, connectTimeout) + c.md.UserAgent = mdata.GetString(md, userAgent) + if c.md.UserAgent == "" { + c.md.UserAgent = defaultUserAgent + } + + return +} diff --git a/connector/relay/bind.go b/connector/relay/bind.go new file mode 100644 index 0000000..e74b6f2 --- /dev/null +++ b/connector/relay/bind.go @@ -0,0 +1,133 @@ +package relay + +import ( + "context" + "fmt" + "net" + "strconv" + + "github.com/go-gost/gost/v3/pkg/common/util/udp" + "github.com/go-gost/gost/v3/pkg/connector" + "github.com/go-gost/gost/v3/pkg/logger" + "github.com/go-gost/relay" + "github.com/go-gost/x/internal/util/mux" + relay_util "github.com/go-gost/x/internal/util/relay" +) + +// Bind implements connector.Binder. +func (c *relayConnector) Bind(ctx context.Context, conn net.Conn, network, address string, opts ...connector.BindOption) (net.Listener, error) { + log := c.options.Logger.WithFields(map[string]any{ + "network": network, + "address": address, + }) + log.Infof("bind on %s/%s", address, network) + + options := connector.BindOptions{} + for _, opt := range opts { + opt(&options) + } + + switch network { + case "tcp", "tcp4", "tcp6": + return c.bindTCP(ctx, conn, network, address, log) + case "udp", "udp4", "udp6": + return c.bindUDP(ctx, conn, network, address, &options, log) + default: + err := fmt.Errorf("network %s is unsupported", network) + log.Error(err) + return nil, err + } +} + +func (c *relayConnector) bindTCP(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) (net.Listener, error) { + laddr, err := c.bind(conn, relay.BIND, network, address) + if err != nil { + return nil, err + } + log.Debugf("bind on %s/%s OK", laddr, laddr.Network()) + + session, err := mux.ServerSession(conn) + if err != nil { + return nil, err + } + + return &tcpListener{ + addr: laddr, + session: session, + logger: log, + }, nil +} + +func (c *relayConnector) bindUDP(ctx context.Context, conn net.Conn, network, address string, opts *connector.BindOptions, log logger.Logger) (net.Listener, error) { + laddr, err := c.bind(conn, relay.FUDP|relay.BIND, network, address) + if err != nil { + return nil, err + } + log.Debugf("bind on %s/%s OK", laddr, laddr.Network()) + + ln := udp.NewListener( + relay_util.UDPTunClientPacketConn(conn), + laddr, + opts.Backlog, + opts.UDPDataQueueSize, opts.UDPDataBufferSize, + opts.UDPConnTTL, + log) + + return ln, nil +} + +func (c *relayConnector) bind(conn net.Conn, cmd uint8, network, address string) (net.Addr, error) { + req := relay.Request{ + Version: relay.Version1, + Flags: cmd, + } + + if c.options.Auth != nil { + pwd, _ := c.options.Auth.Password() + req.Features = append(req.Features, &relay.UserAuthFeature{ + Username: c.options.Auth.Username(), + Password: pwd, + }) + } + fa := &relay.AddrFeature{} + fa.ParseFrom(address) + req.Features = append(req.Features, fa) + if _, err := req.WriteTo(conn); err != nil { + return nil, err + } + + // first reply, bind status + resp := relay.Response{} + if _, err := resp.ReadFrom(conn); err != nil { + return nil, err + } + + if resp.Status != relay.StatusOK { + return nil, fmt.Errorf("bind on %s/%s failed", address, network) + } + + var addr string + for _, f := range resp.Features { + if f.Type() == relay.FeatureAddr { + if fa, ok := f.(*relay.AddrFeature); ok { + addr = net.JoinHostPort(fa.Host, strconv.Itoa(int(fa.Port))) + } + } + } + + var baddr net.Addr + var err error + switch network { + case "tcp", "tcp4", "tcp6": + baddr, err = net.ResolveTCPAddr(network, addr) + case "udp", "udp4", "udp6": + baddr, err = net.ResolveUDPAddr(network, addr) + default: + err = fmt.Errorf("unknown network %s", network) + } + if err != nil { + return nil, err + } + + return baddr, nil +} diff --git a/connector/relay/conn.go b/connector/relay/conn.go new file mode 100644 index 0000000..11b29f9 --- /dev/null +++ b/connector/relay/conn.go @@ -0,0 +1,132 @@ +package relay + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "math" + "net" + "sync" + + "github.com/go-gost/relay" +) + +type tcpConn struct { + net.Conn + wbuf bytes.Buffer + once sync.Once +} + +func (c *tcpConn) Read(b []byte) (n int, err error) { + c.once.Do(func() { + err = readResponse(c.Conn) + }) + + if err != nil { + return + } + return c.Conn.Read(b) +} + +func (c *tcpConn) Write(b []byte) (n int, err error) { + n = len(b) // force byte length consistent + if c.wbuf.Len() > 0 { + c.wbuf.Write(b) // append the data to the cached header + _, err = c.Conn.Write(c.wbuf.Bytes()) + c.wbuf.Reset() + return + } + _, err = c.Conn.Write(b) + return +} + +type udpConn struct { + net.Conn + wbuf bytes.Buffer + once sync.Once +} + +func (c *udpConn) Read(b []byte) (n int, err error) { + c.once.Do(func() { + err = readResponse(c.Conn) + }) + if err != nil { + return + } + + var bb [2]byte + _, err = io.ReadFull(c.Conn, bb[:]) + if err != nil { + return + } + + dlen := int(binary.BigEndian.Uint16(bb[:])) + if len(b) >= dlen { + return io.ReadFull(c.Conn, b[:dlen]) + } + buf := make([]byte, dlen) + _, err = io.ReadFull(c.Conn, buf) + n = copy(b, buf) + + return +} + +func (c *udpConn) Write(b []byte) (n int, err error) { + if len(b) > math.MaxUint16 { + err = errors.New("write: data maximum exceeded") + return + } + + n = len(b) + if c.wbuf.Len() > 0 { + var bb [2]byte + binary.BigEndian.PutUint16(bb[:], uint16(len(b))) + c.wbuf.Write(bb[:]) + c.wbuf.Write(b) // append the data to the cached header + _, err = c.wbuf.WriteTo(c.Conn) + return + } + + var bb [2]byte + binary.BigEndian.PutUint16(bb[:], uint16(len(b))) + _, err = c.Conn.Write(bb[:]) + if err != nil { + return + } + return c.Conn.Write(b) +} + +func readResponse(r io.Reader) (err error) { + resp := relay.Response{} + _, err = resp.ReadFrom(r) + if err != nil { + return + } + + if resp.Version != relay.Version1 { + err = relay.ErrBadVersion + return + } + + if resp.Status != relay.StatusOK { + err = fmt.Errorf("status %d", resp.Status) + return + } + return nil +} + +type bindConn struct { + net.Conn + localAddr net.Addr + remoteAddr net.Addr +} + +func (c *bindConn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *bindConn) RemoteAddr() net.Addr { + return c.remoteAddr +} diff --git a/connector/relay/connector.go b/connector/relay/connector.go new file mode 100644 index 0000000..ee7de80 --- /dev/null +++ b/connector/relay/connector.go @@ -0,0 +1,127 @@ +package relay + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/go-gost/gost/v3/pkg/connector" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "github.com/go-gost/relay" + relay_util "github.com/go-gost/x/internal/util/relay" +) + +func init() { + registry.ConnectorRegistry().Register("relay", NewConnector) +} + +type relayConnector struct { + md metadata + options connector.Options +} + +func NewConnector(opts ...connector.Option) connector.Connector { + options := connector.Options{} + for _, opt := range opts { + opt(&options) + } + + return &relayConnector{ + options: options, + } +} + +func (c *relayConnector) Init(md md.Metadata) (err error) { + return c.parseMetadata(md) +} + +func (c *relayConnector) Connect(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) { + log := c.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + "network": network, + "address": address, + }) + log.Infof("connect %s/%s", address, network) + + if c.md.connectTimeout > 0 { + conn.SetDeadline(time.Now().Add(c.md.connectTimeout)) + defer conn.SetDeadline(time.Time{}) + } + + req := relay.Request{ + Version: relay.Version1, + Flags: relay.CONNECT, + } + if network == "udp" || network == "udp4" || network == "udp6" { + req.Flags |= relay.FUDP + + // UDP association + if address == "" { + baddr, err := c.bind(conn, relay.FUDP|relay.BIND, network, address) + if err != nil { + return nil, err + } + log.Debugf("associate on %s OK", baddr) + + return relay_util.UDPTunClientConn(conn, nil), nil + } + } + + if c.options.Auth != nil { + pwd, _ := c.options.Auth.Password() + req.Features = append(req.Features, &relay.UserAuthFeature{ + Username: c.options.Auth.Username(), + Password: pwd, + }) + } + + if address != "" { + af := &relay.AddrFeature{} + if err := af.ParseFrom(address); err != nil { + return nil, err + } + + // forward mode if port is 0. + if af.Port > 0 { + req.Features = append(req.Features, af) + } + } + + if c.md.noDelay { + if _, err := req.WriteTo(conn); err != nil { + return nil, err + } + } + + switch network { + case "tcp", "tcp4", "tcp6": + cc := &tcpConn{ + Conn: conn, + } + if !c.md.noDelay { + if _, err := req.WriteTo(&cc.wbuf); err != nil { + return nil, err + } + } + conn = cc + case "udp", "udp4", "udp6": + cc := &udpConn{ + Conn: conn, + } + if !c.md.noDelay { + if _, err := req.WriteTo(&cc.wbuf); err != nil { + return nil, err + } + } + conn = cc + default: + err := fmt.Errorf("network %s is unsupported", network) + log.Error(err) + return nil, err + } + + return conn, nil +} diff --git a/connector/relay/listener.go b/connector/relay/listener.go new file mode 100644 index 0000000..199d445 --- /dev/null +++ b/connector/relay/listener.go @@ -0,0 +1,73 @@ +package relay + +import ( + "fmt" + "net" + "strconv" + + "github.com/go-gost/gost/v3/pkg/logger" + "github.com/go-gost/relay" + "github.com/go-gost/x/internal/util/mux" +) + +type tcpListener struct { + addr net.Addr + session *mux.Session + logger logger.Logger +} + +func (p *tcpListener) Accept() (net.Conn, error) { + cc, err := p.session.Accept() + if err != nil { + return nil, err + } + + conn, err := p.getPeerConn(cc) + if err != nil { + cc.Close() + return nil, err + } + + return conn, nil +} + +func (p *tcpListener) getPeerConn(conn net.Conn) (net.Conn, error) { + // second reply, peer connected + resp := relay.Response{} + if _, err := resp.ReadFrom(conn); err != nil { + return nil, err + } + + if resp.Status != relay.StatusOK { + err := fmt.Errorf("peer connect failed") + return nil, err + } + + var address string + for _, f := range resp.Features { + if f.Type() == relay.FeatureAddr { + if fa, ok := f.(*relay.AddrFeature); ok { + address = net.JoinHostPort(fa.Host, strconv.Itoa(int(fa.Port))) + } + } + } + + raddr, err := net.ResolveTCPAddr("tcp", address) + if err != nil { + return nil, err + } + + return &bindConn{ + Conn: conn, + localAddr: p.addr, + remoteAddr: raddr, + }, nil +} + +func (p *tcpListener) Addr() net.Addr { + return p.addr +} + +func (p *tcpListener) Close() error { + return p.session.Close() +} diff --git a/connector/relay/metadata.go b/connector/relay/metadata.go new file mode 100644 index 0000000..ac93fbb --- /dev/null +++ b/connector/relay/metadata.go @@ -0,0 +1,24 @@ +package relay + +import ( + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + connectTimeout time.Duration + noDelay bool +} + +func (c *relayConnector) parseMetadata(md mdata.Metadata) (err error) { + const ( + connectTimeout = "connectTimeout" + noDelay = "nodelay" + ) + + c.md.connectTimeout = mdata.GetDuration(md, connectTimeout) + c.md.noDelay = mdata.GetBool(md, noDelay) + + return +} diff --git a/connector/sni/conn.go b/connector/sni/conn.go new file mode 100644 index 0000000..36471cf --- /dev/null +++ b/connector/sni/conn.go @@ -0,0 +1,128 @@ +package sni + +import ( + "bufio" + "bytes" + "encoding/base64" + "encoding/binary" + "hash/crc32" + "io" + "net" + "strings" + + dissector "github.com/go-gost/tls-dissector" +) + +type sniClientConn struct { + host string + obfuscated bool + net.Conn +} + +func (c *sniClientConn) Write(p []byte) (int, error) { + b, err := c.obfuscate(p) + if err != nil { + return 0, err + } + if _, err = c.Conn.Write(b); err != nil { + return 0, err + } + return len(p), nil +} + +func (c *sniClientConn) obfuscate(p []byte) ([]byte, error) { + if c.host == "" { + return p, nil + } + + if c.obfuscated { + return p, nil + } + + if p[0] == dissector.Handshake { + b, err := readClientHelloRecord(bytes.NewReader(p), c.host) + if err != nil { + return nil, err + } + c.obfuscated = true + return b, nil + } + + buf := &bytes.Buffer{} + br := bufio.NewReader(bytes.NewReader(p)) + for { + s, err := br.ReadString('\n') + if err != nil { + if err != io.EOF { + return nil, err + } + if s != "" { + buf.Write([]byte(s)) + } + break + } + + // end of HTTP header + if s == "\r\n" { + buf.Write([]byte(s)) + // drain the remain bytes. + io.Copy(buf, br) + break + } + + if strings.HasPrefix(s, "Host") { + s = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(s, "Host:"), "\r\n")) + host := encodeServerName(s) + buf.WriteString("Host: " + c.host + "\r\n") + buf.WriteString("Gost-Target: " + host + "\r\n") + // drain the remain bytes. + io.Copy(buf, br) + break + } + buf.Write([]byte(s)) + } + c.obfuscated = true + return buf.Bytes(), nil +} + +func readClientHelloRecord(r io.Reader, host string) ([]byte, error) { + record, err := dissector.ReadRecord(r) + if err != nil { + return nil, err + } + clientHello := dissector.ClientHelloMsg{} + if err := clientHello.Decode(record.Opaque); err != nil { + return nil, err + } + + for _, ext := range clientHello.Extensions { + if ext.Type() == dissector.ExtServerName { + snExtension := ext.(*dissector.ServerNameExtension) + if host != "" { + e, _ := dissector.NewExtension(0xFFFE, []byte(encodeServerName(snExtension.Name))) + clientHello.Extensions = append(clientHello.Extensions, e) + snExtension.Name = host + } + + break + } + } + record.Opaque, err = clientHello.Encode() + if err != nil { + return nil, err + } + + buf := &bytes.Buffer{} + if _, err := record.WriteTo(buf); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func encodeServerName(name string) string { + buf := &bytes.Buffer{} + binary.Write(buf, binary.BigEndian, crc32.ChecksumIEEE([]byte(name))) + buf.WriteString(base64.RawURLEncoding.EncodeToString([]byte(name))) + return base64.RawURLEncoding.EncodeToString(buf.Bytes()) +} diff --git a/connector/sni/connector.go b/connector/sni/connector.go new file mode 100644 index 0000000..33e0ba2 --- /dev/null +++ b/connector/sni/connector.go @@ -0,0 +1,46 @@ +package sni + +import ( + "context" + "net" + + "github.com/go-gost/gost/v3/pkg/connector" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" +) + +func init() { + registry.ConnectorRegistry().Register("sni", NewConnector) +} + +type sniConnector struct { + md metadata + options connector.Options +} + +func NewConnector(opts ...connector.Option) connector.Connector { + options := connector.Options{} + for _, opt := range opts { + opt(&options) + } + + return &sniConnector{ + options: options, + } +} + +func (c *sniConnector) Init(md md.Metadata) (err error) { + return c.parseMetadata(md) +} + +func (c *sniConnector) Connect(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) { + log := c.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + "network": network, + "address": address, + }) + log.Infof("connect %s/%s", address, network) + + return &sniClientConn{Conn: conn, host: c.md.host}, nil +} diff --git a/connector/sni/metadata.go b/connector/sni/metadata.go new file mode 100644 index 0000000..5d26c1f --- /dev/null +++ b/connector/sni/metadata.go @@ -0,0 +1,24 @@ +package sni + +import ( + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + host string + connectTimeout time.Duration +} + +func (c *sniConnector) parseMetadata(md mdata.Metadata) (err error) { + const ( + host = "host" + connectTimeout = "timeout" + ) + + c.md.host = mdata.GetString(md, host) + c.md.connectTimeout = mdata.GetDuration(md, connectTimeout) + + return +} diff --git a/connector/ss/connector.go b/connector/ss/connector.go new file mode 100644 index 0000000..b0472fa --- /dev/null +++ b/connector/ss/connector.go @@ -0,0 +1,111 @@ +package ss + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/go-gost/gosocks5" + "github.com/go-gost/gost/v3/pkg/common/bufpool" + "github.com/go-gost/gost/v3/pkg/connector" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "github.com/go-gost/x/internal/util/ss" + "github.com/shadowsocks/go-shadowsocks2/core" +) + +func init() { + registry.ConnectorRegistry().Register("ss", NewConnector) +} + +type ssConnector struct { + cipher core.Cipher + md metadata + options connector.Options +} + +func NewConnector(opts ...connector.Option) connector.Connector { + options := connector.Options{} + for _, opt := range opts { + opt(&options) + } + + return &ssConnector{ + options: options, + } +} + +func (c *ssConnector) Init(md md.Metadata) (err error) { + if err = c.parseMetadata(md); err != nil { + return + } + + if c.options.Auth != nil { + method := c.options.Auth.Username() + password, _ := c.options.Auth.Password() + c.cipher, err = ss.ShadowCipher(method, password, c.md.key) + } + + return +} + +func (c *ssConnector) Connect(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) { + log := c.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + "network": network, + "address": address, + }) + log.Infof("connect %s/%s", address, network) + + switch network { + case "tcp", "tcp4", "tcp6": + if _, ok := conn.(net.PacketConn); ok { + err := fmt.Errorf("tcp over udp is unsupported") + log.Error(err) + return nil, err + } + default: + err := fmt.Errorf("network %s is unsupported", network) + log.Error(err) + return nil, err + } + + addr := gosocks5.Addr{} + if err := addr.ParseFrom(address); err != nil { + log.Error(err) + return nil, err + } + rawaddr := bufpool.Get(512) + defer bufpool.Put(rawaddr) + + n, err := addr.Encode(*rawaddr) + if err != nil { + log.Error("encoding addr: ", err) + return nil, err + } + + if c.md.connectTimeout > 0 { + conn.SetDeadline(time.Now().Add(c.md.connectTimeout)) + defer conn.SetDeadline(time.Time{}) + } + + if c.cipher != nil { + conn = c.cipher.StreamConn(conn) + } + + var sc net.Conn + if c.md.noDelay { + sc = ss.ShadowConn(conn, nil) + // write the addr at once. + if _, err := sc.Write((*rawaddr)[:n]); err != nil { + return nil, err + } + } else { + // cache the header + sc = ss.ShadowConn(conn, (*rawaddr)[:n]) + } + + return sc, nil +} diff --git a/connector/ss/metadata.go b/connector/ss/metadata.go new file mode 100644 index 0000000..1191106 --- /dev/null +++ b/connector/ss/metadata.go @@ -0,0 +1,27 @@ +package ss + +import ( + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + key string + connectTimeout time.Duration + noDelay bool +} + +func (c *ssConnector) parseMetadata(md mdata.Metadata) (err error) { + const ( + key = "key" + connectTimeout = "timeout" + noDelay = "nodelay" + ) + + c.md.key = mdata.GetString(md, key) + c.md.connectTimeout = mdata.GetDuration(md, connectTimeout) + c.md.noDelay = mdata.GetBool(md, noDelay) + + return +} diff --git a/connector/ss/udp/connector.go b/connector/ss/udp/connector.go new file mode 100644 index 0000000..db307f7 --- /dev/null +++ b/connector/ss/udp/connector.go @@ -0,0 +1,95 @@ +package ss + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/go-gost/gost/v3/pkg/connector" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "github.com/go-gost/x/internal/util/relay" + "github.com/go-gost/x/internal/util/ss" + "github.com/shadowsocks/go-shadowsocks2/core" +) + +func init() { + registry.ConnectorRegistry().Register("ssu", NewConnector) +} + +type ssuConnector struct { + cipher core.Cipher + md metadata + options connector.Options +} + +func NewConnector(opts ...connector.Option) connector.Connector { + options := connector.Options{} + for _, opt := range opts { + opt(&options) + } + + return &ssuConnector{ + options: options, + } +} + +func (c *ssuConnector) Init(md md.Metadata) (err error) { + if err = c.parseMetadata(md); err != nil { + return + } + + if c.options.Auth != nil { + method := c.options.Auth.Username() + password, _ := c.options.Auth.Password() + c.cipher, err = ss.ShadowCipher(method, password, c.md.key) + } + + return +} + +func (c *ssuConnector) Connect(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) { + log := c.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + "network": network, + "address": address, + }) + log.Infof("connect %s/%s", address, network) + + switch network { + case "udp", "udp4", "udp6": + default: + err := fmt.Errorf("network %s is unsupported", network) + log.Error(err) + return nil, err + } + + if c.md.connectTimeout > 0 { + conn.SetDeadline(time.Now().Add(c.md.connectTimeout)) + defer conn.SetDeadline(time.Time{}) + } + + taddr, _ := net.ResolveUDPAddr(network, address) + if taddr == nil { + taddr = &net.UDPAddr{} + } + + pc, ok := conn.(net.PacketConn) + if ok { + if c.cipher != nil { + pc = c.cipher.PacketConn(pc) + } + + // standard UDP relay + return ss.UDPClientConn(pc, conn.RemoteAddr(), taddr, c.md.bufferSize), nil + } + + if c.cipher != nil { + conn = ss.ShadowConn(c.cipher.StreamConn(conn), nil) + } + + // UDP over TCP + return relay.UDPTunClientConn(conn, taddr), nil +} diff --git a/connector/ss/udp/metadata.go b/connector/ss/udp/metadata.go new file mode 100644 index 0000000..8ac7604 --- /dev/null +++ b/connector/ss/udp/metadata.go @@ -0,0 +1,33 @@ +package ss + +import ( + "math" + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + key string + connectTimeout time.Duration + bufferSize int +} + +func (c *ssuConnector) parseMetadata(md mdata.Metadata) (err error) { + const ( + key = "key" + connectTimeout = "timeout" + bufferSize = "bufferSize" // udp buffer size + ) + + c.md.key = mdata.GetString(md, key) + c.md.connectTimeout = mdata.GetDuration(md, connectTimeout) + + if bs := mdata.GetInt(md, bufferSize); bs > 0 { + c.md.bufferSize = int(math.Min(math.Max(float64(bs), 512), 64*1024)) + } else { + c.md.bufferSize = 1024 + } + + return +} diff --git a/connector/sshd/connector.go b/connector/sshd/connector.go new file mode 100644 index 0000000..28f55aa --- /dev/null +++ b/connector/sshd/connector.go @@ -0,0 +1,80 @@ +package sshd + +import ( + "context" + "errors" + "net" + + "github.com/go-gost/gost/v3/pkg/connector" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + ssh_util "github.com/go-gost/x/internal/util/ssh" +) + +func init() { + registry.ConnectorRegistry().Register("sshd", NewConnector) +} + +type sshdConnector struct { + options connector.Options +} + +func NewConnector(opts ...connector.Option) connector.Connector { + options := connector.Options{} + for _, opt := range opts { + opt(&options) + } + + return &sshdConnector{ + options: options, + } +} + +func (c *sshdConnector) Init(md md.Metadata) (err error) { + return nil +} + +func (c *sshdConnector) Connect(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) { + log := c.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + "network": network, + "address": address, + }) + log.Infof("connect %s/%s", address, network) + + cc, ok := conn.(*ssh_util.ClientConn) + if !ok { + return nil, errors.New("ssh: invalid connection") + } + + conn, err := cc.Client().Dial(network, address) + if err != nil { + log.Error(err) + return nil, err + } + + return conn, nil +} + +// Bind implements connector.Binder. +func (c *sshdConnector) Bind(ctx context.Context, conn net.Conn, network, address string, opts ...connector.BindOption) (net.Listener, error) { + log := c.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + "network": network, + "address": address, + }) + log.Infof("bind on %s/%s", address, network) + + cc, ok := conn.(*ssh_util.ClientConn) + if !ok { + return nil, errors.New("ssh: invalid connection") + } + + if host, port, _ := net.SplitHostPort(address); host == "" { + address = net.JoinHostPort("0.0.0.0", port) + } + + return cc.Client().Listen(network, address) +} diff --git a/dialer/ftcp/conn.go b/dialer/ftcp/conn.go new file mode 100644 index 0000000..b0d7e26 --- /dev/null +++ b/dialer/ftcp/conn.go @@ -0,0 +1,21 @@ +package ftcp + +import "net" + +type fakeTCPConn struct { + raddr net.Addr + net.PacketConn +} + +func (c *fakeTCPConn) Read(b []byte) (n int, err error) { + n, _, err = c.ReadFrom(b) + return +} + +func (c *fakeTCPConn) Write(b []byte) (n int, err error) { + return c.WriteTo(b, c.raddr) +} + +func (c *fakeTCPConn) RemoteAddr() net.Addr { + return c.raddr +} diff --git a/dialer/ftcp/dialer.go b/dialer/ftcp/dialer.go new file mode 100644 index 0000000..c4bde48 --- /dev/null +++ b/dialer/ftcp/dialer.go @@ -0,0 +1,51 @@ +package ftcp + +import ( + "context" + "net" + + "github.com/go-gost/gost/v3/pkg/dialer" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "github.com/xtaci/tcpraw" +) + +func init() { + registry.DialerRegistry().Register("ftcp", NewDialer) +} + +type ftcpDialer struct { + md metadata + logger logger.Logger +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := &dialer.Options{} + for _, opt := range opts { + opt(options) + } + + return &ftcpDialer{ + logger: options.Logger, + } +} + +func (d *ftcpDialer) Init(md md.Metadata) (err error) { + return d.parseMetadata(md) +} + +func (d *ftcpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (conn net.Conn, err error) { + raddr, er := net.ResolveTCPAddr("tcp", addr) + if er != nil { + return nil, er + } + c, err := tcpraw.Dial("tcp", addr) + if err != nil { + return + } + return &fakeTCPConn{ + raddr: raddr, + PacketConn: c, + }, nil +} diff --git a/dialer/ftcp/metadata.go b/dialer/ftcp/metadata.go new file mode 100644 index 0000000..d4157e8 --- /dev/null +++ b/dialer/ftcp/metadata.go @@ -0,0 +1,23 @@ +package ftcp + +import ( + "time" + + md "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + dialTimeout = "dialTimeout" +) + +const ( + defaultDialTimeout = 5 * time.Second +) + +type metadata struct { + dialTimeout time.Duration +} + +func (d *ftcpDialer) parseMetadata(md md.Metadata) (err error) { + return +} diff --git a/dialer/grpc/conn.go b/dialer/grpc/conn.go new file mode 100644 index 0000000..90ed28c --- /dev/null +++ b/dialer/grpc/conn.go @@ -0,0 +1,92 @@ +package grpc + +import ( + "errors" + "io" + "net" + "time" + + pb "github.com/go-gost/x/internal/util/grpc/proto" +) + +type conn struct { + c pb.GostTunel_TunnelClient + rb []byte + localAddr net.Addr + remoteAddr net.Addr + closed chan struct{} +} + +func (c *conn) Read(b []byte) (n int, err error) { + select { + case <-c.c.Context().Done(): + err = c.c.Context().Err() + return + case <-c.closed: + err = io.ErrClosedPipe + return + default: + } + + if len(c.rb) == 0 { + chunk, err := c.c.Recv() + if err != nil { + return 0, err + } + c.rb = chunk.Data + } + + n = copy(b, c.rb) + c.rb = c.rb[n:] + return +} + +func (c *conn) Write(b []byte) (n int, err error) { + select { + case <-c.c.Context().Done(): + err = c.c.Context().Err() + return + case <-c.closed: + err = io.ErrClosedPipe + return + default: + } + + if err = c.c.Send(&pb.Chunk{ + Data: b, + }); err != nil { + return + } + n = len(b) + return +} + +func (c *conn) Close() error { + select { + case <-c.closed: + default: + close(c.closed) + } + + return nil +} + +func (c *conn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *conn) RemoteAddr() net.Addr { + return c.remoteAddr +} + +func (c *conn) SetDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "grpc", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *conn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "grpc", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *conn) SetWriteDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "grpc", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} diff --git a/dialer/grpc/dialer.go b/dialer/grpc/dialer.go new file mode 100644 index 0000000..4b37b7f --- /dev/null +++ b/dialer/grpc/dialer.go @@ -0,0 +1,113 @@ +package grpc + +import ( + "context" + "net" + "sync" + "time" + + "github.com/go-gost/gost/v3/pkg/dialer" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + pb "github.com/go-gost/x/internal/util/grpc/proto" + "google.golang.org/grpc" + "google.golang.org/grpc/backoff" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" +) + +func init() { + registry.DialerRegistry().Register("grpc", NewDialer) +} + +type grpcDialer struct { + clients map[string]pb.GostTunelClient + clientMutex sync.Mutex + md metadata + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &grpcDialer{ + clients: make(map[string]pb.GostTunelClient), + options: options, + } +} + +func (d *grpcDialer) Init(md md.Metadata) (err error) { + return d.parseMetadata(md) +} + +// Multiplex implements dialer.Multiplexer interface. +func (d *grpcDialer) Multiplex() bool { + return true +} + +func (d *grpcDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) { + remoteAddr, err := net.ResolveTCPAddr("tcp", addr) + if err != nil { + return nil, err + } + + d.clientMutex.Lock() + defer d.clientMutex.Unlock() + + client, ok := d.clients[addr] + if !ok { + var options dialer.DialOptions + for _, opt := range opts { + opt(&options) + } + + host := d.md.host + if host == "" { + host = options.Host + } + if h, _, _ := net.SplitHostPort(host); h != "" { + host = h + } + + grpcOpts := []grpc.DialOption{ + // grpc.WithBlock(), + grpc.WithContextDialer(func(c context.Context, s string) (net.Conn, error) { + return options.NetDialer.Dial(c, "tcp", s) + }), + grpc.WithAuthority(host), + grpc.WithConnectParams(grpc.ConnectParams{ + Backoff: backoff.DefaultConfig, + MinConnectTimeout: 10 * time.Second, + }), + grpc.FailOnNonTempDialError(true), + } + if !d.md.insecure { + grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(credentials.NewTLS(d.options.TLSConfig))) + } else { + grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + + cc, err := grpc.DialContext(ctx, addr, grpcOpts...) + if err != nil { + d.options.Logger.Error(err) + return nil, err + } + client = pb.NewGostTunelClient(cc) + d.clients[addr] = client + } + + cli, err := client.Tunnel(ctx) + if err != nil { + return nil, err + } + + return &conn{ + c: cli, + localAddr: &net.TCPAddr{}, + remoteAddr: remoteAddr, + closed: make(chan struct{}), + }, nil +} diff --git a/dialer/grpc/metadata.go b/dialer/grpc/metadata.go new file mode 100644 index 0000000..43dee9a --- /dev/null +++ b/dialer/grpc/metadata.go @@ -0,0 +1,22 @@ +package grpc + +import ( + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + insecure bool + host string +} + +func (d *grpcDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + insecure = "grpcInsecure" + host = "host" + ) + + d.md.insecure = mdata.GetBool(md, insecure) + d.md.host = mdata.GetString(md, host) + + return +} diff --git a/dialer/http2/dialer.go b/dialer/http2/dialer.go new file mode 100644 index 0000000..61ac5bd --- /dev/null +++ b/dialer/http2/dialer.go @@ -0,0 +1,101 @@ +package http2 + +import ( + "context" + "net" + "net/http" + "sync" + "time" + + net_dialer "github.com/go-gost/gost/v3/pkg/common/net/dialer" + "github.com/go-gost/gost/v3/pkg/dialer" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + http2_util "github.com/go-gost/x/internal/util/http2" +) + +func init() { + registry.DialerRegistry().Register("http2", NewDialer) +} + +type http2Dialer struct { + clients map[string]*http.Client + clientMutex sync.Mutex + logger logger.Logger + md metadata + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &http2Dialer{ + clients: make(map[string]*http.Client), + logger: options.Logger, + options: options, + } +} + +func (d *http2Dialer) Init(md md.Metadata) (err error) { + if err = d.parseMetadata(md); err != nil { + return + } + + return nil +} + +// Multiplex implements dialer.Multiplexer interface. +func (d *http2Dialer) Multiplex() bool { + return true +} + +func (d *http2Dialer) Dial(ctx context.Context, address string, opts ...dialer.DialOption) (net.Conn, error) { + raddr, err := net.ResolveTCPAddr("tcp", address) + if err != nil { + d.logger.Error(err) + return nil, err + } + + d.clientMutex.Lock() + defer d.clientMutex.Unlock() + + client, ok := d.clients[address] + if !ok { + options := dialer.DialOptions{} + for _, opt := range opts { + opt(&options) + } + + client = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: d.options.TLSConfig, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + netd := options.NetDialer + if netd == nil { + netd = net_dialer.DefaultNetDialer + } + return netd.Dial(ctx, network, addr) + }, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + } + d.clients[address] = client + } + + return http2_util.NewClientConn( + &net.TCPAddr{}, raddr, + client, + func() { + d.clientMutex.Lock() + defer d.clientMutex.Unlock() + delete(d.clients, address) + }), nil +} diff --git a/dialer/http2/h2/conn.go b/dialer/http2/h2/conn.go new file mode 100644 index 0000000..6b786b4 --- /dev/null +++ b/dialer/http2/h2/conn.go @@ -0,0 +1,54 @@ +package h2 + +import ( + "errors" + "io" + "net" + "time" +) + +// 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 r, ok := c.r.(io.Closer); ok { + err = r.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: "h2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *http2Conn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "h2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *http2Conn) SetWriteDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "h2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} diff --git a/dialer/http2/h2/dialer.go b/dialer/http2/h2/dialer.go new file mode 100644 index 0000000..d046dd6 --- /dev/null +++ b/dialer/http2/h2/dialer.go @@ -0,0 +1,165 @@ +package h2 + +import ( + "context" + "crypto/tls" + "errors" + "io" + "net" + "net/http" + "net/http/httputil" + "net/url" + "sync" + "time" + + "github.com/go-gost/gost/v3/pkg/dialer" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "golang.org/x/net/http2" +) + +func init() { + registry.DialerRegistry().Register("h2", NewTLSDialer) + registry.DialerRegistry().Register("h2c", NewDialer) +} + +type h2Dialer struct { + clients map[string]*http.Client + clientMutex sync.Mutex + h2c bool + logger logger.Logger + md metadata + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &h2Dialer{ + h2c: true, + clients: make(map[string]*http.Client), + logger: options.Logger, + options: options, + } +} + +func NewTLSDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &h2Dialer{ + clients: make(map[string]*http.Client), + logger: options.Logger, + options: options, + } +} + +func (d *h2Dialer) Init(md md.Metadata) (err error) { + if err = d.parseMetadata(md); err != nil { + return + } + + return nil +} + +// Multiplex implements dialer.Multiplexer interface. +func (d *h2Dialer) Multiplex() bool { + return true +} + +func (d *h2Dialer) Dial(ctx context.Context, address string, opts ...dialer.DialOption) (net.Conn, error) { + raddr, err := net.ResolveTCPAddr("tcp", address) + if err != nil { + d.logger.Error(err) + return nil, err + } + + d.clientMutex.Lock() + + client, ok := d.clients[address] + if !ok { + options := &dialer.DialOptions{} + for _, opt := range opts { + opt(options) + } + + client = &http.Client{} + if d.h2c { + client.Transport = &http2.Transport{ + DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { + return options.NetDialer.Dial(ctx, network, addr) + }, + } + } else { + client.Transport = &http.Transport{ + TLSClientConfig: d.options.TLSConfig, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return options.NetDialer.Dial(ctx, network, addr) + }, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + } + + d.clients[address] = client + } + d.clientMutex.Unlock() + + host := d.md.host + if host == "" { + host = address + } + + pr, pw := io.Pipe() + req := &http.Request{ + Method: http.MethodConnect, + URL: &url.URL{Scheme: "https", Host: host}, + Header: make(http.Header), + ProtoMajor: 2, + ProtoMinor: 0, + Body: pr, + Host: host, + // ContentLength: -1, + } + if d.md.path != "" { + req.Method = http.MethodGet + req.URL.Path = d.md.path + } + + if d.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpRequest(req, false) + d.logger.Debug(string(dump)) + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + if d.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpResponse(resp, false) + d.logger.Debug(string(dump)) + } + + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, errors.New(resp.Status) + } + + conn := &http2Conn{ + r: resp.Body, + w: pw, + remoteAddr: raddr, + localAddr: &net.TCPAddr{IP: net.IPv4zero, Port: 0}, + } + return conn, nil +} diff --git a/dialer/http2/h2/metadata.go b/dialer/http2/h2/metadata.go new file mode 100644 index 0000000..c757797 --- /dev/null +++ b/dialer/http2/h2/metadata.go @@ -0,0 +1,22 @@ +package h2 + +import ( + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + host string + path string +} + +func (d *h2Dialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + host = "host" + path = "path" + ) + + d.md.host = mdata.GetString(md, host) + d.md.path = mdata.GetString(md, path) + + return +} diff --git a/dialer/http2/metadata.go b/dialer/http2/metadata.go new file mode 100644 index 0000000..675e175 --- /dev/null +++ b/dialer/http2/metadata.go @@ -0,0 +1,12 @@ +package http2 + +import ( + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { +} + +func (d *http2Dialer) parseMetadata(md mdata.Metadata) (err error) { + return +} diff --git a/dialer/http3/dialer.go b/dialer/http3/dialer.go new file mode 100644 index 0000000..4f80461 --- /dev/null +++ b/dialer/http3/dialer.go @@ -0,0 +1,107 @@ +package http3 + +import ( + "context" + "crypto/tls" + "net" + "net/http" + "sync" + + "github.com/go-gost/gost/v3/pkg/dialer" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + pht_util "github.com/go-gost/x/internal/util/pht" + "github.com/lucas-clemente/quic-go" + "github.com/lucas-clemente/quic-go/http3" +) + +func init() { + registry.DialerRegistry().Register("http3", NewDialer) + registry.DialerRegistry().Register("h3", NewDialer) +} + +type http3Dialer struct { + clients map[string]*pht_util.Client + clientMutex sync.Mutex + md metadata + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &http3Dialer{ + clients: make(map[string]*pht_util.Client), + options: options, + } +} + +func (d *http3Dialer) Init(md md.Metadata) (err error) { + if err = d.parseMetadata(md); err != nil { + return + } + + return nil +} + +func (d *http3Dialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) { + d.clientMutex.Lock() + defer d.clientMutex.Unlock() + + client, ok := d.clients[addr] + if !ok { + var options dialer.DialOptions + for _, opt := range opts { + opt(&options) + } + + host := d.md.host + if host == "" { + host = options.Host + } + if h, _, _ := net.SplitHostPort(host); h != "" { + host = h + } + + client = &pht_util.Client{ + Host: host, + Client: &http.Client{ + // Timeout: 60 * time.Second, + Transport: &http3.RoundTripper{ + TLSClientConfig: d.options.TLSConfig, + Dial: func(network, adr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error) { + // d.options.Logger.Infof("dial: %s/%s, %s", addr, network, host) + udpAddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + return nil, err + } + + udpConn, err := options.NetDialer.Dial(context.Background(), "udp", "") + if err != nil { + return nil, err + } + + return quic.DialEarly(udpConn.(net.PacketConn), udpAddr, host, tlsCfg, cfg) + }, + }, + }, + AuthorizePath: d.md.authorizePath, + PushPath: d.md.pushPath, + PullPath: d.md.pullPath, + TLSEnabled: true, + Logger: d.options.Logger, + } + + d.clients[addr] = client + } + + return client.Dial(ctx, addr) +} + +// Multiplex implements dialer.Multiplexer interface. +func (d *http3Dialer) Multiplex() bool { + return true +} diff --git a/dialer/http3/metadata.go b/dialer/http3/metadata.go new file mode 100644 index 0000000..6293f90 --- /dev/null +++ b/dialer/http3/metadata.go @@ -0,0 +1,52 @@ +package http3 + +import ( + "strings" + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + dialTimeout = "dialTimeout" + defaultAuthorizePath = "/authorize" + defaultPushPath = "/push" + defaultPullPath = "/pull" +) + +const ( + defaultDialTimeout = 5 * time.Second +) + +type metadata struct { + dialTimeout time.Duration + authorizePath string + pushPath string + pullPath string + host string +} + +func (d *http3Dialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + authorizePath = "authorizePath" + pushPath = "pushPath" + pullPath = "pullPath" + host = "host" + ) + + d.md.authorizePath = mdata.GetString(md, authorizePath) + if !strings.HasPrefix(d.md.authorizePath, "/") { + d.md.authorizePath = defaultAuthorizePath + } + d.md.pushPath = mdata.GetString(md, pushPath) + if !strings.HasPrefix(d.md.pushPath, "/") { + d.md.pushPath = defaultPushPath + } + d.md.pullPath = mdata.GetString(md, pullPath) + if !strings.HasPrefix(d.md.pullPath, "/") { + d.md.pullPath = defaultPullPath + } + + d.md.host = mdata.GetString(md, host) + return +} diff --git a/dialer/icmp/conn.go b/dialer/icmp/conn.go new file mode 100644 index 0000000..13c05c5 --- /dev/null +++ b/dialer/icmp/conn.go @@ -0,0 +1,42 @@ +package quic + +import ( + "context" + "net" + + "github.com/lucas-clemente/quic-go" +) + +type quicSession struct { + session quic.Session +} + +func (session *quicSession) GetConn() (*quicConn, error) { + stream, err := session.session.OpenStreamSync(context.Background()) + if err != nil { + return nil, err + } + return &quicConn{ + Stream: stream, + laddr: session.session.LocalAddr(), + raddr: session.session.RemoteAddr(), + }, nil +} + +func (session *quicSession) Close() error { + return session.session.CloseWithError(quic.ApplicationErrorCode(0), "closed") +} + +type quicConn struct { + quic.Stream + laddr net.Addr + raddr net.Addr +} + +func (c *quicConn) LocalAddr() net.Addr { + return c.laddr +} + +func (c *quicConn) RemoteAddr() net.Addr { + return c.raddr +} diff --git a/dialer/icmp/dialer.go b/dialer/icmp/dialer.go new file mode 100644 index 0000000..c694c87 --- /dev/null +++ b/dialer/icmp/dialer.go @@ -0,0 +1,130 @@ +package quic + +import ( + "context" + "math" + "math/rand" + "net" + "sync" + "time" + + "github.com/go-gost/gost/v3/pkg/dialer" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + icmp_pkg "github.com/go-gost/x/internal/util/icmp" + "github.com/lucas-clemente/quic-go" + "golang.org/x/net/icmp" +) + +func init() { + registry.DialerRegistry().Register("icmp", NewDialer) +} + +type icmpDialer struct { + sessions map[string]*quicSession + sessionMutex sync.Mutex + logger logger.Logger + md metadata + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &icmpDialer{ + sessions: make(map[string]*quicSession), + logger: options.Logger, + options: options, + } +} + +func (d *icmpDialer) Init(md md.Metadata) (err error) { + if err = d.parseMetadata(md); err != nil { + return + } + + return nil +} + +func (d *icmpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (conn net.Conn, err error) { + if _, _, err := net.SplitHostPort(addr); err != nil { + addr = net.JoinHostPort(addr, "0") + } + + raddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + return nil, err + } + + d.sessionMutex.Lock() + defer d.sessionMutex.Unlock() + + session, ok := d.sessions[addr] + if !ok { + options := &dialer.DialOptions{} + for _, opt := range opts { + opt(options) + } + + var pc net.PacketConn + pc, err = icmp.ListenPacket("ip4:icmp", "") + if err != nil { + return + } + + id := raddr.Port + if id == 0 { + id = rand.New(rand.NewSource(time.Now().UnixNano())).Intn(math.MaxUint16) + 1 + raddr.Port = id + } + pc = icmp_pkg.ClientConn(pc, id) + + session, err = d.initSession(ctx, raddr, pc) + if err != nil { + d.logger.Error(err) + pc.Close() + return nil, err + } + + d.sessions[addr] = session + } + + conn, err = session.GetConn() + if err != nil { + session.Close() + delete(d.sessions, addr) + return nil, err + } + + return +} + +func (d *icmpDialer) initSession(ctx context.Context, addr net.Addr, conn net.PacketConn) (*quicSession, error) { + quicConfig := &quic.Config{ + KeepAlive: d.md.keepAlive, + HandshakeIdleTimeout: d.md.handshakeTimeout, + MaxIdleTimeout: d.md.maxIdleTimeout, + Versions: []quic.VersionNumber{ + quic.Version1, + quic.VersionDraft29, + }, + } + + tlsCfg := d.options.TLSConfig + tlsCfg.NextProtos = []string{"http/3", "quic/v1"} + + session, err := quic.DialContext(ctx, conn, addr, addr.String(), tlsCfg, quicConfig) + if err != nil { + return nil, err + } + return &quicSession{session: session}, nil +} + +// Multiplex implements dialer.Multiplexer interface. +func (d *icmpDialer) Multiplex() bool { + return true +} diff --git a/dialer/icmp/metadata.go b/dialer/icmp/metadata.go new file mode 100644 index 0000000..78c3c38 --- /dev/null +++ b/dialer/icmp/metadata.go @@ -0,0 +1,29 @@ +package quic + +import ( + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + keepAlive bool + maxIdleTimeout time.Duration + handshakeTimeout time.Duration +} + +func (d *icmpDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + keepAlive = "keepAlive" + handshakeTimeout = "handshakeTimeout" + maxIdleTimeout = "maxIdleTimeout" + ) + + d.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + + d.md.keepAlive = mdata.GetBool(md, keepAlive) + d.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + d.md.maxIdleTimeout = mdata.GetDuration(md, maxIdleTimeout) + + return +} diff --git a/dialer/kcp/conn.go b/dialer/kcp/conn.go new file mode 100644 index 0000000..41cf3d5 --- /dev/null +++ b/dialer/kcp/conn.go @@ -0,0 +1,55 @@ +package kcp + +import ( + "net" + + "github.com/xtaci/smux" +) + +type muxSession struct { + session *smux.Session +} + +func (session *muxSession) GetConn() (net.Conn, error) { + return session.session.OpenStream() +} + +func (session *muxSession) Accept() (net.Conn, error) { + return session.session.AcceptStream() +} + +func (session *muxSession) Close() error { + if session.session == nil { + return nil + } + return session.session.Close() +} + +func (session *muxSession) IsClosed() bool { + if session.session == nil { + return true + } + return session.session.IsClosed() +} + +func (session *muxSession) NumStreams() int { + return session.session.NumStreams() +} + +type fakeTCPConn struct { + raddr net.Addr + net.PacketConn +} + +func (c *fakeTCPConn) Read(b []byte) (n int, err error) { + n, _, err = c.ReadFrom(b) + return +} + +func (c *fakeTCPConn) Write(b []byte) (n int, err error) { + return c.WriteTo(b, c.raddr) +} + +func (c *fakeTCPConn) RemoteAddr() net.Addr { + return c.raddr +} diff --git a/dialer/kcp/dialer.go b/dialer/kcp/dialer.go new file mode 100644 index 0000000..eee0bf5 --- /dev/null +++ b/dialer/kcp/dialer.go @@ -0,0 +1,165 @@ +package kcp + +import ( + "context" + "errors" + "net" + "sync" + "time" + + "github.com/go-gost/gost/v3/pkg/dialer" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + kcp_util "github.com/go-gost/x/internal/util/kcp" + "github.com/xtaci/kcp-go/v5" + "github.com/xtaci/smux" + "github.com/xtaci/tcpraw" +) + +func init() { + registry.DialerRegistry().Register("kcp", NewDialer) +} + +type kcpDialer struct { + sessions map[string]*muxSession + sessionMutex sync.Mutex + logger logger.Logger + md metadata + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &kcpDialer{ + sessions: make(map[string]*muxSession), + logger: options.Logger, + options: options, + } +} + +func (d *kcpDialer) Init(md md.Metadata) (err error) { + if err = d.parseMetadata(md); err != nil { + return + } + + d.md.config.Init() + + return nil +} + +func (d *kcpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (conn net.Conn, err error) { + raddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + return nil, err + } + + d.sessionMutex.Lock() + defer d.sessionMutex.Unlock() + + session, ok := d.sessions[addr] + if session != nil && session.IsClosed() { + delete(d.sessions, addr) // session is dead + ok = false + } + if !ok { + var options dialer.DialOptions + for _, opt := range opts { + opt(&options) + } + + var pc net.PacketConn + if d.md.config.TCP { + pc, err = tcpraw.Dial("tcp", addr) + if err != nil { + return nil, err + } + pc = &fakeTCPConn{ + raddr: raddr, + PacketConn: pc, + } + } else { + c, err := options.NetDialer.Dial(ctx, "udp", "") + if err != nil { + return nil, err + } + + var ok bool + pc, ok = c.(net.PacketConn) + if !ok { + c.Close() + return nil, errors.New("quic: wrong connection type") + } + } + + session, err = d.initSession(ctx, raddr, pc) + if err != nil { + d.logger.Error(err) + pc.Close() + return nil, err + } + d.sessions[addr] = session + } + + conn, err = session.GetConn() + if err != nil { + session.Close() + delete(d.sessions, addr) + return nil, err + } + + return +} + +func (d *kcpDialer) initSession(ctx context.Context, addr net.Addr, conn net.PacketConn) (*muxSession, error) { + config := d.md.config + + kcpconn, err := kcp.NewConn(addr.String(), + kcp_util.BlockCrypt(config.Key, config.Crypt, kcp_util.DefaultSalt), + config.DataShard, config.ParityShard, conn) + if err != nil { + return nil, err + } + + kcpconn.SetStreamMode(true) + kcpconn.SetWriteDelay(false) + kcpconn.SetNoDelay(config.NoDelay, config.Interval, config.Resend, config.NoCongestion) + kcpconn.SetWindowSize(config.SndWnd, config.RcvWnd) + kcpconn.SetMtu(config.MTU) + kcpconn.SetACKNoDelay(config.AckNodelay) + + if config.DSCP > 0 { + if er := kcpconn.SetDSCP(config.DSCP); er != nil { + d.logger.Warn("SetDSCP: ", er) + } + } + if er := kcpconn.SetReadBuffer(config.SockBuf); er != nil { + d.logger.Warn("SetReadBuffer: ", er) + } + if er := kcpconn.SetWriteBuffer(config.SockBuf); er != nil { + d.logger.Warn("SetWriteBuffer: ", er) + } + + // stream multiplex + smuxConfig := smux.DefaultConfig() + smuxConfig.MaxReceiveBuffer = config.SockBuf + smuxConfig.KeepAliveInterval = time.Duration(config.KeepAlive) * time.Second + var cc net.Conn = kcpconn + if !config.NoComp { + cc = kcp_util.CompStreamConn(kcpconn) + } + session, err := smux.Client(cc, smuxConfig) + if err != nil { + return nil, err + } + return &muxSession{session: session}, nil +} + +// Multiplex implements dialer.Multiplexer interface. +func (d *kcpDialer) Multiplex() bool { + return true +} diff --git a/dialer/kcp/metadata.go b/dialer/kcp/metadata.go new file mode 100644 index 0000000..8406acf --- /dev/null +++ b/dialer/kcp/metadata.go @@ -0,0 +1,39 @@ +package kcp + +import ( + "encoding/json" + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" + kcp_util "github.com/go-gost/x/internal/util/kcp" +) + +type metadata struct { + handshakeTimeout time.Duration + config *kcp_util.Config +} + +func (d *kcpDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + config = "config" + handshakeTimeout = "handshakeTimeout" + ) + + if m := mdata.GetStringMap(md, config); len(m) > 0 { + b, err := json.Marshal(m) + if err != nil { + return err + } + cfg := &kcp_util.Config{} + if err := json.Unmarshal(b, cfg); err != nil { + return err + } + d.md.config = cfg + } + if d.md.config == nil { + d.md.config = kcp_util.DefaultConfig + } + + d.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + return +} diff --git a/dialer/mtls/conn.go b/dialer/mtls/conn.go new file mode 100644 index 0000000..4b5f8c2 --- /dev/null +++ b/dialer/mtls/conn.go @@ -0,0 +1,38 @@ +package mtls + +import ( + "net" + + "github.com/xtaci/smux" +) + +type muxSession struct { + conn net.Conn + session *smux.Session +} + +func (session *muxSession) GetConn() (net.Conn, error) { + return session.session.OpenStream() +} + +func (session *muxSession) Accept() (net.Conn, error) { + return session.session.AcceptStream() +} + +func (session *muxSession) Close() error { + if session.session == nil { + return nil + } + return session.session.Close() +} + +func (session *muxSession) IsClosed() bool { + if session.session == nil { + return true + } + return session.session.IsClosed() +} + +func (session *muxSession) NumStreams() int { + return session.session.NumStreams() +} diff --git a/dialer/mtls/dialer.go b/dialer/mtls/dialer.go new file mode 100644 index 0000000..64f5ec8 --- /dev/null +++ b/dialer/mtls/dialer.go @@ -0,0 +1,156 @@ +package mtls + +import ( + "context" + "crypto/tls" + "errors" + "net" + "sync" + "time" + + "github.com/go-gost/gost/v3/pkg/dialer" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "github.com/xtaci/smux" +) + +func init() { + registry.DialerRegistry().Register("mtls", NewDialer) +} + +type mtlsDialer struct { + sessions map[string]*muxSession + sessionMutex sync.Mutex + logger logger.Logger + md metadata + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &mtlsDialer{ + sessions: make(map[string]*muxSession), + logger: options.Logger, + options: options, + } +} + +func (d *mtlsDialer) Init(md md.Metadata) (err error) { + if err = d.parseMetadata(md); err != nil { + return + } + + return nil +} + +// Multiplex implements dialer.Multiplexer interface. +func (d *mtlsDialer) Multiplex() bool { + return true +} + +func (d *mtlsDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (conn net.Conn, err error) { + d.sessionMutex.Lock() + defer d.sessionMutex.Unlock() + + session, ok := d.sessions[addr] + if session != nil && session.IsClosed() { + delete(d.sessions, addr) // session is dead + ok = false + } + if !ok { + var options dialer.DialOptions + for _, opt := range opts { + opt(&options) + } + + conn, err = options.NetDialer.Dial(ctx, "tcp", addr) + if err != nil { + return + } + + session = &muxSession{conn: conn} + d.sessions[addr] = session + } + + return session.conn, err +} + +// Handshake implements dialer.Handshaker +func (d *mtlsDialer) Handshake(ctx context.Context, conn net.Conn, options ...dialer.HandshakeOption) (net.Conn, error) { + opts := &dialer.HandshakeOptions{} + for _, option := range options { + option(opts) + } + + d.sessionMutex.Lock() + defer d.sessionMutex.Unlock() + + if d.md.handshakeTimeout > 0 { + conn.SetDeadline(time.Now().Add(d.md.handshakeTimeout)) + defer conn.SetDeadline(time.Time{}) + } + + session, ok := d.sessions[opts.Addr] + if session != nil && session.conn != conn { + conn.Close() + return nil, errors.New("mtls: unrecognized connection") + } + + if !ok || session.session == nil { + s, err := d.initSession(ctx, conn) + if err != nil { + d.logger.Error(err) + conn.Close() + delete(d.sessions, opts.Addr) + return nil, err + } + session = s + d.sessions[opts.Addr] = session + } + cc, err := session.GetConn() + if err != nil { + session.Close() + delete(d.sessions, opts.Addr) + return nil, err + } + + return cc, nil +} + +func (d *mtlsDialer) initSession(ctx context.Context, conn net.Conn) (*muxSession, error) { + tlsConn := tls.Client(conn, d.options.TLSConfig) + if err := tlsConn.HandshakeContext(ctx); err != nil { + return nil, err + } + conn = tlsConn + + // stream multiplex + smuxConfig := smux.DefaultConfig() + smuxConfig.KeepAliveDisabled = d.md.muxKeepAliveDisabled + if d.md.muxKeepAliveInterval > 0 { + smuxConfig.KeepAliveInterval = d.md.muxKeepAliveInterval + } + if d.md.muxKeepAliveTimeout > 0 { + smuxConfig.KeepAliveTimeout = d.md.muxKeepAliveTimeout + } + if d.md.muxMaxFrameSize > 0 { + smuxConfig.MaxFrameSize = d.md.muxMaxFrameSize + } + if d.md.muxMaxReceiveBuffer > 0 { + smuxConfig.MaxReceiveBuffer = d.md.muxMaxReceiveBuffer + } + if d.md.muxMaxStreamBuffer > 0 { + smuxConfig.MaxStreamBuffer = d.md.muxMaxStreamBuffer + } + + session, err := smux.Client(conn, smuxConfig) + if err != nil { + return nil, err + } + return &muxSession{conn: conn, session: session}, nil +} diff --git a/dialer/mtls/metadata.go b/dialer/mtls/metadata.go new file mode 100644 index 0000000..0eb98dc --- /dev/null +++ b/dialer/mtls/metadata.go @@ -0,0 +1,42 @@ +package mtls + +import ( + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + handshakeTimeout time.Duration + + muxKeepAliveDisabled bool + muxKeepAliveInterval time.Duration + muxKeepAliveTimeout time.Duration + muxMaxFrameSize int + muxMaxReceiveBuffer int + muxMaxStreamBuffer int +} + +func (d *mtlsDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + handshakeTimeout = "handshakeTimeout" + + muxKeepAliveDisabled = "muxKeepAliveDisabled" + muxKeepAliveInterval = "muxKeepAliveInterval" + muxKeepAliveTimeout = "muxKeepAliveTimeout" + muxMaxFrameSize = "muxMaxFrameSize" + muxMaxReceiveBuffer = "muxMaxReceiveBuffer" + muxMaxStreamBuffer = "muxMaxStreamBuffer" + ) + + d.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + + d.md.muxKeepAliveDisabled = mdata.GetBool(md, muxKeepAliveDisabled) + d.md.muxKeepAliveInterval = mdata.GetDuration(md, muxKeepAliveInterval) + d.md.muxKeepAliveTimeout = mdata.GetDuration(md, muxKeepAliveTimeout) + d.md.muxMaxFrameSize = mdata.GetInt(md, muxMaxFrameSize) + d.md.muxMaxReceiveBuffer = mdata.GetInt(md, muxMaxReceiveBuffer) + d.md.muxMaxStreamBuffer = mdata.GetInt(md, muxMaxStreamBuffer) + + return +} diff --git a/dialer/mws/conn.go b/dialer/mws/conn.go new file mode 100644 index 0000000..7c5dbe5 --- /dev/null +++ b/dialer/mws/conn.go @@ -0,0 +1,38 @@ +package mws + +import ( + "net" + + "github.com/xtaci/smux" +) + +type muxSession struct { + conn net.Conn + session *smux.Session +} + +func (session *muxSession) GetConn() (net.Conn, error) { + return session.session.OpenStream() +} + +func (session *muxSession) Accept() (net.Conn, error) { + return session.session.AcceptStream() +} + +func (session *muxSession) Close() error { + if session.session == nil { + return nil + } + return session.session.Close() +} + +func (session *muxSession) IsClosed() bool { + if session.session == nil { + return true + } + return session.session.IsClosed() +} + +func (session *muxSession) NumStreams() int { + return session.session.NumStreams() +} diff --git a/dialer/mws/dialer.go b/dialer/mws/dialer.go new file mode 100644 index 0000000..4773033 --- /dev/null +++ b/dialer/mws/dialer.go @@ -0,0 +1,215 @@ +package mws + +import ( + "context" + "errors" + "net" + "net/url" + "sync" + "time" + + "github.com/go-gost/gost/v3/pkg/dialer" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + ws_util "github.com/go-gost/x/internal/util/ws" + "github.com/gorilla/websocket" + "github.com/xtaci/smux" +) + +func init() { + registry.DialerRegistry().Register("mws", NewDialer) + registry.DialerRegistry().Register("mwss", NewTLSDialer) +} + +type mwsDialer struct { + sessions map[string]*muxSession + sessionMutex sync.Mutex + tlsEnabled bool + md metadata + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &mwsDialer{ + sessions: make(map[string]*muxSession), + options: options, + } +} + +func NewTLSDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &mwsDialer{ + tlsEnabled: true, + sessions: make(map[string]*muxSession), + options: options, + } +} +func (d *mwsDialer) Init(md md.Metadata) (err error) { + if err = d.parseMetadata(md); err != nil { + return + } + + return nil +} + +// Multiplex implements dialer.Multiplexer interface. +func (d *mwsDialer) Multiplex() bool { + return true +} + +func (d *mwsDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (conn net.Conn, err error) { + d.sessionMutex.Lock() + defer d.sessionMutex.Unlock() + + session, ok := d.sessions[addr] + if session != nil && session.IsClosed() { + delete(d.sessions, addr) // session is dead + ok = false + } + if !ok { + var options dialer.DialOptions + for _, opt := range opts { + opt(&options) + } + + conn, err = options.NetDialer.Dial(ctx, "tcp", addr) + if err != nil { + return + } + + session = &muxSession{conn: conn} + d.sessions[addr] = session + } + + return session.conn, err +} + +// Handshake implements dialer.Handshaker +func (d *mwsDialer) Handshake(ctx context.Context, conn net.Conn, options ...dialer.HandshakeOption) (net.Conn, error) { + opts := &dialer.HandshakeOptions{} + for _, option := range options { + option(opts) + } + + d.sessionMutex.Lock() + defer d.sessionMutex.Unlock() + + session, ok := d.sessions[opts.Addr] + if session != nil && session.conn != conn { + conn.Close() + return nil, errors.New("mtls: unrecognized connection") + } + + if !ok || session.session == nil { + host := d.md.host + if host == "" { + host = opts.Addr + } + s, err := d.initSession(ctx, host, conn) + if err != nil { + d.options.Logger.Error(err) + conn.Close() + delete(d.sessions, opts.Addr) + return nil, err + } + session = s + d.sessions[opts.Addr] = session + } + cc, err := session.GetConn() + if err != nil { + session.Close() + delete(d.sessions, opts.Addr) + return nil, err + } + + return cc, nil +} + +func (d *mwsDialer) initSession(ctx context.Context, host string, conn net.Conn) (*muxSession, error) { + dialer := websocket.Dialer{ + HandshakeTimeout: d.md.handshakeTimeout, + ReadBufferSize: d.md.readBufferSize, + WriteBufferSize: d.md.writeBufferSize, + EnableCompression: d.md.enableCompression, + NetDial: func(net, addr string) (net.Conn, error) { + return conn, nil + }, + } + + url := url.URL{Scheme: "ws", Host: host, Path: d.md.path} + if d.tlsEnabled { + url.Scheme = "wss" + dialer.TLSClientConfig = d.options.TLSConfig + } + + if d.md.handshakeTimeout > 0 { + conn.SetReadDeadline(time.Now().Add(d.md.handshakeTimeout)) + } + + c, resp, err := dialer.DialContext(ctx, url.String(), d.md.header) + if err != nil { + return nil, err + } + resp.Body.Close() + + if d.md.handshakeTimeout > 0 { + conn.SetReadDeadline(time.Time{}) + } + + cc := ws_util.Conn(c) + + if d.md.keepAlive > 0 { + c.SetReadDeadline(time.Now().Add(d.md.keepAlive * 2)) + c.SetPongHandler(func(string) error { + c.SetReadDeadline(time.Now().Add(d.md.keepAlive * 2)) + return nil + }) + go d.keepAlive(cc) + } + + // stream multiplex + smuxConfig := smux.DefaultConfig() + smuxConfig.KeepAliveDisabled = d.md.muxKeepAliveDisabled + if d.md.muxKeepAliveInterval > 0 { + smuxConfig.KeepAliveInterval = d.md.muxKeepAliveInterval + } + if d.md.muxKeepAliveTimeout > 0 { + smuxConfig.KeepAliveTimeout = d.md.muxKeepAliveTimeout + } + if d.md.muxMaxFrameSize > 0 { + smuxConfig.MaxFrameSize = d.md.muxMaxFrameSize + } + if d.md.muxMaxReceiveBuffer > 0 { + smuxConfig.MaxReceiveBuffer = d.md.muxMaxReceiveBuffer + } + if d.md.muxMaxStreamBuffer > 0 { + smuxConfig.MaxStreamBuffer = d.md.muxMaxStreamBuffer + } + + session, err := smux.Client(cc, smuxConfig) + if err != nil { + return nil, err + } + return &muxSession{conn: cc, session: session}, nil +} + +func (d *mwsDialer) keepAlive(conn ws_util.WebsocketConn) { + ticker := time.NewTicker(d.md.keepAlive) + defer ticker.Stop() + + for range ticker.C { + conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return + } + } +} diff --git a/dialer/mws/metadata.go b/dialer/mws/metadata.go new file mode 100644 index 0000000..7ed3928 --- /dev/null +++ b/dialer/mws/metadata.go @@ -0,0 +1,87 @@ +package mws + +import ( + "net/http" + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultPath = "/ws" +) + +type metadata struct { + host string + path string + + handshakeTimeout time.Duration + readHeaderTimeout time.Duration + readBufferSize int + writeBufferSize int + enableCompression bool + + muxKeepAliveDisabled bool + muxKeepAliveInterval time.Duration + muxKeepAliveTimeout time.Duration + muxMaxFrameSize int + muxMaxReceiveBuffer int + muxMaxStreamBuffer int + + header http.Header + keepAlive time.Duration +} + +func (d *mwsDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + host = "host" + path = "path" + + handshakeTimeout = "handshakeTimeout" + readHeaderTimeout = "readHeaderTimeout" + readBufferSize = "readBufferSize" + writeBufferSize = "writeBufferSize" + enableCompression = "enableCompression" + + header = "header" + keepAlive = "keepAlive" + + muxKeepAliveDisabled = "muxKeepAliveDisabled" + muxKeepAliveInterval = "muxKeepAliveInterval" + muxKeepAliveTimeout = "muxKeepAliveTimeout" + muxMaxFrameSize = "muxMaxFrameSize" + muxMaxReceiveBuffer = "muxMaxReceiveBuffer" + muxMaxStreamBuffer = "muxMaxStreamBuffer" + ) + + d.md.host = mdata.GetString(md, host) + + d.md.path = mdata.GetString(md, path) + if d.md.path == "" { + d.md.path = defaultPath + } + + d.md.muxKeepAliveDisabled = mdata.GetBool(md, muxKeepAliveDisabled) + d.md.muxKeepAliveInterval = mdata.GetDuration(md, muxKeepAliveInterval) + d.md.muxKeepAliveTimeout = mdata.GetDuration(md, muxKeepAliveTimeout) + d.md.muxMaxFrameSize = mdata.GetInt(md, muxMaxFrameSize) + d.md.muxMaxReceiveBuffer = mdata.GetInt(md, muxMaxReceiveBuffer) + d.md.muxMaxStreamBuffer = mdata.GetInt(md, muxMaxStreamBuffer) + + d.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + d.md.readHeaderTimeout = mdata.GetDuration(md, readHeaderTimeout) + d.md.readBufferSize = mdata.GetInt(md, readBufferSize) + d.md.writeBufferSize = mdata.GetInt(md, writeBufferSize) + d.md.enableCompression = mdata.GetBool(md, enableCompression) + + if m := mdata.GetStringMapString(md, header); len(m) > 0 { + h := http.Header{} + for k, v := range m { + h.Add(k, v) + } + d.md.header = h + } + d.md.keepAlive = mdata.GetDuration(md, keepAlive) + + return +} diff --git a/dialer/obfs/http/conn.go b/dialer/obfs/http/conn.go new file mode 100644 index 0000000..5b10745 --- /dev/null +++ b/dialer/obfs/http/conn.go @@ -0,0 +1,144 @@ +package http + +import ( + "bufio" + "bytes" + "crypto/rand" + "encoding/base64" + "io" + "net" + "net/http" + "net/http/httputil" + "net/url" + "sync" + + "github.com/go-gost/gost/v3/pkg/logger" +) + +type obfsHTTPConn struct { + net.Conn + host string + rbuf bytes.Buffer + wbuf bytes.Buffer + headerDrained bool + handshaked bool + handshakeMutex sync.Mutex + header http.Header + logger logger.Logger +} + +func (c *obfsHTTPConn) Handshake() (err error) { + c.handshakeMutex.Lock() + defer c.handshakeMutex.Unlock() + + if c.handshaked { + return nil + } + + err = c.handshake() + if err != nil { + return + } + + c.handshaked = true + return nil +} + +func (c *obfsHTTPConn) handshake() (err error) { + r := &http.Request{ + Method: http.MethodGet, + ProtoMajor: 1, + ProtoMinor: 1, + URL: &url.URL{Scheme: "http", Host: c.host}, + Header: c.header, + } + if r.Header == nil { + r.Header = http.Header{} + } + r.Header.Set("Connection", "Upgrade") + r.Header.Set("Upgrade", "websocket") + key, _ := c.generateChallengeKey() + r.Header.Set("Sec-WebSocket-Key", key) + + // cache the request header + if err = r.Write(&c.wbuf); err != nil { + return + } + + if c.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpRequest(r, false) + c.logger.Debug(string(dump)) + } + + return nil +} + +func (c *obfsHTTPConn) Read(b []byte) (n int, err error) { + if err = c.Handshake(); err != nil { + return + } + + if err = c.drainHeader(); err != nil { + return + } + + if c.rbuf.Len() > 0 { + return c.rbuf.Read(b) + } + return c.Conn.Read(b) +} + +func (c *obfsHTTPConn) drainHeader() (err error) { + if c.headerDrained { + return + } + c.headerDrained = true + + br := bufio.NewReader(c.Conn) + // drain and discard the response header + var line string + var buf bytes.Buffer + for { + line, err = br.ReadString('\n') + if err != nil { + return + } + buf.WriteString(line) + if line == "\r\n" { + break + } + } + + if c.logger.IsLevelEnabled(logger.DebugLevel) { + c.logger.Debug(buf.String()) + } + + // cache the extra data for next read. + var b []byte + b, err = br.Peek(br.Buffered()) + if len(b) > 0 { + _, err = c.rbuf.Write(b) + } + return +} + +func (c *obfsHTTPConn) Write(b []byte) (n int, err error) { + if err = c.Handshake(); err != nil { + return + } + if c.wbuf.Len() > 0 { + c.wbuf.Write(b) // append the data to the cached header + _, err = c.wbuf.WriteTo(c.Conn) + n = len(b) // exclude the header length + return + } + return c.Conn.Write(b) +} + +func (c *obfsHTTPConn) generateChallengeKey() (string, error) { + p := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, p); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(p), nil +} diff --git a/dialer/obfs/http/dialer.go b/dialer/obfs/http/dialer.go new file mode 100644 index 0000000..ac10d7d --- /dev/null +++ b/dialer/obfs/http/dialer.go @@ -0,0 +1,68 @@ +package http + +import ( + "context" + "net" + + "github.com/go-gost/gost/v3/pkg/dialer" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" +) + +func init() { + registry.DialerRegistry().Register("ohttp", NewDialer) +} + +type obfsHTTPDialer struct { + md metadata + logger logger.Logger +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := &dialer.Options{} + for _, opt := range opts { + opt(options) + } + + return &obfsHTTPDialer{ + logger: options.Logger, + } +} + +func (d *obfsHTTPDialer) Init(md md.Metadata) (err error) { + return d.parseMetadata(md) +} + +func (d *obfsHTTPDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) { + options := &dialer.DialOptions{} + for _, opt := range opts { + opt(options) + } + + conn, err := options.NetDialer.Dial(ctx, "tcp", addr) + if err != nil { + d.logger.Error(err) + } + return conn, err +} + +// Handshake implements dialer.Handshaker +func (d *obfsHTTPDialer) Handshake(ctx context.Context, conn net.Conn, options ...dialer.HandshakeOption) (net.Conn, error) { + opts := &dialer.HandshakeOptions{} + for _, option := range options { + option(opts) + } + + host := d.md.host + if host == "" { + host = opts.Addr + } + + return &obfsHTTPConn{ + Conn: conn, + host: host, + header: d.md.header, + logger: d.logger, + }, nil +} diff --git a/dialer/obfs/http/metadata.go b/dialer/obfs/http/metadata.go new file mode 100644 index 0000000..ab824e6 --- /dev/null +++ b/dialer/obfs/http/metadata.go @@ -0,0 +1,29 @@ +package http + +import ( + "net/http" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + host string + header http.Header +} + +func (d *obfsHTTPDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + header = "header" + host = "host" + ) + + if m := mdata.GetStringMapString(md, header); len(m) > 0 { + h := http.Header{} + for k, v := range m { + h.Add(k, v) + } + d.md.header = h + } + d.md.host = mdata.GetString(md, host) + return +} diff --git a/dialer/obfs/tls/conn.go b/dialer/obfs/tls/conn.go new file mode 100644 index 0000000..8425d35 --- /dev/null +++ b/dialer/obfs/tls/conn.go @@ -0,0 +1,266 @@ +package tls + +import ( + "bytes" + "crypto/rand" + "crypto/tls" + "errors" + "net" + "sync" + "time" + + dissector "github.com/go-gost/tls-dissector" +) + +const ( + maxTLSDataLen = 16384 +) + +var ( + cipherSuites = []uint16{ + 0xc02c, 0xc030, 0x009f, 0xcca9, 0xcca8, 0xccaa, 0xc02b, 0xc02f, + 0x009e, 0xc024, 0xc028, 0x006b, 0xc023, 0xc027, 0x0067, 0xc00a, + 0xc014, 0x0039, 0xc009, 0xc013, 0x0033, 0x009d, 0x009c, 0x003d, + 0x003c, 0x0035, 0x002f, 0x00ff, + } + + compressionMethods = []uint8{0x00} + + algorithms = []uint16{ + 0x0601, 0x0602, 0x0603, 0x0501, 0x0502, 0x0503, 0x0401, 0x0402, + 0x0403, 0x0301, 0x0302, 0x0303, 0x0201, 0x0202, 0x0203, + } + + tlsRecordTypes = []uint8{0x16, 0x14, 0x16, 0x17} + tlsVersionMinors = []uint8{0x01, 0x03, 0x03, 0x03} + + ErrBadType = errors.New("bad type") + ErrBadMajorVersion = errors.New("bad major version") + ErrBadMinorVersion = errors.New("bad minor version") + ErrMaxDataLen = errors.New("bad tls data len") +) + +const ( + tlsRecordStateType = iota + tlsRecordStateVersion0 + tlsRecordStateVersion1 + tlsRecordStateLength0 + tlsRecordStateLength1 + tlsRecordStateData +) + +type obfsTLSParser struct { + step uint8 + state uint8 + length uint16 +} + +func (r *obfsTLSParser) Parse(b []byte) (int, error) { + i := 0 + last := 0 + length := len(b) + + for i < length { + ch := b[i] + switch r.state { + case tlsRecordStateType: + if tlsRecordTypes[r.step] != ch { + return 0, ErrBadType + } + r.state = tlsRecordStateVersion0 + i++ + case tlsRecordStateVersion0: + if ch != 0x03 { + return 0, ErrBadMajorVersion + } + r.state = tlsRecordStateVersion1 + i++ + case tlsRecordStateVersion1: + if ch != tlsVersionMinors[r.step] { + return 0, ErrBadMinorVersion + } + r.state = tlsRecordStateLength0 + i++ + case tlsRecordStateLength0: + r.length = uint16(ch) << 8 + r.state = tlsRecordStateLength1 + i++ + case tlsRecordStateLength1: + r.length |= uint16(ch) + if r.step == 0 { + r.length = 91 + } else if r.step == 1 { + r.length = 1 + } else if r.length > maxTLSDataLen { + return 0, ErrMaxDataLen + } + if r.length > 0 { + r.state = tlsRecordStateData + } else { + r.state = tlsRecordStateType + r.step++ + } + i++ + case tlsRecordStateData: + left := uint16(length - i) + if left > r.length { + left = r.length + } + if r.step >= 2 { + skip := i - last + copy(b[last:], b[i:length]) + length -= int(skip) + last += int(left) + i = last + } else { + i += int(left) + } + r.length -= left + if r.length == 0 { + if r.step < 3 { + r.step++ + } + r.state = tlsRecordStateType + } + } + } + + if last == 0 { + return 0, nil + } else if last < length { + length -= last + } + + return length, nil +} + +type obfsTLSConn struct { + net.Conn + wbuf bytes.Buffer + host string + handshaked chan struct{} + parser obfsTLSParser + handshakeMutex sync.Mutex +} + +func (c *obfsTLSConn) Handshaked() bool { + select { + case <-c.handshaked: + return true + default: + return false + } +} + +func (c *obfsTLSConn) Handshake(payload []byte) (err error) { + c.handshakeMutex.Lock() + defer c.handshakeMutex.Unlock() + + if c.Handshaked() { + return + } + + if err = c.handshake(payload); err != nil { + return + } + + close(c.handshaked) + return nil +} + +func (c *obfsTLSConn) handshake(payload []byte) error { + clientMsg := &dissector.ClientHelloMsg{ + Version: tls.VersionTLS12, + SessionID: make([]byte, 32), + CipherSuites: cipherSuites, + CompressionMethods: compressionMethods, + Extensions: []dissector.Extension{ + &dissector.SessionTicketExtension{ + Data: payload, + }, + &dissector.ServerNameExtension{ + Name: c.host, + }, + &dissector.ECPointFormatsExtension{ + Formats: []uint8{0x01, 0x00, 0x02}, + }, + &dissector.SupportedGroupsExtension{ + Groups: []uint16{0x001d, 0x0017, 0x0019, 0x0018}, + }, + &dissector.SignatureAlgorithmsExtension{ + Algorithms: algorithms, + }, + &dissector.EncryptThenMacExtension{}, + &dissector.ExtendedMasterSecretExtension{}, + }, + } + clientMsg.Random.Time = uint32(time.Now().Unix()) + rand.Read(clientMsg.Random.Opaque[:]) + rand.Read(clientMsg.SessionID) + b, err := clientMsg.Encode() + if err != nil { + return err + } + + record := &dissector.Record{ + Type: dissector.Handshake, + Version: tls.VersionTLS10, + Opaque: b, + } + if _, err := record.WriteTo(c.Conn); err != nil { + return err + } + return err +} + +func (c *obfsTLSConn) Read(b []byte) (n int, err error) { + <-c.handshaked + + n, err = c.Conn.Read(b) + if err != nil { + return + } + if n > 0 { + n, err = c.parser.Parse(b[:n]) + } + + return +} + +func (c *obfsTLSConn) Write(b []byte) (n int, err error) { + n = len(b) + + if !c.Handshaked() { + if err = c.Handshake(b); err != nil { + return + } + return + } + + for len(b) > 0 { + data := b + if len(b) > maxTLSDataLen { + data = b[:maxTLSDataLen] + b = b[maxTLSDataLen:] + } else { + b = b[:0] + } + record := &dissector.Record{ + Type: dissector.AppData, + Version: tls.VersionTLS12, + Opaque: data, + } + + if c.wbuf.Len() > 0 { + record.Type = dissector.Handshake + record.WriteTo(&c.wbuf) + _, err = c.wbuf.WriteTo(c.Conn) + return + } + + if _, err = record.WriteTo(c.Conn); err != nil { + return + } + } + return +} diff --git a/dialer/obfs/tls/dialer.go b/dialer/obfs/tls/dialer.go new file mode 100644 index 0000000..b75c74d --- /dev/null +++ b/dialer/obfs/tls/dialer.go @@ -0,0 +1,67 @@ +package tls + +import ( + "context" + "net" + + "github.com/go-gost/gost/v3/pkg/dialer" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" +) + +func init() { + registry.DialerRegistry().Register("otls", NewDialer) +} + +type obfsTLSDialer struct { + md metadata + logger logger.Logger +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := &dialer.Options{} + for _, opt := range opts { + opt(options) + } + + return &obfsTLSDialer{ + logger: options.Logger, + } +} + +func (d *obfsTLSDialer) Init(md md.Metadata) (err error) { + return d.parseMetadata(md) +} + +func (d *obfsTLSDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) { + options := &dialer.DialOptions{} + for _, opt := range opts { + opt(options) + } + + conn, err := options.NetDialer.Dial(ctx, "tcp", addr) + if err != nil { + d.logger.Error(err) + } + return conn, err +} + +// Handshake implements dialer.Handshaker +func (d *obfsTLSDialer) Handshake(ctx context.Context, conn net.Conn, options ...dialer.HandshakeOption) (net.Conn, error) { + opts := &dialer.HandshakeOptions{} + for _, option := range options { + option(opts) + } + + host := d.md.host + if host == "" { + host = opts.Addr + } + + return &obfsTLSConn{ + Conn: conn, + host: host, + handshaked: make(chan struct{}), + }, nil +} diff --git a/dialer/obfs/tls/metadata.go b/dialer/obfs/tls/metadata.go new file mode 100644 index 0000000..0f34ee6 --- /dev/null +++ b/dialer/obfs/tls/metadata.go @@ -0,0 +1,18 @@ +package tls + +import ( + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + host string +} + +func (d *obfsTLSDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + host = "host" + ) + + d.md.host = mdata.GetString(md, host) + return +} diff --git a/dialer/pht/dialer.go b/dialer/pht/dialer.go new file mode 100644 index 0000000..7268d3c --- /dev/null +++ b/dialer/pht/dialer.go @@ -0,0 +1,114 @@ +package pht + +import ( + "context" + "net" + "net/http" + "sync" + "time" + + "github.com/go-gost/gost/v3/pkg/dialer" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + pht_util "github.com/go-gost/x/internal/util/pht" +) + +func init() { + registry.DialerRegistry().Register("pht", NewDialer) + registry.DialerRegistry().Register("phts", NewTLSDialer) +} + +type phtDialer struct { + clients map[string]*pht_util.Client + clientMutex sync.Mutex + tlsEnabled bool + md metadata + logger logger.Logger + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &phtDialer{ + clients: make(map[string]*pht_util.Client), + options: options, + } +} + +func NewTLSDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &phtDialer{ + tlsEnabled: true, + clients: make(map[string]*pht_util.Client), + options: options, + } +} + +func (d *phtDialer) Init(md md.Metadata) (err error) { + if err = d.parseMetadata(md); err != nil { + return + } + + return nil +} + +func (d *phtDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) { + d.clientMutex.Lock() + defer d.clientMutex.Unlock() + + client, ok := d.clients[addr] + if !ok { + var options dialer.DialOptions + for _, opt := range opts { + opt(&options) + } + + host := d.md.host + if host == "" { + host = options.Host + } + if h, _, _ := net.SplitHostPort(host); h != "" { + host = h + } + + tr := &http.Transport{ + // Proxy: http.ProxyFromEnvironment, + DialContext: func(ctx context.Context, network, adr string) (net.Conn, error) { + return options.NetDialer.Dial(ctx, network, addr) + }, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + if d.tlsEnabled { + tr.TLSClientConfig = d.options.TLSConfig + } + + client = &pht_util.Client{ + Host: host, + Client: &http.Client{ + // Timeout: 60 * time.Second, + Transport: tr, + }, + AuthorizePath: d.md.authorizePath, + PushPath: d.md.pushPath, + PullPath: d.md.pullPath, + TLSEnabled: d.tlsEnabled, + Logger: d.options.Logger, + } + d.clients[addr] = client + } + + return client.Dial(ctx, addr) +} diff --git a/dialer/pht/metadata.go b/dialer/pht/metadata.go new file mode 100644 index 0000000..ad29190 --- /dev/null +++ b/dialer/pht/metadata.go @@ -0,0 +1,52 @@ +package pht + +import ( + "strings" + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + dialTimeout = "dialTimeout" + defaultAuthorizePath = "/authorize" + defaultPushPath = "/push" + defaultPullPath = "/pull" +) + +const ( + defaultDialTimeout = 5 * time.Second +) + +type metadata struct { + dialTimeout time.Duration + authorizePath string + pushPath string + pullPath string + host string +} + +func (d *phtDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + authorizePath = "authorizePath" + pushPath = "pushPath" + pullPath = "pullPath" + host = "host" + ) + + d.md.authorizePath = mdata.GetString(md, authorizePath) + if !strings.HasPrefix(d.md.authorizePath, "/") { + d.md.authorizePath = defaultAuthorizePath + } + d.md.pushPath = mdata.GetString(md, pushPath) + if !strings.HasPrefix(d.md.pushPath, "/") { + d.md.pushPath = defaultPushPath + } + d.md.pullPath = mdata.GetString(md, pullPath) + if !strings.HasPrefix(d.md.pullPath, "/") { + d.md.pullPath = defaultPullPath + } + + d.md.host = mdata.GetString(md, host) + return +} diff --git a/dialer/quic/conn.go b/dialer/quic/conn.go new file mode 100644 index 0000000..13c05c5 --- /dev/null +++ b/dialer/quic/conn.go @@ -0,0 +1,42 @@ +package quic + +import ( + "context" + "net" + + "github.com/lucas-clemente/quic-go" +) + +type quicSession struct { + session quic.Session +} + +func (session *quicSession) GetConn() (*quicConn, error) { + stream, err := session.session.OpenStreamSync(context.Background()) + if err != nil { + return nil, err + } + return &quicConn{ + Stream: stream, + laddr: session.session.LocalAddr(), + raddr: session.session.RemoteAddr(), + }, nil +} + +func (session *quicSession) Close() error { + return session.session.CloseWithError(quic.ApplicationErrorCode(0), "closed") +} + +type quicConn struct { + quic.Stream + laddr net.Addr + raddr net.Addr +} + +func (c *quicConn) LocalAddr() net.Addr { + return c.laddr +} + +func (c *quicConn) RemoteAddr() net.Addr { + return c.raddr +} diff --git a/dialer/quic/dialer.go b/dialer/quic/dialer.go new file mode 100644 index 0000000..0c98a70 --- /dev/null +++ b/dialer/quic/dialer.go @@ -0,0 +1,128 @@ +package quic + +import ( + "context" + "errors" + "net" + "sync" + + "github.com/go-gost/gost/v3/pkg/dialer" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + quic_util "github.com/go-gost/x/internal/util/quic" + "github.com/lucas-clemente/quic-go" +) + +func init() { + registry.DialerRegistry().Register("quic", NewDialer) +} + +type quicDialer struct { + sessions map[string]*quicSession + sessionMutex sync.Mutex + logger logger.Logger + md metadata + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &quicDialer{ + sessions: make(map[string]*quicSession), + logger: options.Logger, + options: options, + } +} + +func (d *quicDialer) Init(md md.Metadata) (err error) { + if err = d.parseMetadata(md); err != nil { + return + } + + return nil +} + +func (d *quicDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (conn net.Conn, err error) { + if _, _, err := net.SplitHostPort(addr); err != nil { + addr = net.JoinHostPort(addr, "0") + } + + udpAddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + return nil, err + } + + d.sessionMutex.Lock() + defer d.sessionMutex.Unlock() + + session, ok := d.sessions[addr] + if !ok { + options := &dialer.DialOptions{} + for _, opt := range opts { + opt(options) + } + + c, err := options.NetDialer.Dial(ctx, "udp", "") + if err != nil { + return nil, err + } + pc, ok := c.(net.PacketConn) + if !ok { + c.Close() + return nil, errors.New("quic: wrong connection type") + } + + if d.md.cipherKey != nil { + pc = quic_util.CipherPacketConn(pc, d.md.cipherKey) + } + + session, err = d.initSession(ctx, udpAddr, pc) + if err != nil { + d.logger.Error(err) + pc.Close() + return nil, err + } + + d.sessions[addr] = session + } + + conn, err = session.GetConn() + if err != nil { + session.Close() + delete(d.sessions, addr) + return nil, err + } + + return +} + +func (d *quicDialer) initSession(ctx context.Context, addr net.Addr, conn net.PacketConn) (*quicSession, error) { + quicConfig := &quic.Config{ + KeepAlive: d.md.keepAlive, + HandshakeIdleTimeout: d.md.handshakeTimeout, + MaxIdleTimeout: d.md.maxIdleTimeout, + Versions: []quic.VersionNumber{ + quic.Version1, + quic.VersionDraft29, + }, + } + + tlsCfg := d.options.TLSConfig + tlsCfg.NextProtos = []string{"http/3", "quic/v1"} + + session, err := quic.DialContext(ctx, conn, addr, addr.String(), tlsCfg, quicConfig) + if err != nil { + return nil, err + } + return &quicSession{session: session}, nil +} + +// Multiplex implements dialer.Multiplexer interface. +func (d *quicDialer) Multiplex() bool { + return true +} diff --git a/dialer/quic/metadata.go b/dialer/quic/metadata.go new file mode 100644 index 0000000..7d3773a --- /dev/null +++ b/dialer/quic/metadata.go @@ -0,0 +1,40 @@ +package quic + +import ( + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + keepAlive bool + maxIdleTimeout time.Duration + handshakeTimeout time.Duration + + cipherKey []byte + host string +} + +func (d *quicDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + keepAlive = "keepAlive" + handshakeTimeout = "handshakeTimeout" + maxIdleTimeout = "maxIdleTimeout" + + cipherKey = "cipherKey" + host = "host" + ) + + d.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + + if key := mdata.GetString(md, cipherKey); key != "" { + d.md.cipherKey = []byte(key) + } + + d.md.keepAlive = mdata.GetBool(md, keepAlive) + d.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + d.md.maxIdleTimeout = mdata.GetDuration(md, maxIdleTimeout) + + d.md.host = mdata.GetString(md, host) + return +} diff --git a/dialer/ssh/conn.go b/dialer/ssh/conn.go new file mode 100644 index 0000000..fb14c6f --- /dev/null +++ b/dialer/ssh/conn.go @@ -0,0 +1,31 @@ +package ssh + +import ( + "net" + + "golang.org/x/crypto/ssh" +) + +type sshSession struct { + addr string + conn net.Conn + client *ssh.Client + closed chan struct{} + dead chan struct{} +} + +func (s *sshSession) IsClosed() bool { + select { + case <-s.dead: + return true + case <-s.closed: + return true + default: + } + return false +} + +func (s *sshSession) wait() error { + defer close(s.closed) + return s.client.Wait() +} diff --git a/dialer/ssh/dialer.go b/dialer/ssh/dialer.go new file mode 100644 index 0000000..863f5f9 --- /dev/null +++ b/dialer/ssh/dialer.go @@ -0,0 +1,165 @@ +package ssh + +import ( + "context" + "errors" + "net" + "sync" + "time" + + "github.com/go-gost/gost/v3/pkg/dialer" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + ssh_util "github.com/go-gost/x/internal/util/ssh" + "golang.org/x/crypto/ssh" +) + +func init() { + registry.DialerRegistry().Register("ssh", NewDialer) +} + +type sshDialer struct { + sessions map[string]*sshSession + sessionMutex sync.Mutex + logger logger.Logger + md metadata +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := &dialer.Options{} + for _, opt := range opts { + opt(options) + } + + return &sshDialer{ + sessions: make(map[string]*sshSession), + logger: options.Logger, + } +} + +func (d *sshDialer) Init(md md.Metadata) (err error) { + if err = d.parseMetadata(md); err != nil { + return + } + + return nil +} + +// Multiplex implements dialer.Multiplexer interface. +func (d *sshDialer) Multiplex() bool { + return true +} + +func (d *sshDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (conn net.Conn, err error) { + d.sessionMutex.Lock() + defer d.sessionMutex.Unlock() + + session, ok := d.sessions[addr] + if session != nil && session.IsClosed() { + delete(d.sessions, addr) // session is dead + ok = false + } + if !ok { + var options dialer.DialOptions + for _, opt := range opts { + opt(&options) + } + + conn, err = options.NetDialer.Dial(ctx, "tcp", addr) + if err != nil { + return + } + + session = &sshSession{ + addr: addr, + conn: conn, + } + d.sessions[addr] = session + } + + return session.conn, err +} + +// Handshake implements dialer.Handshaker +func (d *sshDialer) Handshake(ctx context.Context, conn net.Conn, options ...dialer.HandshakeOption) (net.Conn, error) { + opts := &dialer.HandshakeOptions{} + for _, option := range options { + option(opts) + } + + d.sessionMutex.Lock() + defer d.sessionMutex.Unlock() + + if d.md.handshakeTimeout > 0 { + conn.SetDeadline(time.Now().Add(d.md.handshakeTimeout)) + defer conn.SetDeadline(time.Time{}) + } + + session, ok := d.sessions[opts.Addr] + if session != nil && session.conn != conn { + err := errors.New("ssh: unrecognized connection") + d.logger.Error(err) + conn.Close() + delete(d.sessions, opts.Addr) + return nil, err + } + + if !ok || session.client == nil { + s, err := d.initSession(ctx, opts.Addr, conn) + if err != nil { + d.logger.Error(err) + conn.Close() + delete(d.sessions, opts.Addr) + return nil, err + } + session = s + go func() { + s.wait() + d.logger.Debug("session closed") + }() + d.sessions[opts.Addr] = session + } + if session.IsClosed() { + delete(d.sessions, opts.Addr) + return nil, ssh_util.ErrSessionDead + } + + channel, reqs, err := session.client.OpenChannel(ssh_util.GostSSHTunnelRequest, nil) + if err != nil { + return nil, err + } + go ssh.DiscardRequests(reqs) + + return ssh_util.NewConn(conn, channel), nil +} + +func (d *sshDialer) initSession(ctx context.Context, addr string, conn net.Conn) (*sshSession, error) { + config := ssh.ClientConfig{ + Timeout: 30 * time.Second, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + if d.md.user != nil { + config.User = d.md.user.Username() + if password, _ := d.md.user.Password(); password != "" { + config.Auth = []ssh.AuthMethod{ + ssh.Password(password), + } + } + } + if d.md.signer != nil { + config.Auth = append(config.Auth, ssh.PublicKeys(d.md.signer)) + } + + sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, &config) + if err != nil { + return nil, err + } + + return &sshSession{ + conn: conn, + client: ssh.NewClient(sshConn, chans, reqs), + closed: make(chan struct{}), + dead: make(chan struct{}), + }, nil +} diff --git a/dialer/ssh/metadata.go b/dialer/ssh/metadata.go new file mode 100644 index 0000000..0c67ef7 --- /dev/null +++ b/dialer/ssh/metadata.go @@ -0,0 +1,56 @@ +package ssh + +import ( + "io/ioutil" + "net/url" + "strings" + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" + "golang.org/x/crypto/ssh" +) + +type metadata struct { + handshakeTimeout time.Duration + user *url.Userinfo + signer ssh.Signer +} + +func (d *sshDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + handshakeTimeout = "handshakeTimeout" + user = "user" + privateKeyFile = "privateKeyFile" + passphrase = "passphrase" + ) + + if v := mdata.GetString(md, user); v != "" { + ss := strings.SplitN(v, ":", 2) + if len(ss) == 1 { + d.md.user = url.User(ss[0]) + } else { + d.md.user = url.UserPassword(ss[0], ss[1]) + } + } + + if key := mdata.GetString(md, privateKeyFile); key != "" { + data, err := ioutil.ReadFile(key) + if err != nil { + return err + } + + pp := mdata.GetString(md, passphrase) + if pp == "" { + d.md.signer, err = ssh.ParsePrivateKey(data) + } else { + d.md.signer, err = ssh.ParsePrivateKeyWithPassphrase(data, []byte(pp)) + } + if err != nil { + return err + } + } + + d.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + + return +} diff --git a/dialer/sshd/conn.go b/dialer/sshd/conn.go new file mode 100644 index 0000000..120ffb5 --- /dev/null +++ b/dialer/sshd/conn.go @@ -0,0 +1,31 @@ +package sshd + +import ( + "net" + + "golang.org/x/crypto/ssh" +) + +type sshSession struct { + addr string + conn net.Conn + client *ssh.Client + closed chan struct{} + dead chan struct{} +} + +func (s *sshSession) IsClosed() bool { + select { + case <-s.dead: + return true + case <-s.closed: + return true + default: + } + return false +} + +func (s *sshSession) wait() error { + defer close(s.closed) + return s.client.Wait() +} diff --git a/dialer/sshd/dialer.go b/dialer/sshd/dialer.go new file mode 100644 index 0000000..2d94d05 --- /dev/null +++ b/dialer/sshd/dialer.go @@ -0,0 +1,160 @@ +package sshd + +import ( + "context" + "errors" + "net" + "sync" + "time" + + "github.com/go-gost/gost/v3/pkg/dialer" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + ssh_util "github.com/go-gost/x/internal/util/ssh" + "golang.org/x/crypto/ssh" +) + +func init() { + registry.DialerRegistry().Register("sshd", NewDialer) +} + +type sshdDialer struct { + sessions map[string]*sshSession + sessionMutex sync.Mutex + md metadata + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &sshdDialer{ + sessions: make(map[string]*sshSession), + options: options, + } +} + +func (d *sshdDialer) Init(md md.Metadata) (err error) { + if err = d.parseMetadata(md); err != nil { + return + } + + return nil +} + +// Multiplex implements dialer.Multiplexer interface. +func (d *sshdDialer) Multiplex() bool { + return true +} + +func (d *sshdDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (conn net.Conn, err error) { + d.sessionMutex.Lock() + defer d.sessionMutex.Unlock() + + session, ok := d.sessions[addr] + if session != nil && session.IsClosed() { + delete(d.sessions, addr) // session is dead + ok = false + } + if !ok { + var options dialer.DialOptions + for _, opt := range opts { + opt(&options) + } + + conn, err = options.NetDialer.Dial(ctx, "tcp", addr) + if err != nil { + return + } + + session = &sshSession{ + addr: addr, + conn: conn, + } + d.sessions[addr] = session + } + + return session.conn, err +} + +// Handshake implements dialer.Handshaker +func (d *sshdDialer) Handshake(ctx context.Context, conn net.Conn, options ...dialer.HandshakeOption) (net.Conn, error) { + opts := &dialer.HandshakeOptions{} + for _, option := range options { + option(opts) + } + + d.sessionMutex.Lock() + defer d.sessionMutex.Unlock() + + if d.md.handshakeTimeout > 0 { + conn.SetDeadline(time.Now().Add(d.md.handshakeTimeout)) + defer conn.SetDeadline(time.Time{}) + } + + log := d.options.Logger + + session, ok := d.sessions[opts.Addr] + if session != nil && session.conn != conn { + err := errors.New("ssh: unrecognized connection") + log.Error(err) + conn.Close() + delete(d.sessions, opts.Addr) + return nil, err + } + + if !ok || session.client == nil { + s, err := d.initSession(ctx, opts.Addr, conn) + if err != nil { + log.Error(err) + conn.Close() + delete(d.sessions, opts.Addr) + return nil, err + } + session = s + go func() { + s.wait() + log.Debug("session closed") + }() + d.sessions[opts.Addr] = session + } + if session.IsClosed() { + delete(d.sessions, opts.Addr) + return nil, ssh_util.ErrSessionDead + } + + return ssh_util.NewClientConn(session.conn, session.client), nil +} + +func (d *sshdDialer) initSession(ctx context.Context, addr string, conn net.Conn) (*sshSession, error) { + config := ssh.ClientConfig{ + // Timeout: timeout, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + if d.options.Auth != nil { + config.User = d.options.Auth.Username() + if password, _ := d.options.Auth.Password(); password != "" { + config.Auth = []ssh.AuthMethod{ + ssh.Password(password), + } + } + } + if d.md.signer != nil { + config.Auth = append(config.Auth, ssh.PublicKeys(d.md.signer)) + } + + sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, &config) + if err != nil { + return nil, err + } + + return &sshSession{ + conn: conn, + client: ssh.NewClient(sshConn, chans, reqs), + closed: make(chan struct{}), + dead: make(chan struct{}), + }, nil +} diff --git a/dialer/sshd/metadata.go b/dialer/sshd/metadata.go new file mode 100644 index 0000000..0f66a0c --- /dev/null +++ b/dialer/sshd/metadata.go @@ -0,0 +1,43 @@ +package sshd + +import ( + "io/ioutil" + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" + "golang.org/x/crypto/ssh" +) + +type metadata struct { + handshakeTimeout time.Duration + signer ssh.Signer +} + +func (d *sshdDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + handshakeTimeout = "handshakeTimeout" + privateKeyFile = "privateKeyFile" + passphrase = "passphrase" + ) + + if key := mdata.GetString(md, privateKeyFile); key != "" { + data, err := ioutil.ReadFile(key) + if err != nil { + return err + } + + pp := mdata.GetString(md, passphrase) + if pp == "" { + d.md.signer, err = ssh.ParsePrivateKey(data) + } else { + d.md.signer, err = ssh.ParsePrivateKeyWithPassphrase(data, []byte(pp)) + } + if err != nil { + return err + } + } + + d.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + + return +} diff --git a/dialer/ws/dialer.go b/dialer/ws/dialer.go new file mode 100644 index 0000000..e410afb --- /dev/null +++ b/dialer/ws/dialer.go @@ -0,0 +1,132 @@ +package ws + +import ( + "context" + "net" + "net/url" + "time" + + "github.com/go-gost/gost/v3/pkg/dialer" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + ws_util "github.com/go-gost/x/internal/util/ws" + "github.com/gorilla/websocket" +) + +func init() { + registry.DialerRegistry().Register("ws", NewDialer) + registry.DialerRegistry().Register("wss", NewTLSDialer) +} + +type wsDialer struct { + tlsEnabled bool + md metadata + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &wsDialer{ + options: options, + } +} + +func NewTLSDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &wsDialer{ + tlsEnabled: true, + options: options, + } +} + +func (d *wsDialer) Init(md md.Metadata) (err error) { + return d.parseMetadata(md) +} + +func (d *wsDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) { + var options dialer.DialOptions + for _, opt := range opts { + opt(&options) + } + + conn, err := options.NetDialer.Dial(ctx, "tcp", addr) + if err != nil { + d.options.Logger.Error(err) + } + return conn, err +} + +// Handshake implements dialer.Handshaker +func (d *wsDialer) Handshake(ctx context.Context, conn net.Conn, options ...dialer.HandshakeOption) (net.Conn, error) { + opts := &dialer.HandshakeOptions{} + for _, option := range options { + option(opts) + } + + if d.md.handshakeTimeout > 0 { + conn.SetReadDeadline(time.Now().Add(d.md.handshakeTimeout)) + defer conn.SetReadDeadline(time.Time{}) + } + + host := d.md.host + if host == "" { + host = opts.Addr + } + + dialer := websocket.Dialer{ + HandshakeTimeout: d.md.handshakeTimeout, + ReadBufferSize: d.md.readBufferSize, + WriteBufferSize: d.md.writeBufferSize, + EnableCompression: d.md.enableCompression, + NetDial: func(net, addr string) (net.Conn, error) { + return conn, nil + }, + } + + url := url.URL{Scheme: "ws", Host: host, Path: d.md.path} + if d.tlsEnabled { + url.Scheme = "wss" + dialer.TLSClientConfig = d.options.TLSConfig + } + + c, resp, err := dialer.DialContext(ctx, url.String(), d.md.header) + if err != nil { + return nil, err + } + resp.Body.Close() + + cc := ws_util.Conn(c) + + if d.md.keepAlive > 0 { + c.SetReadDeadline(time.Now().Add(d.md.keepAlive * 2)) + c.SetPongHandler(func(string) error { + c.SetReadDeadline(time.Now().Add(d.md.keepAlive * 2)) + d.options.Logger.Infof("pong: set read deadline: %v", d.md.keepAlive*2) + return nil + }) + go d.keepAlive(cc) + } + + return cc, nil +} + +func (d *wsDialer) keepAlive(conn ws_util.WebsocketConn) { + ticker := time.NewTicker(d.md.keepAlive) + defer ticker.Stop() + + for range ticker.C { + conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return + } + d.options.Logger.Infof("send ping") + } +} diff --git a/dialer/ws/metadata.go b/dialer/ws/metadata.go new file mode 100644 index 0000000..a625214 --- /dev/null +++ b/dialer/ws/metadata.go @@ -0,0 +1,66 @@ +package ws + +import ( + "net/http" + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultPath = "/ws" +) + +type metadata struct { + host string + path string + + handshakeTimeout time.Duration + readHeaderTimeout time.Duration + readBufferSize int + writeBufferSize int + enableCompression bool + + header http.Header + keepAlive time.Duration +} + +func (d *wsDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + host = "host" + path = "path" + + handshakeTimeout = "handshakeTimeout" + readHeaderTimeout = "readHeaderTimeout" + readBufferSize = "readBufferSize" + writeBufferSize = "writeBufferSize" + enableCompression = "enableCompression" + + header = "header" + keepAlive = "keepAlive" + ) + + d.md.host = mdata.GetString(md, host) + + d.md.path = mdata.GetString(md, path) + if d.md.path == "" { + d.md.path = defaultPath + } + + d.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + d.md.readHeaderTimeout = mdata.GetDuration(md, readHeaderTimeout) + d.md.readBufferSize = mdata.GetInt(md, readBufferSize) + d.md.writeBufferSize = mdata.GetInt(md, writeBufferSize) + d.md.enableCompression = mdata.GetBool(md, enableCompression) + + if m := mdata.GetStringMapString(md, header); len(m) > 0 { + h := http.Header{} + for k, v := range m { + h.Add(k, v) + } + d.md.header = h + } + d.md.keepAlive = mdata.GetDuration(md, keepAlive) + + return +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bec2a12 --- /dev/null +++ b/go.mod @@ -0,0 +1,71 @@ +module github.com/go-gost/x + +go 1.18 + +replace github.com/templexxx/cpu v0.0.7 => github.com/templexxx/cpu v0.0.10-0.20211111114238-98168dcec14a + +require ( + github.com/LiamHaworth/go-tproxy v0.0.0-20190726054950-ef7efd7f24ed + github.com/docker/libcontainer v2.2.1+incompatible + github.com/go-gost/gosocks5 v0.3.1-0.20211109033403-d894d75b7f09 + github.com/go-gost/gost/v3 v3.0.0-alpha.3.0.20220314122048-c282e69ffdc6 + github.com/go-gost/relay v0.1.1-0.20211123134818-8ef7fd81ffd7 + github.com/go-gost/tls-dissector v0.0.2-0.20211125135007-2b5d5bd9c07e + github.com/golang/snappy v0.0.4 + github.com/gorilla/websocket v1.5.0 + github.com/lucas-clemente/quic-go v0.25.0 + github.com/miekg/dns v1.1.26 + github.com/milosgajdos/tenus v0.0.3 + github.com/rs/xid v1.3.0 + github.com/shadowsocks/go-shadowsocks2 v0.1.5 + github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601 + github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 + github.com/xtaci/kcp-go/v5 v5.6.1 + github.com/xtaci/smux v1.5.16 + github.com/xtaci/tcpraw v1.2.25 + golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 + golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd + google.golang.org/grpc v1.45.0 + google.golang.org/protobuf v1.27.1 +) + +require ( + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cheekybits/genny v1.0.0 // indirect + github.com/coreos/go-iptables v0.5.0 // indirect + github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/klauspost/cpuid v1.3.1 // indirect + github.com/klauspost/reedsolomon v1.9.9 // indirect + github.com/marten-seemann/qpack v0.2.1 // 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/marten-seemann/qtls-go1-18 v0.1.0-beta.1 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104 // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/onsi/ginkgo v1.16.4 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.12.1 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/templexxx/cpu v0.0.7 // indirect + github.com/templexxx/xorsimd v0.4.1 // indirect + github.com/tjfoc/gmsm v1.3.2 // indirect + golang.org/x/mod v0.4.2 // indirect + golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/tools v0.1.1 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e34b1dd --- /dev/null +++ b/go.sum @@ -0,0 +1,744 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/LiamHaworth/go-tproxy v0.0.0-20190726054950-ef7efd7f24ed h1:eqa6queieK8SvoszxCu0WwH7lSVeL4/N/f1JwOMw1G4= +github.com/LiamHaworth/go-tproxy v0.0.0-20190726054950-ef7efd7f24ed/go.mod h1:rA52xkgZwql9LRZXWb2arHEFP6qSR48KY2xOfWzEciQ= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-iptables v0.5.0 h1:mw6SAibtHKZcNzAsOxjoHIG0gy5YFHhypWSSNc6EjbQ= +github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/libcontainer v2.2.1+incompatible h1:++SbbkCw+X8vAd4j2gOCzZ2Nn7s2xFALTf7LZKmM1/0= +github.com/docker/libcontainer v2.2.1+incompatible/go.mod h1:osvj61pYsqhNCMLGX31xr7klUBhHb/ZBuXS0o1Fvwbw= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gost/gosocks5 v0.3.1-0.20211109033403-d894d75b7f09 h1:A95M6UWcfZgOuJkQ7QLfG0Hs5peWIUSysCDNz4pfe04= +github.com/go-gost/gosocks5 v0.3.1-0.20211109033403-d894d75b7f09/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4= +github.com/go-gost/gost/v3 v3.0.0-alpha.3.0.20220314111021-eae153d48180 h1:vJg3eBA7kT0dWPxFj2gP8rn2fdZWzK3g4EoqPYNA2WU= +github.com/go-gost/gost/v3 v3.0.0-alpha.3.0.20220314111021-eae153d48180/go.mod h1:b7dAgXbJVtvCCvpfVl0NJuMxsxJmYjdEgQeG6kkIXHg= +github.com/go-gost/gost/v3 v3.0.0-alpha.3.0.20220314122048-c282e69ffdc6 h1:epQ9Sv+Y6mhqTS/IKKOBQgIIw6JBbbet7ZmdH/8bgfM= +github.com/go-gost/gost/v3 v3.0.0-alpha.3.0.20220314122048-c282e69ffdc6/go.mod h1:Hv/IAKyc+9uEGQT5DPWsnqgAFEp4Or7uOxFawr6ghjM= +github.com/go-gost/relay v0.1.1-0.20211123134818-8ef7fd81ffd7 h1:itaaJhQJ19kUXEB4Igb0EbY8m+1Py2AaNNSBds/9gk4= +github.com/go-gost/relay v0.1.1-0.20211123134818-8ef7fd81ffd7/go.mod h1:lcX+23LCQ3khIeASBo+tJ/WbwXFO32/N5YN6ucuYTG8= +github.com/go-gost/tls-dissector v0.0.2-0.20211125135007-2b5d5bd9c07e h1:73NGqAs22ey3wJkIYVD/ACEoovuIuOlEzQTEoqrO5+U= +github.com/go-gost/tls-dissector v0.0.2-0.20211125135007-2b5d5bd9c07e/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid v1.2.4/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= +github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= +github.com/klauspost/reedsolomon v1.9.9 h1:qCL7LZlv17xMixl55nq2/Oa1Y86nfO8EqDfv2GHND54= +github.com/klauspost/reedsolomon v1.9.9/go.mod h1:O7yFFHiQwDR6b2t63KPUpccPtNdp5ADgh1gg4fd12wo= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucas-clemente/quic-go v0.25.0 h1:K+X9Gvd7JXsOHtU0N2icZ2Nw3rx82uBej3mP4CLgibc= +github.com/lucas-clemente/quic-go v0.25.0/go.mod h1:YtzP8bxRVCBlO77yRanE264+fY/T2U9ZlW1AaHOsMOg= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs= +github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= +github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I= +github.com/marten-seemann/qtls-go1-16 v0.1.4 h1:xbHbOGGhrenVtII6Co8akhLEdrawwB2iHl5yhJRpnco= +github.com/marten-seemann/qtls-go1-16 v0.1.4/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= +github.com/marten-seemann/qtls-go1-17 v0.1.0 h1:P9ggrs5xtwiqXv/FHNwntmuLMNq3KaSIG93AtAZ48xk= +github.com/marten-seemann/qtls-go1-17 v0.1.0/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8= +github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1 h1:EnzzN9fPUkUck/1CuY1FlzBaIYMoiBsdwTNmNGkwUUM= +github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1/go.mod h1:PUhIQk19LoFt2174H4+an8TYvWOGjb/hHwphBeaDHwI= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/milosgajdos/tenus v0.0.3 h1:jmaJzwaY1DUyYVD0lM4U+uvP2kkEg1VahDqRFxIkVBE= +github.com/milosgajdos/tenus v0.0.3/go.mod h1:eIjx29vNeDOYWJuCnaHY2r4fq5egetV26ry3on7p8qY= +github.com/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104 h1:ULR/QWMgcgRiZLUjSSJMU+fW+RDMstRdmnDWj9Q+AsA= +github.com/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104/go.mod h1:wqKykBG2QzQDJEzvRkcS8x6MiSJkF52hXZsXcjaB3ls= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= +github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= +github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28= +github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM= +github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601 h1:XU9hik0exChEmY92ALW4l9WnDodxLVS9yOSNh2SizaQ= +github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601/go.mod h1:mttDPaeLm87u74HMrP+n2tugXvIKWcwff/cqSX0lehY= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= +github.com/templexxx/cpu v0.0.10-0.20211111114238-98168dcec14a h1:f0GQM8LuKYnXdNLcAg+di6PULSlR5iQtZT3bDwDRiA0= +github.com/templexxx/cpu v0.0.10-0.20211111114238-98168dcec14a/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= +github.com/templexxx/xorsimd v0.4.1 h1:iUZcywbOYDRAZUasAs2eSCUW8eobuZDy0I9FJiORkVg= +github.com/templexxx/xorsimd v0.4.1/go.mod h1:W+ffZz8jJMH2SXwuKu9WhygqBMbFnp14G2fqEr8qaNo= +github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM= +github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/xtaci/kcp-go/v5 v5.6.1 h1:Pwn0aoeNSPF9dTS7IgiPXn0HEtaIlVb6y5UKWPsx8bI= +github.com/xtaci/kcp-go/v5 v5.6.1/go.mod h1:W3kVPyNYwZ06p79dNwFWQOVFrdcBpDBsdyvK8moQrYo= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= +github.com/xtaci/smux v1.5.16 h1:FBPYOkW8ZTjLKUM4LI4xnnuuDC8CQ/dB04HD519WoEk= +github.com/xtaci/smux v1.5.16/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +github.com/xtaci/tcpraw v1.2.25 h1:VDlqo0op17JeXBM6e2G9ocCNLOJcw9mZbobMbJjo0vk= +github.com/xtaci/tcpraw v1.2.25/go.mod h1:dKyZ2V75s0cZ7cbgJYdxPvms7af0joIeOyx1GgJQbLk= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/arch v0.0.0-20190909030613-46d78d1859ac/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 h1:syTAU9FwmvzEoIYMqcPHOcVm4H3U5u90WsvuYgwpETU= +golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200425043458-8463f397d07c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200808161706-5bf02b21f123/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1 h1:wGiQel/hW0NnEkJUk8lbzkX2gFJU6PFxf1v5OlCfuOs= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/handler/dns/handler.go b/handler/dns/handler.go new file mode 100644 index 0000000..00a2213 --- /dev/null +++ b/handler/dns/handler.go @@ -0,0 +1,280 @@ +package dns + +import ( + "bytes" + "context" + "errors" + "fmt" + "net" + "strconv" + "strings" + "time" + + "github.com/go-gost/gost/v3/pkg/chain" + "github.com/go-gost/gost/v3/pkg/common/bufpool" + resolver_util "github.com/go-gost/gost/v3/pkg/common/util/resolver" + "github.com/go-gost/gost/v3/pkg/handler" + "github.com/go-gost/gost/v3/pkg/hosts" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "github.com/go-gost/gost/v3/pkg/resolver/exchanger" + "github.com/miekg/dns" +) + +const ( + defaultNameserver = "udp://127.0.0.1:53" +) + +func init() { + registry.HandlerRegistry().Register("dns", NewHandler) +} + +type dnsHandler struct { + exchangers []exchanger.Exchanger + cache *resolver_util.Cache + router *chain.Router + hosts hosts.HostMapper + md metadata + options handler.Options +} + +func NewHandler(opts ...handler.Option) handler.Handler { + options := handler.Options{} + for _, opt := range opts { + opt(&options) + } + + return &dnsHandler{ + options: options, + } +} + +func (h *dnsHandler) Init(md md.Metadata) (err error) { + if err = h.parseMetadata(md); err != nil { + return + } + log := h.options.Logger + + h.cache = resolver_util.NewCache().WithLogger(log) + + h.router = h.options.Router + if h.router == nil { + h.router = (&chain.Router{}).WithLogger(log) + } + h.hosts = h.router.Hosts() + + for _, server := range h.md.dns { + server = strings.TrimSpace(server) + if server == "" { + continue + } + ex, err := exchanger.NewExchanger( + server, + exchanger.RouterOption(h.router), + exchanger.TimeoutOption(h.md.timeout), + exchanger.LoggerOption(log), + ) + if err != nil { + log.Warnf("parse %s: %v", server, err) + continue + } + h.exchangers = append(h.exchangers, ex) + } + if len(h.exchangers) == 0 { + ex, err := exchanger.NewExchanger( + defaultNameserver, + exchanger.RouterOption(h.router), + exchanger.TimeoutOption(h.md.timeout), + exchanger.LoggerOption(log), + ) + log.Warnf("resolver not found, default to %s", defaultNameserver) + if err != nil { + return err + } + h.exchangers = append(h.exchangers, ex) + } + + return +} + +func (h *dnsHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error { + defer conn.Close() + + start := time.Now() + log := h.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + }) + + log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) + defer func() { + log.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr()) + }() + + b := bufpool.Get(4096) + defer bufpool.Put(b) + + n, err := conn.Read(*b) + if err != nil { + log.Error(err) + return err + } + + reply, err := h.exchange(ctx, (*b)[:n], log) + if err != nil { + return err + } + defer bufpool.Put(&reply) + + if _, err = conn.Write(reply); err != nil { + log.Error(err) + return err + } + return nil +} + +func (h *dnsHandler) exchange(ctx context.Context, msg []byte, log logger.Logger) ([]byte, error) { + mq := dns.Msg{} + if err := mq.Unpack(msg); err != nil { + log.Error(err) + return nil, err + } + + if len(mq.Question) == 0 { + return nil, errors.New("msg: empty question") + } + + resolver_util.AddSubnetOpt(&mq, h.md.clientIP) + + if log.IsLevelEnabled(logger.DebugLevel) { + log.Debug(mq.String()) + } + + var mr *dns.Msg + + if log.IsLevelEnabled(logger.DebugLevel) { + defer func() { + if mr != nil { + log.Debug(mr.String()) + } + }() + } + + mr = h.lookupHosts(&mq, log) + if mr != nil { + b := bufpool.Get(4096) + return mr.PackBuffer(*b) + } + + // only cache for single question message. + if len(mq.Question) == 1 { + key := resolver_util.NewCacheKey(&mq.Question[0]) + mr = h.cache.Load(key) + if mr != nil { + log.Debugf("exchange message %d (cached): %s", mq.Id, mq.Question[0].String()) + mr.Id = mq.Id + + b := bufpool.Get(4096) + return mr.PackBuffer(*b) + } + + defer func() { + if mr != nil { + h.cache.Store(key, mr, h.md.ttl) + } + }() + } + + b := bufpool.Get(4096) + defer bufpool.Put(b) + + query, err := mq.PackBuffer(*b) + if err != nil { + log.Error(err) + return nil, err + } + + var reply []byte + for _, ex := range h.exchangers { + log.Debugf("exchange message %d via %s: %s", mq.Id, ex.String(), mq.Question[0].String()) + reply, err = ex.Exchange(ctx, query) + if err == nil { + break + } + log.Error(err) + } + if err != nil { + return nil, err + } + + mr = &dns.Msg{} + if err = mr.Unpack(reply); err != nil { + log.Error(err) + return nil, err + } + + return reply, nil +} + +// lookup host mapper +func (h *dnsHandler) lookupHosts(r *dns.Msg, log logger.Logger) (m *dns.Msg) { + if h.hosts == nil || + r.Question[0].Qclass != dns.ClassINET || + (r.Question[0].Qtype != dns.TypeA && r.Question[0].Qtype != dns.TypeAAAA) { + return nil + } + + m = &dns.Msg{} + m.SetReply(r) + + host := strings.TrimSuffix(r.Question[0].Name, ".") + + switch r.Question[0].Qtype { + case dns.TypeA: + ips, _ := h.hosts.Lookup("ip4", host) + if len(ips) == 0 { + return nil + } + log.Debugf("hit host mapper: %s -> %s", host, ips) + + for _, ip := range ips { + rr, err := dns.NewRR(fmt.Sprintf("%s IN A %s\n", r.Question[0].Name, ip.String())) + if err != nil { + log.Error(err) + return nil + } + m.Answer = append(m.Answer, rr) + } + + case dns.TypeAAAA: + ips, _ := h.hosts.Lookup("ip6", host) + if len(ips) == 0 { + return nil + } + log.Debugf("hit host mapper: %s -> %s", host, ips) + + for _, ip := range ips { + rr, err := dns.NewRR(fmt.Sprintf("%s IN AAAA %s\n", r.Question[0].Name, ip.String())) + if err != nil { + log.Error(err) + return nil + } + m.Answer = append(m.Answer, rr) + } + } + + return +} + +func (h *dnsHandler) dumpMsgHeader(m *dns.Msg) string { + buf := new(bytes.Buffer) + buf.WriteString(m.MsgHdr.String() + " ") + buf.WriteString("QUERY: " + strconv.Itoa(len(m.Question)) + ", ") + buf.WriteString("ANSWER: " + strconv.Itoa(len(m.Answer)) + ", ") + buf.WriteString("AUTHORITY: " + strconv.Itoa(len(m.Ns)) + ", ") + buf.WriteString("ADDITIONAL: " + strconv.Itoa(len(m.Extra))) + return buf.String() +} diff --git a/handler/dns/metadata.go b/handler/dns/metadata.go new file mode 100644 index 0000000..73a045f --- /dev/null +++ b/handler/dns/metadata.go @@ -0,0 +1,41 @@ +package dns + +import ( + "net" + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + readTimeout time.Duration + ttl time.Duration + timeout time.Duration + clientIP net.IP + // nameservers + dns []string +} + +func (h *dnsHandler) parseMetadata(md mdata.Metadata) (err error) { + const ( + readTimeout = "readTimeout" + ttl = "ttl" + timeout = "timeout" + clientIP = "clientIP" + dns = "dns" + ) + + h.md.readTimeout = mdata.GetDuration(md, readTimeout) + h.md.ttl = mdata.GetDuration(md, ttl) + h.md.timeout = mdata.GetDuration(md, timeout) + if h.md.timeout <= 0 { + h.md.timeout = 5 * time.Second + } + sip := mdata.GetString(md, clientIP) + if sip != "" { + h.md.clientIP = net.ParseIP(sip) + } + h.md.dns = mdata.GetStrings(md, dns) + + return +} diff --git a/handler/http2/conn.go b/handler/http2/conn.go new file mode 100644 index 0000000..5454565 --- /dev/null +++ b/handler/http2/conn.go @@ -0,0 +1,46 @@ +package http2 + +import ( + "errors" + "io" + "net/http" +) + +type readWriter struct { + r io.Reader + w io.Writer +} + +func (rw *readWriter) Read(p []byte) (n int, err error) { + return rw.r.Read(p) +} + +func (rw *readWriter) Write(p []byte) (n int, err error) { + return rw.w.Write(p) +} + +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/handler/http2/handler.go b/handler/http2/handler.go new file mode 100644 index 0000000..f5cf9cb --- /dev/null +++ b/handler/http2/handler.go @@ -0,0 +1,343 @@ +package http2 + +import ( + "bufio" + "bytes" + "context" + "encoding/base64" + "encoding/binary" + "errors" + "hash/crc32" + "io" + "io/ioutil" + "net" + "net/http" + "net/http/httputil" + "os" + "strconv" + "strings" + "time" + + "github.com/go-gost/gost/v3/pkg/chain" + netpkg "github.com/go-gost/gost/v3/pkg/common/net" + "github.com/go-gost/gost/v3/pkg/handler" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + http2_util "github.com/go-gost/x/internal/util/http2" +) + +func init() { + registry.HandlerRegistry().Register("http2", NewHandler) +} + +type http2Handler struct { + router *chain.Router + md metadata + options handler.Options +} + +func NewHandler(opts ...handler.Option) handler.Handler { + options := handler.Options{} + for _, opt := range opts { + opt(&options) + } + + return &http2Handler{ + options: options, + } +} + +func (h *http2Handler) Init(md md.Metadata) error { + if err := h.parseMetadata(md); err != nil { + return err + } + + h.router = h.options.Router + if h.router == nil { + h.router = (&chain.Router{}).WithLogger(h.options.Logger) + } + + return nil +} + +func (h *http2Handler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error { + defer conn.Close() + + start := time.Now() + log := h.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + }) + log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) + defer func() { + log.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr()) + }() + + cc, ok := conn.(*http2_util.ServerConn) + if !ok { + err := errors.New("wrong connection type") + log.Error(err) + return err + } + return h.roundTrip(ctx, cc.Writer(), cc.Request(), log) +} + +// NOTE: there is an issue (golang/go#43989) will cause the client hangs +// when server returns an non-200 status code, +// May be fixed in go1.18. +func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req *http.Request, log logger.Logger) error { + // Try to get the actual host. + // Compatible with GOST 2.x. + if v := req.Header.Get("Gost-Target"); v != "" { + if h, err := h.decodeServerName(v); err == nil { + req.Host = h + } + } + req.Header.Del("Gost-Target") + + if v := req.Header.Get("X-Gost-Target"); v != "" { + if h, err := h.decodeServerName(v); err == nil { + req.Host = h + } + } + req.Header.Del("X-Gost-Target") + + addr := req.Host + if _, port, _ := net.SplitHostPort(addr); port == "" { + addr = net.JoinHostPort(addr, "80") + } + + fields := map[string]any{ + "dst": addr, + } + if u, _, _ := h.basicProxyAuth(req.Header.Get("Proxy-Authorization")); u != "" { + fields["user"] = u + } + log = log.WithFields(fields) + + if log.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpRequest(req, false) + log.Debug(string(dump)) + } + log.Infof("%s >> %s", req.RemoteAddr, addr) + + for k := range h.md.header { + w.Header().Set(k, h.md.header.Get(k)) + } + + if h.options.Bypass != nil && h.options.Bypass.Contains(addr) { + w.WriteHeader(http.StatusForbidden) + log.Info("bypass: ", addr) + return nil + } + + resp := &http.Response{ + ProtoMajor: 2, + ProtoMinor: 0, + Header: http.Header{}, + Body: ioutil.NopCloser(bytes.NewReader([]byte{})), + } + + if !h.authenticate(w, req, resp, log) { + return nil + } + + // delete the proxy related headers. + req.Header.Del("Proxy-Authorization") + req.Header.Del("Proxy-Connection") + + cc, err := h.router.Dial(ctx, "tcp", addr) + if err != nil { + log.Error(err) + w.WriteHeader(http.StatusServiceUnavailable) + return err + } + defer cc.Close() + + if req.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 && req.ProtoMajor == 1 { + // we take over the underly connection + conn, _, err := hj.Hijack() + if err != nil { + log.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return err + } + defer conn.Close() + + start := time.Now() + log.Infof("%s <-> %s", conn.RemoteAddr(), addr) + netpkg.Transport(conn, cc) + log.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s >-< %s", conn.RemoteAddr(), addr) + + return nil + } + + start := time.Now() + log.Infof("%s <-> %s", req.RemoteAddr, addr) + netpkg.Transport(&readWriter{r: req.Body, w: flushWriter{w}}, cc) + log.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s >-< %s", req.RemoteAddr, addr) + return nil + } + + // TODO: forward request + return nil +} + +func (h *http2Handler) decodeServerName(s string) (string, error) { + b, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + return "", err + } + if len(b) < 4 { + return "", errors.New("invalid name") + } + v, err := base64.RawURLEncoding.DecodeString(string(b[4:])) + if err != nil { + return "", err + } + if crc32.ChecksumIEEE(v) != binary.BigEndian.Uint32(b[:4]) { + return "", errors.New("invalid name") + } + return string(v), nil +} + +func (h *http2Handler) basicProxyAuth(proxyAuth string) (username, password string, ok bool) { + if proxyAuth == "" { + return + } + + if !strings.HasPrefix(proxyAuth, "Basic ") { + return + } + c, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(proxyAuth, "Basic ")) + if err != nil { + return + } + cs := string(c) + s := strings.IndexByte(cs, ':') + if s < 0 { + return + } + + return cs[:s], cs[s+1:], true +} + +func (h *http2Handler) authenticate(w http.ResponseWriter, r *http.Request, resp *http.Response, log logger.Logger) (ok bool) { + u, p, _ := h.basicProxyAuth(r.Header.Get("Proxy-Authorization")) + if h.options.Auther == nil || h.options.Auther.Authenticate(u, p) { + return true + } + + pr := h.md.probeResistance + // probing resistance is enabled, and knocking host is mismatch. + if pr != nil && (pr.Knock == "" || !strings.EqualFold(r.URL.Hostname(), pr.Knock)) { + resp.StatusCode = http.StatusServiceUnavailable // default status code + switch pr.Type { + case "code": + resp.StatusCode, _ = strconv.Atoi(pr.Value) + case "web": + url := pr.Value + if !strings.HasPrefix(url, "http") { + url = "http://" + url + } + r, err := http.Get(url) + if err != nil { + log.Error(err) + break + } + resp = r + defer resp.Body.Close() + case "host": + cc, err := net.Dial("tcp", pr.Value) + if err != nil { + log.Error(err) + break + } + defer cc.Close() + + if err := h.forwardRequest(w, r, cc); err != nil { + log.Error(err) + } + return + case "file": + f, _ := os.Open(pr.Value) + if f != nil { + defer f.Close() + + resp.StatusCode = http.StatusOK + if finfo, _ := f.Stat(); finfo != nil { + resp.ContentLength = finfo.Size() + } + resp.Header.Set("Content-Type", "text/html") + resp.Body = f + } + } + } + + if resp.StatusCode == 0 { + resp.StatusCode = http.StatusProxyAuthRequired + resp.Header.Add("Proxy-Authenticate", "Basic realm=\"gost\"") + if strings.ToLower(r.Header.Get("Proxy-Connection")) == "keep-alive" { + // XXX libcurl will keep sending auth request in same conn + // which we don't supported yet. + resp.Header.Add("Connection", "close") + resp.Header.Add("Proxy-Connection", "close") + } + + log.Info("proxy authentication required") + } else { + resp.Header = http.Header{} + resp.Header.Set("Server", "nginx/1.20.1") + resp.Header.Set("Date", time.Now().Format(http.TimeFormat)) + if resp.StatusCode == http.StatusOK { + resp.Header.Set("Connection", "keep-alive") + } + } + + if log.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpResponse(resp, false) + log.Debug(string(dump)) + } + + h.writeResponse(w, resp) + + return +} +func (h *http2Handler) forwardRequest(w http.ResponseWriter, r *http.Request, rw io.ReadWriter) (err error) { + if err = r.Write(rw); err != nil { + return + } + + resp, err := http.ReadResponse(bufio.NewReader(rw), r) + if err != nil { + return + } + defer resp.Body.Close() + + return h.writeResponse(w, resp) +} + +func (h *http2Handler) writeResponse(w http.ResponseWriter, resp *http.Response) error { + for k, v := range resp.Header { + for _, vv := range v { + w.Header().Add(k, vv) + } + } + w.WriteHeader(resp.StatusCode) + _, err := io.Copy(flushWriter{w}, resp.Body) + return err +} diff --git a/handler/http2/metadata.go b/handler/http2/metadata.go new file mode 100644 index 0000000..5ce2845 --- /dev/null +++ b/handler/http2/metadata.go @@ -0,0 +1,47 @@ +package http2 + +import ( + "net/http" + "strings" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + probeResistance *probeResistance + header http.Header +} + +func (h *http2Handler) parseMetadata(md mdata.Metadata) error { + const ( + header = "header" + probeResistKey = "probeResistance" + knock = "knock" + ) + + if m := mdata.GetStringMapString(md, header); len(m) > 0 { + hd := http.Header{} + for k, v := range m { + hd.Add(k, v) + } + h.md.header = hd + } + + if v := mdata.GetString(md, probeResistKey); v != "" { + if ss := strings.SplitN(v, ":", 2); len(ss) == 2 { + h.md.probeResistance = &probeResistance{ + Type: ss[0], + Value: ss[1], + Knock: mdata.GetString(md, knock), + } + } + } + + return nil +} + +type probeResistance struct { + Type string + Value string + Knock string +} diff --git a/handler/redirect/handler.go b/handler/redirect/handler.go new file mode 100644 index 0000000..263e239 --- /dev/null +++ b/handler/redirect/handler.go @@ -0,0 +1,112 @@ +package redirect + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/go-gost/gost/v3/pkg/chain" + netpkg "github.com/go-gost/gost/v3/pkg/common/net" + "github.com/go-gost/gost/v3/pkg/handler" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" +) + +func init() { + registry.HandlerRegistry().Register("red", NewHandler) + registry.HandlerRegistry().Register("redu", NewHandler) + registry.HandlerRegistry().Register("redir", NewHandler) + registry.HandlerRegistry().Register("redirect", NewHandler) +} + +type redirectHandler struct { + router *chain.Router + md metadata + options handler.Options +} + +func NewHandler(opts ...handler.Option) handler.Handler { + options := handler.Options{} + for _, opt := range opts { + opt(&options) + } + + return &redirectHandler{ + options: options, + } +} + +func (h *redirectHandler) Init(md md.Metadata) (err error) { + if err = h.parseMetadata(md); err != nil { + return + } + + h.router = h.options.Router + if h.router == nil { + h.router = (&chain.Router{}).WithLogger(h.options.Logger) + } + + return +} + +func (h *redirectHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error { + defer conn.Close() + + start := time.Now() + log := h.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + }) + + log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) + defer func() { + log.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr()) + }() + + network := "tcp" + var dstAddr net.Addr + var err error + + if _, ok := conn.(net.PacketConn); ok { + network = "udp" + dstAddr = conn.LocalAddr() + } + + if network == "tcp" { + dstAddr, conn, err = h.getOriginalDstAddr(conn) + if err != nil { + log.Error(err) + return err + } + } + + log = log.WithFields(map[string]any{ + "dst": fmt.Sprintf("%s/%s", dstAddr, network), + }) + + log.Infof("%s >> %s", conn.RemoteAddr(), dstAddr) + + if h.options.Bypass != nil && h.options.Bypass.Contains(dstAddr.String()) { + log.Info("bypass: ", dstAddr) + return nil + } + + cc, err := h.router.Dial(ctx, network, dstAddr.String()) + if err != nil { + log.Error(err) + return err + } + defer cc.Close() + + t := time.Now() + log.Infof("%s <-> %s", conn.RemoteAddr(), dstAddr) + netpkg.Transport(conn, cc) + log.WithFields(map[string]any{ + "duration": time.Since(t), + }).Infof("%s >-< %s", conn.RemoteAddr(), dstAddr) + + return nil +} diff --git a/handler/redirect/handler_linux.go b/handler/redirect/handler_linux.go new file mode 100644 index 0000000..fb9ee42 --- /dev/null +++ b/handler/redirect/handler_linux.go @@ -0,0 +1,40 @@ +package redirect + +import ( + "errors" + "fmt" + "net" + "syscall" +) + +func (h *redirectHandler) getOriginalDstAddr(conn net.Conn) (addr net.Addr, c net.Conn, err error) { + defer conn.Close() + + tc, ok := conn.(*net.TCPConn) + if !ok { + err = errors.New("wrong connection type, must be TCP") + return + } + + fc, err := tc.File() + if err != nil { + return + } + defer fc.Close() + + mreq, err := syscall.GetsockoptIPv6Mreq(int(fc.Fd()), syscall.IPPROTO_IP, 80) + if err != nil { + return + } + + // only ipv4 support + ip := net.IPv4(mreq.Multiaddr[4], mreq.Multiaddr[5], mreq.Multiaddr[6], mreq.Multiaddr[7]) + port := uint16(mreq.Multiaddr[2])<<8 + uint16(mreq.Multiaddr[3]) + addr, err = net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", ip.String(), port)) + if err != nil { + return + } + + c, err = net.FileConn(fc) + return +} diff --git a/handler/redirect/handler_other.go b/handler/redirect/handler_other.go new file mode 100644 index 0000000..58a6f7e --- /dev/null +++ b/handler/redirect/handler_other.go @@ -0,0 +1,15 @@ +//go:build !linux + +package redirect + +import ( + "errors" + "net" +) + +func (h *redirectHandler) getOriginalDstAddr(conn net.Conn) (addr net.Addr, c net.Conn, err error) { + defer conn.Close() + + err = errors.New("TCP redirect is not available on non-linux platform") + return +} diff --git a/handler/redirect/metadata.go b/handler/redirect/metadata.go new file mode 100644 index 0000000..96fb93f --- /dev/null +++ b/handler/redirect/metadata.go @@ -0,0 +1,18 @@ +package redirect + +import ( + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + retryCount int +} + +func (h *redirectHandler) parseMetadata(md mdata.Metadata) (err error) { + const ( + retryCount = "retry" + ) + + h.md.retryCount = mdata.GetInt(md, retryCount) + return +} diff --git a/handler/relay/bind.go b/handler/relay/bind.go new file mode 100644 index 0000000..940da6d --- /dev/null +++ b/handler/relay/bind.go @@ -0,0 +1,193 @@ +package relay + +import ( + "context" + "fmt" + "net" + "time" + + netpkg "github.com/go-gost/gost/v3/pkg/common/net" + net_relay "github.com/go-gost/gost/v3/pkg/common/net/relay" + "github.com/go-gost/gost/v3/pkg/logger" + "github.com/go-gost/relay" + "github.com/go-gost/x/internal/util/mux" + relay_util "github.com/go-gost/x/internal/util/relay" +) + +func (h *relayHandler) handleBind(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) error { + log = log.WithFields(map[string]any{ + "dst": fmt.Sprintf("%s/%s", address, network), + "cmd": "bind", + }) + + log.Infof("%s >> %s", conn.RemoteAddr(), address) + + resp := relay.Response{ + Version: relay.Version1, + Status: relay.StatusOK, + } + + if !h.md.enableBind { + resp.Status = relay.StatusForbidden + log.Error("relay: BIND is disabled") + _, err := resp.WriteTo(conn) + return err + } + + if network == "tcp" { + return h.bindTCP(ctx, conn, network, address, log) + } else { + return h.bindUDP(ctx, conn, network, address, log) + } +} + +func (h *relayHandler) bindTCP(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) error { + resp := relay.Response{ + Version: relay.Version1, + Status: relay.StatusOK, + } + + ln, err := net.Listen(network, address) // strict mode: if the port already in use, it will return error + if err != nil { + log.Error(err) + resp.Status = relay.StatusServiceUnavailable + resp.WriteTo(conn) + return err + } + + af := &relay.AddrFeature{} + err = af.ParseFrom(ln.Addr().String()) + if err != nil { + log.Warn(err) + } + + // Issue: may not reachable when host has multi-interface + af.Host, _, _ = net.SplitHostPort(conn.LocalAddr().String()) + af.AType = relay.AddrIPv4 + resp.Features = append(resp.Features, af) + if _, err := resp.WriteTo(conn); err != nil { + log.Error(err) + ln.Close() + return err + } + + log = log.WithFields(map[string]any{ + "bind": fmt.Sprintf("%s/%s", ln.Addr(), ln.Addr().Network()), + }) + log.Debugf("bind on %s OK", ln.Addr()) + + return h.serveTCPBind(ctx, conn, ln, log) +} + +func (h *relayHandler) bindUDP(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) error { + resp := relay.Response{ + Version: relay.Version1, + Status: relay.StatusOK, + } + + bindAddr, _ := net.ResolveUDPAddr(network, address) + pc, err := net.ListenUDP(network, bindAddr) + if err != nil { + log.Error(err) + return err + } + defer pc.Close() + + af := &relay.AddrFeature{} + err = af.ParseFrom(pc.LocalAddr().String()) + if err != nil { + log.Warn(err) + } + + // Issue: may not reachable when host has multi-interface + af.Host, _, _ = net.SplitHostPort(conn.LocalAddr().String()) + af.AType = relay.AddrIPv4 + resp.Features = append(resp.Features, af) + if _, err := resp.WriteTo(conn); err != nil { + log.Error(err) + return err + } + + log = log.WithFields(map[string]any{ + "bind": pc.LocalAddr().String(), + }) + log.Debugf("bind on %s OK", pc.LocalAddr()) + + r := net_relay.NewUDPRelay(relay_util.UDPTunServerConn(conn), pc). + WithBypass(h.options.Bypass). + WithLogger(log) + r.SetBufferSize(h.md.udpBufferSize) + + t := time.Now() + log.Infof("%s <-> %s", conn.RemoteAddr(), pc.LocalAddr()) + r.Run() + log.WithFields(map[string]any{ + "duration": time.Since(t), + }).Infof("%s >-< %s", conn.RemoteAddr(), pc.LocalAddr()) + return nil +} + +func (h *relayHandler) serveTCPBind(ctx context.Context, conn net.Conn, ln net.Listener, log logger.Logger) error { + // Upgrade connection to multiplex stream. + session, err := mux.ClientSession(conn) + if err != nil { + log.Error(err) + return err + } + defer session.Close() + + go func() { + defer ln.Close() + for { + conn, err := session.Accept() + if err != nil { + log.Error(err) + return + } + conn.Close() // we do not handle incoming connections. + } + }() + + for { + rc, err := ln.Accept() + if err != nil { + log.Error(err) + return err + } + log.Debugf("peer %s accepted", rc.RemoteAddr()) + + go func(c net.Conn) { + defer c.Close() + + log = log.WithFields(map[string]any{ + "local": ln.Addr().String(), + "remote": c.RemoteAddr().String(), + }) + + sc, err := session.GetConn() + if err != nil { + log.Error(err) + return + } + defer sc.Close() + + af := &relay.AddrFeature{} + af.ParseFrom(c.RemoteAddr().String()) + resp := relay.Response{ + Version: relay.Version1, + Status: relay.StatusOK, + Features: []relay.Feature{af}, + } + if _, err := resp.WriteTo(sc); err != nil { + log.Error(err) + return + } + + t := time.Now() + log.Infof("%s <-> %s", c.LocalAddr(), c.RemoteAddr()) + netpkg.Transport(sc, c) + log.WithFields(map[string]any{"duration": time.Since(t)}). + Infof("%s >-< %s", c.LocalAddr(), c.RemoteAddr()) + }(rc) + } +} diff --git a/handler/relay/conn.go b/handler/relay/conn.go new file mode 100644 index 0000000..350ed0a --- /dev/null +++ b/handler/relay/conn.go @@ -0,0 +1,81 @@ +package relay + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + "math" + "net" +) + +type tcpConn struct { + net.Conn + wbuf bytes.Buffer +} + +func (c *tcpConn) Read(b []byte) (n int, err error) { + if err != nil { + return + } + return c.Conn.Read(b) +} + +func (c *tcpConn) Write(b []byte) (n int, err error) { + n = len(b) // force byte length consistent + if c.wbuf.Len() > 0 { + c.wbuf.Write(b) // append the data to the cached header + _, err = c.wbuf.WriteTo(c.Conn) + return + } + _, err = c.Conn.Write(b) + return +} + +type udpConn struct { + net.Conn + wbuf bytes.Buffer +} + +func (c *udpConn) Read(b []byte) (n int, err error) { + var bb [2]byte + _, err = io.ReadFull(c.Conn, bb[:]) + if err != nil { + return + } + + dlen := int(binary.BigEndian.Uint16(bb[:])) + if len(b) >= dlen { + return io.ReadFull(c.Conn, b[:dlen]) + } + buf := make([]byte, dlen) + _, err = io.ReadFull(c.Conn, buf) + n = copy(b, buf) + + return +} + +func (c *udpConn) Write(b []byte) (n int, err error) { + if len(b) > math.MaxUint16 { + err = errors.New("write: data maximum exceeded") + return + } + + n = len(b) + if c.wbuf.Len() > 0 { + var bb [2]byte + binary.BigEndian.PutUint16(bb[:], uint16(len(b))) + c.wbuf.Write(bb[:]) + c.wbuf.Write(b) // append the data to the cached header + _, err = c.wbuf.WriteTo(c.Conn) + return + } + + var bb [2]byte + binary.BigEndian.PutUint16(bb[:], uint16(len(b))) + _, err = c.Conn.Write(bb[:]) + if err != nil { + return + } + return c.Conn.Write(b) +} diff --git a/handler/relay/connect.go b/handler/relay/connect.go new file mode 100644 index 0000000..7fedad5 --- /dev/null +++ b/handler/relay/connect.go @@ -0,0 +1,91 @@ +package relay + +import ( + "context" + "errors" + "fmt" + "net" + "time" + + netpkg "github.com/go-gost/gost/v3/pkg/common/net" + "github.com/go-gost/gost/v3/pkg/logger" + "github.com/go-gost/relay" +) + +func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) error { + log = log.WithFields(map[string]any{ + "dst": fmt.Sprintf("%s/%s", address, network), + "cmd": "connect", + }) + + log.Infof("%s >> %s", conn.RemoteAddr(), address) + + resp := relay.Response{ + Version: relay.Version1, + Status: relay.StatusOK, + } + + if address == "" { + resp.Status = relay.StatusBadRequest + resp.WriteTo(conn) + err := errors.New("target not specified") + log.Error(err) + return err + } + + if h.options.Bypass != nil && h.options.Bypass.Contains(address) { + log.Info("bypass: ", address) + resp.Status = relay.StatusForbidden + _, err := resp.WriteTo(conn) + return err + } + + cc, err := h.router.Dial(ctx, network, address) + if err != nil { + resp.Status = relay.StatusNetworkUnreachable + resp.WriteTo(conn) + return err + } + defer cc.Close() + + if h.md.noDelay { + if _, err := resp.WriteTo(conn); err != nil { + log.Error(err) + return err + } + } + + switch network { + case "udp", "udp4", "udp6": + rc := &udpConn{ + Conn: conn, + } + if !h.md.noDelay { + // cache the header + if _, err := resp.WriteTo(&rc.wbuf); err != nil { + return err + } + } + conn = rc + default: + rc := &tcpConn{ + Conn: conn, + } + if !h.md.noDelay { + // cache the header + if _, err := resp.WriteTo(&rc.wbuf); err != nil { + return err + } + } + conn = rc + } + + t := time.Now() + log.Infof("%s <-> %s", conn.RemoteAddr(), address) + netpkg.Transport(conn, cc) + log.WithFields(map[string]any{ + "duration": time.Since(t), + }).Infof("%s >-< %s", conn.RemoteAddr(), address) + + return nil +} diff --git a/handler/relay/forward.go b/handler/relay/forward.go new file mode 100644 index 0000000..472bcd1 --- /dev/null +++ b/handler/relay/forward.go @@ -0,0 +1,91 @@ +package relay + +import ( + "context" + "errors" + "fmt" + "net" + "time" + + netpkg "github.com/go-gost/gost/v3/pkg/common/net" + "github.com/go-gost/gost/v3/pkg/logger" + "github.com/go-gost/relay" +) + +func (h *relayHandler) handleForward(ctx context.Context, conn net.Conn, network string, log logger.Logger) error { + resp := relay.Response{ + Version: relay.Version1, + Status: relay.StatusOK, + } + target := h.group.Next() + if target == nil { + resp.Status = relay.StatusServiceUnavailable + resp.WriteTo(conn) + err := errors.New("target not available") + log.Error(err) + return err + } + + log = log.WithFields(map[string]any{ + "dst": fmt.Sprintf("%s/%s", target.Addr, network), + "cmd": "forward", + }) + + log.Infof("%s >> %s", conn.RemoteAddr(), target.Addr) + + cc, err := h.router.Dial(ctx, network, target.Addr) + if err != nil { + // TODO: the router itself may be failed due to the failed node in the router, + // the dead marker may be a wrong operation. + target.Marker.Mark() + + resp.Status = relay.StatusHostUnreachable + resp.WriteTo(conn) + log.Error(err) + + return err + } + defer cc.Close() + target.Marker.Reset() + + if h.md.noDelay { + if _, err := resp.WriteTo(conn); err != nil { + log.Error(err) + return err + } + } + + switch network { + case "udp", "udp4", "udp6": + rc := &udpConn{ + Conn: conn, + } + if !h.md.noDelay { + // cache the header + if _, err := resp.WriteTo(&rc.wbuf); err != nil { + return err + } + } + conn = rc + default: + rc := &tcpConn{ + Conn: conn, + } + if !h.md.noDelay { + // cache the header + if _, err := resp.WriteTo(&rc.wbuf); err != nil { + return err + } + } + conn = rc + } + + t := time.Now() + log.Infof("%s <-> %s", conn.RemoteAddr(), target.Addr) + netpkg.Transport(conn, cc) + log.WithFields(map[string]any{ + "duration": time.Since(t), + }).Infof("%s >-< %s", conn.RemoteAddr(), target.Addr) + + return nil +} diff --git a/handler/relay/handler.go b/handler/relay/handler.go new file mode 100644 index 0000000..9087b10 --- /dev/null +++ b/handler/relay/handler.go @@ -0,0 +1,147 @@ +package relay + +import ( + "context" + "errors" + "net" + "strconv" + "time" + + "github.com/go-gost/gost/v3/pkg/chain" + "github.com/go-gost/gost/v3/pkg/handler" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "github.com/go-gost/relay" +) + +var ( + ErrBadVersion = errors.New("relay: bad version") + ErrUnknownCmd = errors.New("relay: unknown command") +) + +func init() { + registry.HandlerRegistry().Register("relay", NewHandler) +} + +type relayHandler struct { + group *chain.NodeGroup + router *chain.Router + md metadata + options handler.Options +} + +func NewHandler(opts ...handler.Option) handler.Handler { + options := handler.Options{} + for _, opt := range opts { + opt(&options) + } + + return &relayHandler{ + options: options, + } +} + +func (h *relayHandler) Init(md md.Metadata) (err error) { + if err := h.parseMetadata(md); err != nil { + return err + } + + h.router = h.options.Router + if h.router == nil { + h.router = (&chain.Router{}).WithLogger(h.options.Logger) + } + + return nil +} + +// Forward implements handler.Forwarder. +func (h *relayHandler) Forward(group *chain.NodeGroup) { + h.group = group +} + +func (h *relayHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error { + defer conn.Close() + + start := time.Now() + log := h.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + }) + + log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) + defer func() { + log.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr()) + }() + + if h.md.readTimeout > 0 { + conn.SetReadDeadline(time.Now().Add(h.md.readTimeout)) + } + + req := relay.Request{} + if _, err := req.ReadFrom(conn); err != nil { + log.Error(err) + return err + } + + conn.SetReadDeadline(time.Time{}) + + if req.Version != relay.Version1 { + err := ErrBadVersion + log.Error(err) + return err + } + + var user, pass string + var address string + for _, f := range req.Features { + if f.Type() == relay.FeatureUserAuth { + feature := f.(*relay.UserAuthFeature) + user, pass = feature.Username, feature.Password + } + if f.Type() == relay.FeatureAddr { + feature := f.(*relay.AddrFeature) + address = net.JoinHostPort(feature.Host, strconv.Itoa(int(feature.Port))) + } + } + + if user != "" { + log = log.WithFields(map[string]any{"user": user}) + } + + resp := relay.Response{ + Version: relay.Version1, + Status: relay.StatusOK, + } + if h.options.Auther != nil && !h.options.Auther.Authenticate(user, pass) { + resp.Status = relay.StatusUnauthorized + log.Error("unauthorized") + _, err := resp.WriteTo(conn) + return err + } + + network := "tcp" + if (req.Flags & relay.FUDP) == relay.FUDP { + network = "udp" + } + + if h.group != nil { + if address != "" { + resp.Status = relay.StatusForbidden + log.Error("forward mode, connect is forbidden") + _, err := resp.WriteTo(conn) + return err + } + // forward mode + return h.handleForward(ctx, conn, network, log) + } + + switch req.Flags & relay.CmdMask { + case 0, relay.CONNECT: + return h.handleConnect(ctx, conn, network, address, log) + case relay.BIND: + return h.handleBind(ctx, conn, network, address, log) + } + return ErrUnknownCmd +} diff --git a/handler/relay/metadata.go b/handler/relay/metadata.go new file mode 100644 index 0000000..e56d070 --- /dev/null +++ b/handler/relay/metadata.go @@ -0,0 +1,35 @@ +package relay + +import ( + "math" + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + readTimeout time.Duration + enableBind bool + udpBufferSize int + noDelay bool +} + +func (h *relayHandler) parseMetadata(md mdata.Metadata) (err error) { + const ( + readTimeout = "readTimeout" + enableBind = "bind" + udpBufferSize = "udpBufferSize" + noDelay = "nodelay" + ) + + h.md.readTimeout = mdata.GetDuration(md, readTimeout) + h.md.enableBind = mdata.GetBool(md, enableBind) + h.md.noDelay = mdata.GetBool(md, noDelay) + + if bs := mdata.GetInt(md, udpBufferSize); bs > 0 { + h.md.udpBufferSize = int(math.Min(math.Max(float64(bs), 512), 64*1024)) + } else { + h.md.udpBufferSize = 1024 + } + return +} diff --git a/handler/sni/conn.go b/handler/sni/conn.go new file mode 100644 index 0000000..22d1035 --- /dev/null +++ b/handler/sni/conn.go @@ -0,0 +1,20 @@ +package sni + +import ( + "net" +) + +type cacheConn struct { + net.Conn + buf []byte +} + +func (c *cacheConn) Read(b []byte) (n int, err error) { + if len(c.buf) > 0 { + n = copy(b, c.buf) + c.buf = c.buf[n:] + return + } + + return c.Conn.Read(b) +} diff --git a/handler/sni/handler.go b/handler/sni/handler.go new file mode 100644 index 0000000..dfd3a2b --- /dev/null +++ b/handler/sni/handler.go @@ -0,0 +1,222 @@ +package sni + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/binary" + "errors" + "hash/crc32" + "io" + "net" + "time" + + "github.com/go-gost/gost/v3/pkg/chain" + "github.com/go-gost/gost/v3/pkg/common/bufpool" + netpkg "github.com/go-gost/gost/v3/pkg/common/net" + "github.com/go-gost/gost/v3/pkg/handler" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + dissector "github.com/go-gost/tls-dissector" +) + +func init() { + registry.HandlerRegistry().Register("sni", NewHandler) +} + +type sniHandler struct { + httpHandler handler.Handler + router *chain.Router + md metadata + options handler.Options +} + +func NewHandler(opts ...handler.Option) handler.Handler { + options := handler.Options{} + for _, opt := range opts { + opt(&options) + } + + h := &sniHandler{ + options: options, + } + + if f := registry.HandlerRegistry().Get("http"); f != nil { + v := append(opts, + handler.LoggerOption(h.options.Logger.WithFields(map[string]any{"type": "http"}))) + h.httpHandler = f(v...) + } + + return h +} + +func (h *sniHandler) Init(md md.Metadata) (err error) { + if err = h.parseMetadata(md); err != nil { + return + } + if h.httpHandler != nil { + if md != nil { + md.Set("sni", true) + } + if err = h.httpHandler.Init(md); err != nil { + return + } + } + + h.router = h.options.Router + if h.router == nil { + h.router = (&chain.Router{}).WithLogger(h.options.Logger) + } + + return nil +} + +func (h *sniHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error { + defer conn.Close() + + start := time.Now() + log := h.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + }) + + log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) + defer func() { + log.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr()) + }() + + var hdr [dissector.RecordHeaderLen]byte + if _, err := io.ReadFull(conn, hdr[:]); err != nil { + log.Error(err) + return err + } + + if hdr[0] != dissector.Handshake { + // We assume it is an HTTP request + conn = &cacheConn{ + Conn: conn, + buf: hdr[:], + } + + if h.httpHandler != nil { + return h.httpHandler.Handle(ctx, conn) + } + return nil + } + + length := binary.BigEndian.Uint16(hdr[3:5]) + + buf := bufpool.Get(int(length) + dissector.RecordHeaderLen) + defer bufpool.Put(buf) + if _, err := io.ReadFull(conn, (*buf)[dissector.RecordHeaderLen:]); err != nil { + log.Error(err) + return err + } + copy(*buf, hdr[:]) + + opaque, host, err := h.decodeHost(bytes.NewReader(*buf)) + if err != nil { + log.Error(err) + return err + } + target := net.JoinHostPort(host, "443") + + log = log.WithFields(map[string]any{ + "dst": target, + }) + log.Infof("%s >> %s", conn.RemoteAddr(), target) + + if h.options.Bypass != nil && h.options.Bypass.Contains(target) { + log.Info("bypass: ", target) + return nil + } + + cc, err := h.router.Dial(ctx, "tcp", target) + if err != nil { + log.Error(err) + return err + } + defer cc.Close() + + if _, err := cc.Write(opaque); err != nil { + log.Error(err) + return err + } + + t := time.Now() + log.Infof("%s <-> %s", conn.RemoteAddr(), target) + netpkg.Transport(conn, cc) + log.WithFields(map[string]any{ + "duration": time.Since(t), + }).Infof("%s >-< %s", conn.RemoteAddr(), target) + + return nil +} + +func (h *sniHandler) decodeHost(r io.Reader) (opaque []byte, host string, err error) { + record, err := dissector.ReadRecord(r) + if err != nil { + return + } + clientHello := dissector.ClientHelloMsg{} + if err = clientHello.Decode(record.Opaque); err != nil { + return + } + + var extensions []dissector.Extension + for _, ext := range clientHello.Extensions { + if ext.Type() == 0xFFFE { + b, _ := ext.Encode() + if v, err := h.decodeServerName(string(b)); err == nil { + host = v + } + continue + } + extensions = append(extensions, ext) + } + clientHello.Extensions = extensions + + for _, ext := range clientHello.Extensions { + if ext.Type() == dissector.ExtServerName { + snExtension := ext.(*dissector.ServerNameExtension) + if host == "" { + host = snExtension.Name + } else { + snExtension.Name = host + } + break + } + } + + record.Opaque, err = clientHello.Encode() + if err != nil { + return + } + + buf := &bytes.Buffer{} + if _, err = record.WriteTo(buf); err != nil { + return + } + opaque = buf.Bytes() + return +} + +func (h *sniHandler) decodeServerName(s string) (string, error) { + b, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + return "", err + } + if len(b) < 4 { + return "", errors.New("invalid name") + } + v, err := base64.RawURLEncoding.DecodeString(string(b[4:])) + if err != nil { + return "", err + } + if crc32.ChecksumIEEE(v) != binary.BigEndian.Uint32(b[:4]) { + return "", errors.New("invalid name") + } + return string(v), nil +} diff --git a/handler/sni/metadata.go b/handler/sni/metadata.go new file mode 100644 index 0000000..c50f576 --- /dev/null +++ b/handler/sni/metadata.go @@ -0,0 +1,20 @@ +package sni + +import ( + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + readTimeout time.Duration +} + +func (h *sniHandler) parseMetadata(md mdata.Metadata) (err error) { + const ( + readTimeout = "readTimeout" + ) + + h.md.readTimeout = mdata.GetDuration(md, readTimeout) + return +} diff --git a/handler/ss/handler.go b/handler/ss/handler.go new file mode 100644 index 0000000..c04ece0 --- /dev/null +++ b/handler/ss/handler.go @@ -0,0 +1,119 @@ +package ss + +import ( + "context" + "io" + "io/ioutil" + "net" + "time" + + "github.com/go-gost/gosocks5" + "github.com/go-gost/gost/v3/pkg/chain" + netpkg "github.com/go-gost/gost/v3/pkg/common/net" + "github.com/go-gost/gost/v3/pkg/handler" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "github.com/go-gost/x/internal/util/ss" + "github.com/shadowsocks/go-shadowsocks2/core" +) + +func init() { + registry.HandlerRegistry().Register("ss", NewHandler) +} + +type ssHandler struct { + cipher core.Cipher + router *chain.Router + md metadata + options handler.Options +} + +func NewHandler(opts ...handler.Option) handler.Handler { + options := handler.Options{} + for _, opt := range opts { + opt(&options) + } + + return &ssHandler{ + options: options, + } +} + +func (h *ssHandler) Init(md md.Metadata) (err error) { + if err = h.parseMetadata(md); err != nil { + return + } + if h.options.Auth != nil { + method := h.options.Auth.Username() + password, _ := h.options.Auth.Password() + h.cipher, err = ss.ShadowCipher(method, password, h.md.key) + if err != nil { + return + } + } + + h.router = h.options.Router + if h.router == nil { + h.router = (&chain.Router{}).WithLogger(h.options.Logger) + } + + return +} + +func (h *ssHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error { + defer conn.Close() + + start := time.Now() + log := h.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + }) + + log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) + defer func() { + log.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr()) + }() + + if h.cipher != nil { + conn = ss.ShadowConn(h.cipher.StreamConn(conn), nil) + } + + if h.md.readTimeout > 0 { + conn.SetReadDeadline(time.Now().Add(h.md.readTimeout)) + } + + addr := &gosocks5.Addr{} + if _, err := addr.ReadFrom(conn); err != nil { + log.Error(err) + io.Copy(ioutil.Discard, conn) + return err + } + + log = log.WithFields(map[string]any{ + "dst": addr.String(), + }) + + log.Infof("%s >> %s", conn.RemoteAddr(), addr) + + if h.options.Bypass != nil && h.options.Bypass.Contains(addr.String()) { + log.Info("bypass: ", addr.String()) + return nil + } + + cc, err := h.router.Dial(ctx, "tcp", addr.String()) + if err != nil { + return err + } + defer cc.Close() + + t := time.Now() + log.Infof("%s <-> %s", conn.RemoteAddr(), addr) + netpkg.Transport(conn, cc) + log.WithFields(map[string]any{ + "duration": time.Since(t), + }).Infof("%s >-< %s", conn.RemoteAddr(), addr) + + return nil +} diff --git a/handler/ss/metadata.go b/handler/ss/metadata.go new file mode 100644 index 0000000..6aa31f9 --- /dev/null +++ b/handler/ss/metadata.go @@ -0,0 +1,24 @@ +package ss + +import ( + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + key string + readTimeout time.Duration +} + +func (h *ssHandler) parseMetadata(md mdata.Metadata) (err error) { + const ( + key = "key" + readTimeout = "readTimeout" + ) + + h.md.key = mdata.GetString(md, key) + h.md.readTimeout = mdata.GetDuration(md, readTimeout) + + return +} diff --git a/handler/ss/udp/handler.go b/handler/ss/udp/handler.go new file mode 100644 index 0000000..b986512 --- /dev/null +++ b/handler/ss/udp/handler.go @@ -0,0 +1,188 @@ +package ss + +import ( + "context" + "errors" + "net" + "time" + + "github.com/go-gost/gost/v3/pkg/chain" + "github.com/go-gost/gost/v3/pkg/common/bufpool" + "github.com/go-gost/gost/v3/pkg/handler" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "github.com/go-gost/x/internal/util/relay" + "github.com/go-gost/x/internal/util/ss" + "github.com/shadowsocks/go-shadowsocks2/core" +) + +func init() { + registry.HandlerRegistry().Register("ssu", NewHandler) +} + +type ssuHandler struct { + cipher core.Cipher + router *chain.Router + md metadata + options handler.Options +} + +func NewHandler(opts ...handler.Option) handler.Handler { + options := handler.Options{} + for _, opt := range opts { + opt(&options) + } + + return &ssuHandler{ + options: options, + } +} + +func (h *ssuHandler) Init(md md.Metadata) (err error) { + if err = h.parseMetadata(md); err != nil { + return + } + + if h.options.Auth != nil { + method := h.options.Auth.Username() + password, _ := h.options.Auth.Password() + h.cipher, err = ss.ShadowCipher(method, password, h.md.key) + if err != nil { + return + } + } + + h.router = h.options.Router + if h.router == nil { + h.router = (&chain.Router{}).WithLogger(h.options.Logger) + } + + return +} + +func (h *ssuHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error { + defer conn.Close() + + start := time.Now() + log := h.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + }) + + log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) + defer func() { + log.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr()) + }() + + pc, ok := conn.(net.PacketConn) + if ok { + if h.cipher != nil { + pc = h.cipher.PacketConn(pc) + } + // standard UDP relay. + pc = ss.UDPServerConn(pc, conn.RemoteAddr(), h.md.bufferSize) + } else { + if h.cipher != nil { + conn = ss.ShadowConn(h.cipher.StreamConn(conn), nil) + } + // UDP over TCP + pc = relay.UDPTunServerConn(conn) + } + + // obtain a udp connection + c, err := h.router.Dial(ctx, "udp", "") // UDP association + if err != nil { + log.Error(err) + return err + } + defer c.Close() + + cc, ok := c.(net.PacketConn) + if !ok { + err := errors.New("ss: wrong connection type") + log.Error(err) + return err + } + + t := time.Now() + log.Infof("%s <-> %s", conn.LocalAddr(), cc.LocalAddr()) + h.relayPacket(pc, cc, log) + log.WithFields(map[string]any{"duration": time.Since(t)}). + Infof("%s >-< %s", conn.LocalAddr(), cc.LocalAddr()) + + return nil +} + +func (h *ssuHandler) relayPacket(pc1, pc2 net.PacketConn, log logger.Logger) (err error) { + bufSize := h.md.bufferSize + errc := make(chan error, 2) + + go func() { + for { + err := func() error { + b := bufpool.Get(bufSize) + defer bufpool.Put(b) + + n, addr, err := pc1.ReadFrom(*b) + if err != nil { + return err + } + + if h.options.Bypass != nil && h.options.Bypass.Contains(addr.String()) { + log.Warn("bypass: ", addr) + return nil + } + + if _, err = pc2.WriteTo((*b)[:n], addr); err != nil { + return err + } + + log.Debugf("%s >>> %s data: %d", + pc2.LocalAddr(), addr, n) + return nil + }() + + if err != nil { + errc <- err + return + } + } + }() + + go func() { + for { + err := func() error { + b := bufpool.Get(bufSize) + defer bufpool.Put(b) + + n, raddr, err := pc2.ReadFrom(*b) + if err != nil { + return err + } + + if h.options.Bypass != nil && h.options.Bypass.Contains(raddr.String()) { + log.Warn("bypass: ", raddr) + return nil + } + + if _, err = pc1.WriteTo((*b)[:n], raddr); err != nil { + return err + } + + log.Debugf("%s <<< %s data: %d", + pc2.LocalAddr(), raddr, n) + return nil + }() + + if err != nil { + errc <- err + return + } + } + }() + + return <-errc +} diff --git a/handler/ss/udp/metadata.go b/handler/ss/udp/metadata.go new file mode 100644 index 0000000..532c74c --- /dev/null +++ b/handler/ss/udp/metadata.go @@ -0,0 +1,32 @@ +package ss + +import ( + "math" + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + key string + readTimeout time.Duration + bufferSize int +} + +func (h *ssuHandler) parseMetadata(md mdata.Metadata) (err error) { + const ( + key = "key" + readTimeout = "readTimeout" + bufferSize = "bufferSize" + ) + + h.md.key = mdata.GetString(md, key) + h.md.readTimeout = mdata.GetDuration(md, readTimeout) + + if bs := mdata.GetInt(md, bufferSize); bs > 0 { + h.md.bufferSize = int(math.Min(math.Max(float64(bs), 512), 64*1024)) + } else { + h.md.bufferSize = 1024 + } + return +} diff --git a/handler/sshd/handler.go b/handler/sshd/handler.go new file mode 100644 index 0000000..fca384b --- /dev/null +++ b/handler/sshd/handler.go @@ -0,0 +1,245 @@ +package ssh + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "net" + "strconv" + "time" + + "github.com/go-gost/gost/v3/pkg/chain" + netpkg "github.com/go-gost/gost/v3/pkg/common/net" + "github.com/go-gost/gost/v3/pkg/handler" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + sshd_util "github.com/go-gost/x/internal/util/sshd" + "golang.org/x/crypto/ssh" +) + +// Applicable SSH Request types for Port Forwarding - RFC 4254 7.X +const ( + ForwardedTCPReturnRequest = "forwarded-tcpip" // RFC 4254 7.2 +) + +func init() { + registry.HandlerRegistry().Register("sshd", NewHandler) +} + +type forwardHandler struct { + router *chain.Router + md metadata + options handler.Options +} + +func NewHandler(opts ...handler.Option) handler.Handler { + options := handler.Options{} + for _, opt := range opts { + opt(&options) + } + + return &forwardHandler{ + options: options, + } +} + +func (h *forwardHandler) Init(md md.Metadata) (err error) { + if err = h.parseMetadata(md); err != nil { + return + } + + h.router = h.options.Router + if h.router == nil { + h.router = (&chain.Router{}).WithLogger(h.options.Logger) + } + + return nil +} + +func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error { + defer conn.Close() + + log := h.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + }) + + switch cc := conn.(type) { + case *sshd_util.DirectForwardConn: + return h.handleDirectForward(ctx, cc, log) + case *sshd_util.RemoteForwardConn: + return h.handleRemoteForward(ctx, cc, log) + default: + err := errors.New("sshd: wrong connection type") + log.Error(err) + return err + } +} + +func (h *forwardHandler) handleDirectForward(ctx context.Context, conn *sshd_util.DirectForwardConn, log logger.Logger) error { + targetAddr := conn.DstAddr() + + log = log.WithFields(map[string]any{ + "dst": fmt.Sprintf("%s/%s", targetAddr, "tcp"), + "cmd": "connect", + }) + + log.Infof("%s >> %s", conn.RemoteAddr(), targetAddr) + + if h.options.Bypass != nil && h.options.Bypass.Contains(targetAddr) { + log.Infof("bypass %s", targetAddr) + return nil + } + + cc, err := h.router.Dial(ctx, "tcp", targetAddr) + if err != nil { + return err + } + defer cc.Close() + + t := time.Now() + log.Infof("%s <-> %s", cc.LocalAddr(), targetAddr) + netpkg.Transport(conn, cc) + log.WithFields(map[string]any{ + "duration": time.Since(t), + }).Infof("%s >-< %s", cc.LocalAddr(), targetAddr) + + return nil +} + +func (h *forwardHandler) handleRemoteForward(ctx context.Context, conn *sshd_util.RemoteForwardConn, log logger.Logger) error { + req := conn.Request() + + t := tcpipForward{} + if err := ssh.Unmarshal(req.Payload, &t); err != nil { + log.Error(err) + return err + } + + network := "tcp" + addr := net.JoinHostPort(t.Host, strconv.Itoa(int(t.Port))) + + log = log.WithFields(map[string]any{ + "dst": fmt.Sprintf("%s/%s", addr, network), + "cmd": "bind", + }) + + log.Infof("%s >> %s", conn.RemoteAddr(), addr) + + // tie to the client connection + ln, err := net.Listen(network, addr) + if err != nil { + log.Error(err) + req.Reply(false, nil) + return err + } + defer ln.Close() + + log = log.WithFields(map[string]any{ + "bind": fmt.Sprintf("%s/%s", ln.Addr(), ln.Addr().Network()), + }) + log.Debugf("bind on %s OK", ln.Addr()) + + err = func() error { + if t.Port == 0 && req.WantReply { // Client sent port 0. let them know which port is actually being used + _, port, err := getHostPortFromAddr(ln.Addr()) + if err != nil { + return err + } + var b [4]byte + binary.BigEndian.PutUint32(b[:], uint32(port)) + t.Port = uint32(port) + return req.Reply(true, b[:]) + } + return req.Reply(true, nil) + }() + if err != nil { + log.Error(err) + return err + } + + sshConn := conn.Conn() + + go func() { + for { + cc, err := ln.Accept() + if err != nil { // Unable to accept new connection - listener is likely closed + return + } + + go func(conn net.Conn) { + defer conn.Close() + + log := log.WithFields(map[string]any{ + "local": conn.LocalAddr().String(), + "remote": conn.RemoteAddr().String(), + }) + + p := directForward{} + var err error + + var portnum int + p.Host1 = t.Host + p.Port1 = t.Port + p.Host2, portnum, err = getHostPortFromAddr(conn.RemoteAddr()) + if err != nil { + return + } + + p.Port2 = uint32(portnum) + ch, reqs, err := sshConn.OpenChannel(ForwardedTCPReturnRequest, ssh.Marshal(p)) + if err != nil { + log.Error("open forwarded channel: ", err) + return + } + defer ch.Close() + go ssh.DiscardRequests(reqs) + + t := time.Now() + log.Infof("%s <-> %s", conn.LocalAddr(), conn.RemoteAddr()) + netpkg.Transport(ch, conn) + log.WithFields(map[string]any{ + "duration": time.Since(t), + }).Infof("%s >-< %s", conn.LocalAddr(), conn.RemoteAddr()) + }(cc) + } + }() + + tm := time.Now() + log.Infof("%s <-> %s", conn.RemoteAddr(), addr) + <-conn.Done() + log.WithFields(map[string]any{ + "duration": time.Since(tm), + }).Infof("%s >-< %s", conn.RemoteAddr(), addr) + + return nil +} + +func getHostPortFromAddr(addr net.Addr) (host string, port int, err error) { + host, portString, err := net.SplitHostPort(addr.String()) + if err != nil { + return + } + port, err = strconv.Atoi(portString) + return +} + +// directForward is structure for RFC 4254 7.2 - can be used for "forwarded-tcpip" and "direct-tcpip" +type directForward struct { + Host1 string + Port1 uint32 + Host2 string + Port2 uint32 +} + +func (p directForward) String() string { + return fmt.Sprintf("%s:%d -> %s:%d", p.Host2, p.Port2, p.Host1, p.Port1) +} + +// tcpipForward is structure for RFC 4254 7.1 "tcpip-forward" request +type tcpipForward struct { + Host string + Port uint32 +} diff --git a/handler/sshd/metadata.go b/handler/sshd/metadata.go new file mode 100644 index 0000000..8c7eef2 --- /dev/null +++ b/handler/sshd/metadata.go @@ -0,0 +1,12 @@ +package ssh + +import ( + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { +} + +func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) { + return +} diff --git a/handler/tap/handler.go b/handler/tap/handler.go new file mode 100644 index 0000000..d492e36 --- /dev/null +++ b/handler/tap/handler.go @@ -0,0 +1,341 @@ +package tap + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "os" + "sync" + "time" + + "github.com/go-gost/gost/v3/pkg/chain" + "github.com/go-gost/gost/v3/pkg/common/bufpool" + "github.com/go-gost/gost/v3/pkg/handler" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "github.com/go-gost/x/internal/util/ss" + tap_util "github.com/go-gost/x/internal/util/tap" + "github.com/shadowsocks/go-shadowsocks2/core" + "github.com/shadowsocks/go-shadowsocks2/shadowaead" + "github.com/songgao/water/waterutil" +) + +func init() { + registry.HandlerRegistry().Register("tap", NewHandler) +} + +type tapHandler struct { + group *chain.NodeGroup + routes sync.Map + exit chan struct{} + cipher core.Cipher + router *chain.Router + md metadata + options handler.Options +} + +func NewHandler(opts ...handler.Option) handler.Handler { + options := handler.Options{} + for _, opt := range opts { + opt(&options) + } + + return &tapHandler{ + exit: make(chan struct{}, 1), + options: options, + } +} + +func (h *tapHandler) Init(md md.Metadata) (err error) { + if err = h.parseMetadata(md); err != nil { + return + } + + if h.options.Auth != nil { + method := h.options.Auth.Username() + password, _ := h.options.Auth.Password() + h.cipher, err = ss.ShadowCipher(method, password, h.md.key) + if err != nil { + return + } + } + + h.router = h.options.Router + if h.router == nil { + h.router = (&chain.Router{}).WithLogger(h.options.Logger) + } + + return +} + +// Forward implements handler.Forwarder. +func (h *tapHandler) Forward(group *chain.NodeGroup) { + h.group = group +} + +func (h *tapHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error { + defer os.Exit(0) + defer conn.Close() + + log := h.options.Logger + cc, ok := conn.(*tap_util.Conn) + if !ok || cc.Config() == nil { + err := errors.New("tap: wrong connection type") + log.Error(err) + return err + } + + start := time.Now() + log = log.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + }) + + log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) + defer func() { + log.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr()) + }() + + network := "udp" + var raddr net.Addr + var err error + + target := h.group.Next() + if target != nil { + raddr, err = net.ResolveUDPAddr(network, target.Addr) + if err != nil { + log.Error(err) + return err + } + log = log.WithFields(map[string]any{ + "dst": fmt.Sprintf("%s/%s", raddr.String(), raddr.Network()), + }) + log.Infof("%s >> %s", conn.RemoteAddr(), target.Addr) + } + + h.handleLoop(ctx, conn, raddr, cc.Config(), log) + return nil +} + +func (h *tapHandler) handleLoop(ctx context.Context, conn net.Conn, addr net.Addr, config *tap_util.Config, log logger.Logger) { + var tempDelay time.Duration + for { + err := func() error { + var err error + var pc net.PacketConn + + if addr != nil { + cc, err := h.router.Dial(ctx, addr.Network(), "") + if err != nil { + return err + } + + var ok bool + pc, ok = cc.(net.PacketConn) + if !ok { + return errors.New("wrong connection type") + } + } else { + laddr, _ := net.ResolveUDPAddr("udp", conn.LocalAddr().String()) + pc, err = net.ListenUDP("udp", laddr) + } + if err != nil { + return err + } + + if h.cipher != nil { + pc = h.cipher.PacketConn(pc) + } + defer pc.Close() + + return h.transport(conn, pc, addr, config, log) + }() + if err != nil { + log.Error(err) + } + + select { + case <-h.exit: + return + default: + } + + if err != nil { + if tempDelay == 0 { + tempDelay = 1000 * time.Millisecond + } else { + tempDelay *= 2 + } + if max := 6 * time.Second; tempDelay > max { + tempDelay = max + } + time.Sleep(tempDelay) + continue + } + tempDelay = 0 + } + +} + +func (h *tapHandler) transport(tap net.Conn, conn net.PacketConn, raddr net.Addr, config *tap_util.Config, log logger.Logger) error { + errc := make(chan error, 1) + + go func() { + for { + err := func() error { + b := bufpool.Get(h.md.bufferSize) + defer bufpool.Put(b) + + n, err := tap.Read(*b) + if err != nil { + select { + case h.exit <- struct{}{}: + default: + } + return err + } + + src := waterutil.MACSource((*b)[:n]) + dst := waterutil.MACDestination((*b)[:n]) + eType := etherType(waterutil.MACEthertype((*b)[:n])) + + log.Debugf("%s >> %s %s %d", src, dst, eType, n) + + // client side, deliver frame directly. + if raddr != nil { + _, err := conn.WriteTo((*b)[:n], raddr) + return err + } + + // server side, broadcast. + if waterutil.IsBroadcast(dst) { + go h.routes.Range(func(k, v any) bool { + conn.WriteTo((*b)[:n], v.(net.Addr)) + return true + }) + return nil + } + + var addr net.Addr + if v, ok := h.routes.Load(hwAddrToTapRouteKey(dst)); ok { + addr = v.(net.Addr) + } + if addr == nil { + log.Warnf("no route for %s -> %s %s %d", src, dst, eType, n) + return nil + } + + if _, err := conn.WriteTo((*b)[:n], addr); err != nil { + return err + } + + return nil + }() + + if err != nil { + errc <- err + return + } + } + }() + + go func() { + for { + err := func() error { + b := bufpool.Get(h.md.bufferSize) + defer bufpool.Put(b) + + n, addr, err := conn.ReadFrom(*b) + if err != nil && + err != shadowaead.ErrShortPacket { + return err + } + + src := waterutil.MACSource((*b)[:n]) + dst := waterutil.MACDestination((*b)[:n]) + eType := etherType(waterutil.MACEthertype((*b)[:n])) + + log.Debugf("%s >> %s %s %d", src, dst, eType, n) + + // client side, deliver frame to tap device. + if raddr != nil { + _, err := tap.Write((*b)[:n]) + return err + } + + // server side, record route. + rkey := hwAddrToTapRouteKey(src) + if actual, loaded := h.routes.LoadOrStore(rkey, addr); loaded { + if actual.(net.Addr).String() != addr.String() { + log.Debugf("update route: %s -> %s (old %s)", + src, addr, actual.(net.Addr)) + h.routes.Store(rkey, addr) + } + } else { + log.Debugf("new route: %s -> %s", src, addr) + } + + if waterutil.IsBroadcast(dst) { + go h.routes.Range(func(k, v any) bool { + if k.(tapRouteKey) != rkey { + conn.WriteTo((*b)[:n], v.(net.Addr)) + } + return true + }) + } + + if v, ok := h.routes.Load(hwAddrToTapRouteKey(dst)); ok { + log.Debugf("find route: %s -> %s", dst, v) + _, err := conn.WriteTo((*b)[:n], v.(net.Addr)) + return err + } + + if _, err := tap.Write((*b)[:n]); err != nil { + select { + case h.exit <- struct{}{}: + default: + } + return err + } + return nil + }() + + if err != nil { + errc <- err + return + } + } + }() + + err := <-errc + if err != nil && err == io.EOF { + err = nil + } + return err +} + +var mEtherTypes = map[waterutil.Ethertype]string{ + waterutil.IPv4: "ip", + waterutil.ARP: "arp", + waterutil.RARP: "rarp", + waterutil.IPv6: "ip6", +} + +func etherType(et waterutil.Ethertype) string { + if s, ok := mEtherTypes[et]; ok { + return s + } + return fmt.Sprintf("unknown(%v)", et) +} + +type tapRouteKey [6]byte + +func hwAddrToTapRouteKey(addr net.HardwareAddr) (key tapRouteKey) { + copy(key[:], addr) + return +} diff --git a/handler/tap/metadata.go b/handler/tap/metadata.go new file mode 100644 index 0000000..9153b5d --- /dev/null +++ b/handler/tap/metadata.go @@ -0,0 +1,24 @@ +package tap + +import ( + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + key string + bufferSize int +} + +func (h *tapHandler) parseMetadata(md mdata.Metadata) (err error) { + const ( + key = "key" + bufferSize = "bufferSize" + ) + + h.md.key = mdata.GetString(md, key) + h.md.bufferSize = mdata.GetInt(md, bufferSize) + if h.md.bufferSize <= 0 { + h.md.bufferSize = 1500 + } + return +} diff --git a/handler/tun/handler.go b/handler/tun/handler.go new file mode 100644 index 0000000..aade1b1 --- /dev/null +++ b/handler/tun/handler.go @@ -0,0 +1,391 @@ +package tun + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "os" + "sync" + "time" + + "github.com/go-gost/gost/v3/pkg/chain" + "github.com/go-gost/gost/v3/pkg/common/bufpool" + "github.com/go-gost/gost/v3/pkg/handler" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "github.com/go-gost/x/internal/util/ss" + tun_util "github.com/go-gost/x/internal/util/tun" + "github.com/shadowsocks/go-shadowsocks2/core" + "github.com/shadowsocks/go-shadowsocks2/shadowaead" + "github.com/songgao/water/waterutil" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +func init() { + registry.HandlerRegistry().Register("tun", NewHandler) +} + +type tunHandler struct { + group *chain.NodeGroup + routes sync.Map + exit chan struct{} + cipher core.Cipher + router *chain.Router + md metadata + options handler.Options +} + +func NewHandler(opts ...handler.Option) handler.Handler { + options := handler.Options{} + for _, opt := range opts { + opt(&options) + } + + return &tunHandler{ + exit: make(chan struct{}, 1), + options: options, + } +} + +func (h *tunHandler) Init(md md.Metadata) (err error) { + if err = h.parseMetadata(md); err != nil { + return + } + + if h.options.Auth != nil { + method := h.options.Auth.Username() + password, _ := h.options.Auth.Password() + h.cipher, err = ss.ShadowCipher(method, password, h.md.key) + if err != nil { + return + } + } + + h.router = h.options.Router + if h.router == nil { + h.router = (&chain.Router{}).WithLogger(h.options.Logger) + } + + return +} + +// Forward implements handler.Forwarder. +func (h *tunHandler) Forward(group *chain.NodeGroup) { + h.group = group +} + +func (h *tunHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error { + defer os.Exit(0) + defer conn.Close() + + log := h.options.Logger + + cc, ok := conn.(*tun_util.Conn) + if !ok || cc.Config() == nil { + err := errors.New("tun: wrong connection type") + log.Error(err) + return err + } + + start := time.Now() + log = log.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + }) + + log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) + defer func() { + log.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr()) + }() + + network := "udp" + var raddr net.Addr + var err error + + target := h.group.Next() + if target != nil { + raddr, err = net.ResolveUDPAddr(network, target.Addr) + if err != nil { + log.Error(err) + return err + } + log = log.WithFields(map[string]any{ + "dst": fmt.Sprintf("%s/%s", raddr.String(), raddr.Network()), + }) + log.Infof("%s >> %s", conn.RemoteAddr(), target.Addr) + } + + h.handleLoop(ctx, conn, raddr, cc.Config(), log) + return nil +} + +func (h *tunHandler) handleLoop(ctx context.Context, conn net.Conn, addr net.Addr, config *tun_util.Config, log logger.Logger) { + var tempDelay time.Duration + for { + err := func() error { + var err error + var pc net.PacketConn + if addr != nil { + cc, err := h.router.Dial(ctx, addr.Network(), "") + if err != nil { + return err + } + + var ok bool + pc, ok = cc.(net.PacketConn) + if !ok { + cc.Close() + return errors.New("wrong connection type") + } + } else { + laddr, _ := net.ResolveUDPAddr("udp", conn.LocalAddr().String()) + pc, err = net.ListenUDP("udp", laddr) + } + if err != nil { + return err + } + + if h.cipher != nil { + pc = h.cipher.PacketConn(pc) + } + defer pc.Close() + + return h.transport(conn, pc, addr, config, log) + }() + if err != nil { + log.Error(err) + } + + select { + case <-h.exit: + return + default: + } + + if err != nil { + if tempDelay == 0 { + tempDelay = 1000 * time.Millisecond + } else { + tempDelay *= 2 + } + if max := 6 * time.Second; tempDelay > max { + tempDelay = max + } + time.Sleep(tempDelay) + continue + } + tempDelay = 0 + } + +} + +func (h *tunHandler) transport(tun net.Conn, conn net.PacketConn, raddr net.Addr, config *tun_util.Config, log logger.Logger) error { + errc := make(chan error, 1) + + go func() { + for { + err := func() error { + b := bufpool.Get(h.md.bufferSize) + defer bufpool.Put(b) + + n, err := tun.Read(*b) + if err != nil { + select { + case h.exit <- struct{}{}: + default: + } + return err + } + + var src, dst net.IP + if waterutil.IsIPv4((*b)[:n]) { + header, err := ipv4.ParseHeader((*b)[:n]) + if err != nil { + log.Error(err) + return nil + } + log.Debugf("%s >> %s %-4s %d/%-4d %-4x %d", + header.Src, header.Dst, ipProtocol(waterutil.IPv4Protocol((*b)[:n])), + header.Len, header.TotalLen, header.ID, header.Flags) + + src, dst = header.Src, header.Dst + } else if waterutil.IsIPv6((*b)[:n]) { + header, err := ipv6.ParseHeader((*b)[:n]) + if err != nil { + log.Warn(err) + return nil + } + log.Debugf("%s >> %s %s %d %d", + header.Src, header.Dst, + ipProtocol(waterutil.IPProtocol(header.NextHeader)), + header.PayloadLen, header.TrafficClass) + + src, dst = header.Src, header.Dst + } else { + log.Warn("unknown packet, discarded") + return nil + } + + // client side, deliver packet directly. + if raddr != nil { + _, err := conn.WriteTo((*b)[:n], raddr) + return err + } + + addr := h.findRouteFor(dst, config.Routes...) + if addr == nil { + log.Warnf("no route for %s -> %s", src, dst) + return nil + } + + log.Debugf("find route: %s -> %s", dst, addr) + + if _, err := conn.WriteTo((*b)[:n], addr); err != nil { + return err + } + return nil + }() + + if err != nil { + errc <- err + return + } + } + }() + + go func() { + for { + err := func() error { + b := bufpool.Get(h.md.bufferSize) + defer bufpool.Put(b) + + n, addr, err := conn.ReadFrom(*b) + if err != nil && + err != shadowaead.ErrShortPacket { + return err + } + + var src, dst net.IP + if waterutil.IsIPv4((*b)[:n]) { + header, err := ipv4.ParseHeader((*b)[:n]) + if err != nil { + log.Warn(err) + return nil + } + + log.Debugf("%s >> %s %-4s %d/%-4d %-4x %d", + header.Src, header.Dst, ipProtocol(waterutil.IPv4Protocol((*b)[:n])), + header.Len, header.TotalLen, header.ID, header.Flags) + + src, dst = header.Src, header.Dst + } else if waterutil.IsIPv6((*b)[:n]) { + header, err := ipv6.ParseHeader((*b)[:n]) + if err != nil { + log.Warn(err) + return nil + } + + log.Debugf("%s > %s %s %d %d", + header.Src, header.Dst, + ipProtocol(waterutil.IPProtocol(header.NextHeader)), + header.PayloadLen, header.TrafficClass) + + src, dst = header.Src, header.Dst + } else { + log.Warn("unknown packet, discarded") + return nil + } + + // client side, deliver packet to tun device. + if raddr != nil { + _, err := tun.Write((*b)[:n]) + return err + } + + rkey := ipToTunRouteKey(src) + if actual, loaded := h.routes.LoadOrStore(rkey, addr); loaded { + if actual.(net.Addr).String() != addr.String() { + log.Debugf("update route: %s -> %s (old %s)", + src, addr, actual.(net.Addr)) + h.routes.Store(rkey, addr) + } + } else { + log.Warnf("no route for %s -> %s", src, addr) + } + + if addr := h.findRouteFor(dst, config.Routes...); addr != nil { + log.Debugf("find route: %s -> %s", dst, addr) + + _, err := conn.WriteTo((*b)[:n], addr) + return err + } + + if _, err := tun.Write((*b)[:n]); err != nil { + select { + case h.exit <- struct{}{}: + default: + } + return err + } + return nil + }() + + if err != nil { + errc <- err + return + } + } + }() + + err := <-errc + if err != nil && err == io.EOF { + err = nil + } + return err +} + +func (h *tunHandler) findRouteFor(dst net.IP, routes ...tun_util.Route) net.Addr { + if v, ok := h.routes.Load(ipToTunRouteKey(dst)); ok { + return v.(net.Addr) + } + for _, route := range routes { + if route.Net.Contains(dst) && route.Gateway != nil { + if v, ok := h.routes.Load(ipToTunRouteKey(route.Gateway)); ok { + return v.(net.Addr) + } + } + } + return nil +} + +var mIPProts = map[waterutil.IPProtocol]string{ + waterutil.HOPOPT: "HOPOPT", + waterutil.ICMP: "ICMP", + waterutil.IGMP: "IGMP", + waterutil.GGP: "GGP", + waterutil.TCP: "TCP", + waterutil.UDP: "UDP", + waterutil.IPv6_Route: "IPv6-Route", + waterutil.IPv6_Frag: "IPv6-Frag", + waterutil.IPv6_ICMP: "IPv6-ICMP", +} + +func ipProtocol(p waterutil.IPProtocol) string { + if v, ok := mIPProts[p]; ok { + return v + } + return fmt.Sprintf("unknown(%d)", p) +} + +type tunRouteKey [16]byte + +func ipToTunRouteKey(ip net.IP) (key tunRouteKey) { + copy(key[:], ip.To16()) + return +} diff --git a/handler/tun/metadata.go b/handler/tun/metadata.go new file mode 100644 index 0000000..c5329eb --- /dev/null +++ b/handler/tun/metadata.go @@ -0,0 +1,24 @@ +package tun + +import ( + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + key string + bufferSize int +} + +func (h *tunHandler) parseMetadata(md mdata.Metadata) (err error) { + const ( + key = "key" + bufferSize = "bufferSize" + ) + + h.md.key = mdata.GetString(md, key) + h.md.bufferSize = mdata.GetInt(md, bufferSize) + if h.md.bufferSize <= 0 { + h.md.bufferSize = 1500 + } + return +} diff --git a/internal/util/grpc/proto/gost.pb.go b/internal/util/grpc/proto/gost.pb.go new file mode 100644 index 0000000..d3167d3 --- /dev/null +++ b/internal/util/grpc/proto/gost.pb.go @@ -0,0 +1,148 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.12.4 +// source: gost.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Chunk struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *Chunk) Reset() { + *x = Chunk{} + if protoimpl.UnsafeEnabled { + mi := &file_gost_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Chunk) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Chunk) ProtoMessage() {} + +func (x *Chunk) ProtoReflect() protoreflect.Message { + mi := &file_gost_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Chunk.ProtoReflect.Descriptor instead. +func (*Chunk) Descriptor() ([]byte, []int) { + return file_gost_proto_rawDescGZIP(), []int{0} +} + +func (x *Chunk) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +var File_gost_proto protoreflect.FileDescriptor + +var file_gost_proto_rawDesc = []byte{ + 0x0a, 0x0a, 0x67, 0x6f, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x1b, 0x0a, 0x05, + 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0x29, 0x0a, 0x09, 0x47, 0x6f, 0x73, + 0x74, 0x54, 0x75, 0x6e, 0x65, 0x6c, 0x12, 0x1c, 0x0a, 0x06, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, + 0x12, 0x06, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x1a, 0x06, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, + 0x28, 0x01, 0x30, 0x01, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x67, 0x6f, 0x73, 0x74, 0x2f, 0x67, 0x6f, 0x73, 0x74, 0x2f, + 0x70, 0x6b, 0x67, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f, + 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_gost_proto_rawDescOnce sync.Once + file_gost_proto_rawDescData = file_gost_proto_rawDesc +) + +func file_gost_proto_rawDescGZIP() []byte { + file_gost_proto_rawDescOnce.Do(func() { + file_gost_proto_rawDescData = protoimpl.X.CompressGZIP(file_gost_proto_rawDescData) + }) + return file_gost_proto_rawDescData +} + +var file_gost_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_gost_proto_goTypes = []any{ + (*Chunk)(nil), // 0: Chunk +} +var file_gost_proto_depIdxs = []int32{ + 0, // 0: GostTunel.Tunnel:input_type -> Chunk + 0, // 1: GostTunel.Tunnel:output_type -> Chunk + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_gost_proto_init() } +func file_gost_proto_init() { + if File_gost_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_gost_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*Chunk); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_gost_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_gost_proto_goTypes, + DependencyIndexes: file_gost_proto_depIdxs, + MessageInfos: file_gost_proto_msgTypes, + }.Build() + File_gost_proto = out.File + file_gost_proto_rawDesc = nil + file_gost_proto_goTypes = nil + file_gost_proto_depIdxs = nil +} diff --git a/internal/util/grpc/proto/gost.proto b/internal/util/grpc/proto/gost.proto new file mode 100644 index 0000000..9af1713 --- /dev/null +++ b/internal/util/grpc/proto/gost.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; +option go_package = "github.com/go-gost/gost/v3/pkg/common/util/grpc/proto"; + +message Chunk { + bytes data = 1; +} + +service GostTunel { + rpc Tunnel (stream Chunk) returns (stream Chunk); +} \ No newline at end of file diff --git a/internal/util/grpc/proto/gost_grpc.pb.go b/internal/util/grpc/proto/gost_grpc.pb.go new file mode 100644 index 0000000..0730e5d --- /dev/null +++ b/internal/util/grpc/proto/gost_grpc.pb.go @@ -0,0 +1,133 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// GostTunelClient is the client API for GostTunel service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GostTunelClient interface { + Tunnel(ctx context.Context, opts ...grpc.CallOption) (GostTunel_TunnelClient, error) +} + +type gostTunelClient struct { + cc grpc.ClientConnInterface +} + +func NewGostTunelClient(cc grpc.ClientConnInterface) GostTunelClient { + return &gostTunelClient{cc} +} + +func (c *gostTunelClient) Tunnel(ctx context.Context, opts ...grpc.CallOption) (GostTunel_TunnelClient, error) { + stream, err := c.cc.NewStream(ctx, &GostTunel_ServiceDesc.Streams[0], "/GostTunel/Tunnel", opts...) + if err != nil { + return nil, err + } + x := &gostTunelTunnelClient{stream} + return x, nil +} + +type GostTunel_TunnelClient interface { + Send(*Chunk) error + Recv() (*Chunk, error) + grpc.ClientStream +} + +type gostTunelTunnelClient struct { + grpc.ClientStream +} + +func (x *gostTunelTunnelClient) Send(m *Chunk) error { + return x.ClientStream.SendMsg(m) +} + +func (x *gostTunelTunnelClient) Recv() (*Chunk, error) { + m := new(Chunk) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// GostTunelServer is the server API for GostTunel service. +// All implementations must embed UnimplementedGostTunelServer +// for forward compatibility +type GostTunelServer interface { + Tunnel(GostTunel_TunnelServer) error + mustEmbedUnimplementedGostTunelServer() +} + +// UnimplementedGostTunelServer must be embedded to have forward compatible implementations. +type UnimplementedGostTunelServer struct { +} + +func (UnimplementedGostTunelServer) Tunnel(GostTunel_TunnelServer) error { + return status.Errorf(codes.Unimplemented, "method Tunnel not implemented") +} +func (UnimplementedGostTunelServer) mustEmbedUnimplementedGostTunelServer() {} + +// UnsafeGostTunelServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GostTunelServer will +// result in compilation errors. +type UnsafeGostTunelServer interface { + mustEmbedUnimplementedGostTunelServer() +} + +func RegisterGostTunelServer(s grpc.ServiceRegistrar, srv GostTunelServer) { + s.RegisterService(&GostTunel_ServiceDesc, srv) +} + +func _GostTunel_Tunnel_Handler(srv any, stream grpc.ServerStream) error { + return srv.(GostTunelServer).Tunnel(&gostTunelTunnelServer{stream}) +} + +type GostTunel_TunnelServer interface { + Send(*Chunk) error + Recv() (*Chunk, error) + grpc.ServerStream +} + +type gostTunelTunnelServer struct { + grpc.ServerStream +} + +func (x *gostTunelTunnelServer) Send(m *Chunk) error { + return x.ServerStream.SendMsg(m) +} + +func (x *gostTunelTunnelServer) Recv() (*Chunk, error) { + m := new(Chunk) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// GostTunel_ServiceDesc is the grpc.ServiceDesc for GostTunel service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GostTunel_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "GostTunel", + HandlerType: (*GostTunelServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Tunnel", + Handler: _GostTunel_Tunnel_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "gost.proto", +} diff --git a/internal/util/grpc/proto/protoc.sh b/internal/util/grpc/proto/protoc.sh new file mode 100755 index 0000000..4e33305 --- /dev/null +++ b/internal/util/grpc/proto/protoc.sh @@ -0,0 +1,3 @@ +protoc --go_out=. --go_opt=paths=source_relative \ + --go-grpc_out=. --go-grpc_opt=paths=source_relative \ + gost.proto \ No newline at end of file diff --git a/internal/util/http2/conn.go b/internal/util/http2/conn.go new file mode 100644 index 0000000..eb2f43c --- /dev/null +++ b/internal/util/http2/conn.go @@ -0,0 +1,132 @@ +package http2 + +import ( + "errors" + "net" + "net/http" + "time" +) + +// a dummy HTTP2 client conn used by HTTP2 client connector +type ClientConn struct { + localAddr net.Addr + remoteAddr net.Addr + client *http.Client + onClose func() +} + +func NewClientConn(localAddr, remoteAddr net.Addr, client *http.Client, onClose func()) net.Conn { + return &ClientConn{ + localAddr: localAddr, + remoteAddr: remoteAddr, + client: client, + onClose: onClose, + } +} + +func (c *ClientConn) Client() *http.Client { + return c.client +} + +func (c *ClientConn) Close() error { + if c.onClose != nil { + c.onClose() + } + return nil +} + +func (c *ClientConn) Read(b []byte) (n int, err error) { + return 0, &net.OpError{Op: "read", Net: "nop", Source: nil, Addr: nil, Err: errors.New("read not supported")} +} + +func (c *ClientConn) Write(b []byte) (n int, err error) { + return 0, &net.OpError{Op: "write", Net: "nop", Source: nil, Addr: nil, Err: errors.New("write not supported")} +} + +func (c *ClientConn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *ClientConn) RemoteAddr() net.Addr { + return c.remoteAddr +} + +func (c *ClientConn) SetDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "nop", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *ClientConn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "nop", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *ClientConn) SetWriteDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "nop", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +// a dummy HTTP2 server conn used by HTTP2 handler +type ServerConn struct { + r *http.Request + w http.ResponseWriter + localAddr net.Addr + remoteAddr net.Addr + closed chan struct{} +} + +func NewServerConn(w http.ResponseWriter, r *http.Request, localAddr, remoteAddr net.Addr) *ServerConn { + return &ServerConn{ + r: r, + w: w, + localAddr: localAddr, + remoteAddr: remoteAddr, + closed: make(chan struct{}), + } +} + +func (c *ServerConn) Done() <-chan struct{} { + return c.closed +} + +func (c *ServerConn) Request() *http.Request { + return c.r +} + +func (c *ServerConn) Writer() http.ResponseWriter { + return c.w +} + +func (c *ServerConn) 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 *ServerConn) 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 *ServerConn) Close() error { + select { + case <-c.closed: + default: + close(c.closed) + } + return nil +} + +func (c *ServerConn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *ServerConn) RemoteAddr() net.Addr { + return c.remoteAddr +} + +func (c *ServerConn) SetDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *ServerConn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *ServerConn) SetWriteDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} diff --git a/internal/util/icmp/conn.go b/internal/util/icmp/conn.go new file mode 100644 index 0000000..9d39132 --- /dev/null +++ b/internal/util/icmp/conn.go @@ -0,0 +1,285 @@ +package icmp + +import ( + "encoding/binary" + "errors" + "fmt" + "math" + "net" + "sync/atomic" + + "github.com/go-gost/gost/v3/pkg/common/bufpool" + "github.com/go-gost/gost/v3/pkg/logger" + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" +) + +const ( + readBufferSize = 1500 + writeBufferSize = 1500 + magicNumber = 0x474F5354 +) + +const ( + messageHeaderLen = 10 +) + +const ( + FlagAck = 1 +) + +var ( + ErrInvalidPacket = errors.New("icmp: invalid packet") + ErrInvalidType = errors.New("icmp: invalid type") + ErrShortBuffer = errors.New("icmp: short buffer") +) + +type message struct { + // magic uint32 // magic number + flags uint16 // flags + // rsv uint16 // reserved field + // len uint16 // length of data + data []byte +} + +func (m *message) Encode(b []byte) (n int, err error) { + if len(b) < messageHeaderLen+len(m.data) { + err = ErrShortBuffer + return + } + binary.BigEndian.PutUint32(b[:4], magicNumber) // magic number + binary.BigEndian.PutUint16(b[4:6], m.flags) // flags + binary.BigEndian.PutUint16(b[6:8], 0) // reserved + binary.BigEndian.PutUint16(b[8:10], uint16(len(m.data))) + copy(b[messageHeaderLen:], m.data) + + n = messageHeaderLen + len(m.data) + return +} + +func (m *message) Decode(b []byte) (n int, err error) { + if len(b) < messageHeaderLen { + err = ErrShortBuffer + return + } + if binary.BigEndian.Uint32(b[:4]) != magicNumber { + err = ErrInvalidPacket + return + } + m.flags = binary.BigEndian.Uint16(b[4:6]) + length := binary.BigEndian.Uint16(b[8:10]) + if len(b[messageHeaderLen:]) < int(length) { + err = ErrShortBuffer + return + } + m.data = b[messageHeaderLen : messageHeaderLen+length] + + n = messageHeaderLen + int(length) + return +} + +type clientConn struct { + net.PacketConn + id int + seq uint32 +} + +func ClientConn(conn net.PacketConn, id int) net.PacketConn { + return &clientConn{ + PacketConn: conn, + id: id, + } +} + +func (c *clientConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + buf := bufpool.Get(readBufferSize) + defer bufpool.Put(buf) + + for { + n, addr, err = c.PacketConn.ReadFrom(*buf) + if err != nil { + return + } + + m, err := icmp.ParseMessage(1, (*buf)[:n]) + if err != nil { + // logger.Default().Error("icmp: parse message %v", err) + return 0, addr, err + } + echo, ok := m.Body.(*icmp.Echo) + if !ok || m.Type != ipv4.ICMPTypeEchoReply { + // logger.Default().Warnf("icmp: invalid type %s (discarded)", m.Type) + continue // discard + } + + if echo.ID != c.id { + // logger.Default().Warnf("icmp: id mismatch got %d, should be %d (discarded)", echo.ID, c.id) + continue + } + + msg := message{} + if _, err := msg.Decode(echo.Data); err != nil { + logger.Default().Warn(err) + continue + } + + if msg.flags&FlagAck == 0 { + // logger.Default().Warn("icmp: invalid message (discarded)") + continue + } + n = copy(b, msg.data) + break + } + + if v, ok := addr.(*net.IPAddr); ok { + addr = &net.UDPAddr{ + IP: v.IP, + Port: c.id, + } + } + // logger.Default().Infof("icmp: read from: %v %d", addr, n) + + return +} + +func (c *clientConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + // logger.Default().Infof("icmp: write to: %v %d", addr, len(b)) + switch v := addr.(type) { + case *net.UDPAddr: + addr = &net.IPAddr{IP: v.IP} + } + + buf := bufpool.Get(writeBufferSize) + defer bufpool.Put(buf) + + msg := message{ + data: b, + } + nn, err := msg.Encode(*buf) + if err != nil { + return + } + + echo := icmp.Echo{ + ID: c.id, + Seq: int(atomic.AddUint32(&c.seq, 1)), + Data: (*buf)[:nn], + } + m := icmp.Message{ + Type: ipv4.ICMPTypeEcho, + Code: 0, + Body: &echo, + } + wb, err := m.Marshal(nil) + if err != nil { + return 0, err + } + _, err = c.PacketConn.WriteTo(wb, addr) + n = len(b) + return +} + +type serverConn struct { + net.PacketConn + seqs [65535]uint32 +} + +func ServerConn(conn net.PacketConn) net.PacketConn { + return &serverConn{ + PacketConn: conn, + } +} + +func (c *serverConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + buf := bufpool.Get(readBufferSize) + defer bufpool.Put(buf) + + for { + n, addr, err = c.PacketConn.ReadFrom(*buf) + if err != nil { + return + } + + m, err := icmp.ParseMessage(1, (*buf)[:n]) + if err != nil { + // logger.Default().Error("icmp: parse message %v", err) + return 0, addr, err + } + + echo, ok := m.Body.(*icmp.Echo) + if !ok || m.Type != ipv4.ICMPTypeEcho || echo.ID <= 0 { + // logger.Default().Warnf("icmp: invalid type %s (discarded)", m.Type) + continue + } + + atomic.StoreUint32(&c.seqs[uint16(echo.ID-1)], uint32(echo.Seq)) + + msg := message{} + if _, err := msg.Decode(echo.Data); err != nil { + continue + } + + if msg.flags&FlagAck > 0 { + continue + } + + n = copy(b, msg.data) + + if v, ok := addr.(*net.IPAddr); ok { + addr = &net.UDPAddr{ + IP: v.IP, + Port: echo.ID, + } + } + break + } + + // logger.Default().Infof("icmp: read from: %v %d", addr, n) + + return +} + +func (c *serverConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + // logger.Default().Infof("icmp: write to: %v %d", addr, len(b)) + var id int + switch v := addr.(type) { + case *net.UDPAddr: + addr = &net.IPAddr{IP: v.IP} + id = v.Port + } + + if id <= 0 || id > math.MaxUint16 { + err = fmt.Errorf("icmp: invalid message id %v", addr) + return + } + + buf := bufpool.Get(writeBufferSize) + defer bufpool.Put(buf) + + msg := message{ + flags: FlagAck, + data: b, + } + nn, err := msg.Encode(*buf) + if err != nil { + return + } + + echo := icmp.Echo{ + ID: id, + Seq: int(atomic.LoadUint32(&c.seqs[id-1])), + Data: (*buf)[:nn], + } + m := icmp.Message{ + Type: ipv4.ICMPTypeEchoReply, + Code: 0, + Body: &echo, + } + wb, err := m.Marshal(nil) + if err != nil { + return 0, err + } + _, err = c.PacketConn.WriteTo(wb, addr) + n = len(b) + return +} diff --git a/internal/util/kcp/config.go b/internal/util/kcp/config.go new file mode 100644 index 0000000..85cfb0b --- /dev/null +++ b/internal/util/kcp/config.go @@ -0,0 +1,115 @@ +package kcp + +import ( + "crypto/sha1" + + "github.com/xtaci/kcp-go/v5" + "golang.org/x/crypto/pbkdf2" +) + +var ( + // DefaultSalt is the default salt for KCP cipher. + DefaultSalt = "kcp-go" +) + +var ( + // DefaultKCPConfig is the default KCP config. + DefaultConfig = &Config{ + Key: "it's a secrect", + Crypt: "aes", + Mode: "fast", + MTU: 1350, + SndWnd: 1024, + RcvWnd: 1024, + DataShard: 10, + ParityShard: 3, + DSCP: 0, + NoComp: false, + AckNodelay: false, + NoDelay: 0, + Interval: 50, + Resend: 0, + NoCongestion: 0, + SockBuf: 4194304, + KeepAlive: 10, + SnmpLog: "", + SnmpPeriod: 60, + Signal: false, + TCP: false, + } +) + +// KCPConfig describes the config for KCP. +type Config struct { + Key string `json:"key"` + Crypt string `json:"crypt"` + Mode string `json:"mode"` + MTU int `json:"mtu"` + SndWnd int `json:"sndwnd"` + RcvWnd int `json:"rcvwnd"` + DataShard int `json:"datashard"` + ParityShard int `json:"parityshard"` + DSCP int `json:"dscp"` + NoComp bool `json:"nocomp"` + AckNodelay bool `json:"acknodelay"` + NoDelay int `json:"nodelay"` + Interval int `json:"interval"` + Resend int `json:"resend"` + NoCongestion int `json:"nc"` + SockBuf int `json:"sockbuf"` + KeepAlive int `json:"keepalive"` + SnmpLog string `json:"snmplog"` + SnmpPeriod int `json:"snmpperiod"` + Signal bool `json:"signal"` // Signal enables the signal SIGUSR1 feature. + TCP bool `json:"tcp"` +} + +// Init initializes the KCP config. +func (c *Config) Init() { + switch c.Mode { + case "normal": + c.NoDelay, c.Interval, c.Resend, c.NoCongestion = 0, 40, 2, 1 + case "fast": + c.NoDelay, c.Interval, c.Resend, c.NoCongestion = 0, 30, 2, 1 + case "fast2": + c.NoDelay, c.Interval, c.Resend, c.NoCongestion = 1, 20, 2, 1 + case "fast3": + c.NoDelay, c.Interval, c.Resend, c.NoCongestion = 1, 10, 2, 1 + } +} + +func BlockCrypt(key, crypt, salt string) (block kcp.BlockCrypt) { + pass := pbkdf2.Key([]byte(key), []byte(salt), 4096, 32, sha1.New) + + switch crypt { + case "sm4": + block, _ = kcp.NewSM4BlockCrypt(pass[:16]) + case "tea": + block, _ = kcp.NewTEABlockCrypt(pass[:16]) + case "xor": + block, _ = kcp.NewSimpleXORBlockCrypt(pass) + case "none": + block, _ = kcp.NewNoneBlockCrypt(pass) + case "aes-128": + block, _ = kcp.NewAESBlockCrypt(pass[:16]) + case "aes-192": + block, _ = kcp.NewAESBlockCrypt(pass[:24]) + case "blowfish": + block, _ = kcp.NewBlowfishBlockCrypt(pass) + case "twofish": + block, _ = kcp.NewTwofishBlockCrypt(pass) + case "cast5": + block, _ = kcp.NewCast5BlockCrypt(pass[:16]) + case "3des": + block, _ = kcp.NewTripleDESBlockCrypt(pass[:24]) + case "xtea": + block, _ = kcp.NewXTEABlockCrypt(pass[:16]) + case "salsa20": + block, _ = kcp.NewSalsa20BlockCrypt(pass) + case "aes": + fallthrough + default: // aes + block, _ = kcp.NewAESBlockCrypt(pass) + } + return +} diff --git a/internal/util/kcp/kcp.go b/internal/util/kcp/kcp.go new file mode 100644 index 0000000..22def39 --- /dev/null +++ b/internal/util/kcp/kcp.go @@ -0,0 +1,34 @@ +package kcp + +import ( + "net" + + "github.com/golang/snappy" +) + +type kcpCompStreamConn struct { + net.Conn + w *snappy.Writer + r *snappy.Reader +} + +func CompStreamConn(conn net.Conn) net.Conn { + return &kcpCompStreamConn{ + Conn: conn, + w: snappy.NewBufferedWriter(conn), + r: snappy.NewReader(conn), + } +} + +func (c *kcpCompStreamConn) Read(b []byte) (n int, err error) { + return c.r.Read(b) +} + +func (c *kcpCompStreamConn) Write(b []byte) (n int, err error) { + n, err = c.w.Write(b) + if err != nil { + return + } + err = c.w.Flush() + return n, err +} diff --git a/internal/util/mux/mux.go b/internal/util/mux/mux.go new file mode 100644 index 0000000..4fae020 --- /dev/null +++ b/internal/util/mux/mux.go @@ -0,0 +1,85 @@ +package mux + +import ( + "net" + + smux "github.com/xtaci/smux" +) + +type Session struct { + conn net.Conn + session *smux.Session +} + +func ClientSession(conn net.Conn) (*Session, error) { + s, err := smux.Client(conn, smux.DefaultConfig()) + if err != nil { + return nil, err + } + return &Session{ + conn: conn, + session: s, + }, nil +} + +func ServerSession(conn net.Conn) (*Session, error) { + s, err := smux.Server(conn, smux.DefaultConfig()) + if err != nil { + return nil, err + } + return &Session{ + conn: conn, + session: s, + }, nil +} + +func (session *Session) GetConn() (net.Conn, error) { + stream, err := session.session.OpenStream() + if err != nil { + return nil, err + } + return &streamConn{Conn: session.conn, stream: stream}, nil +} + +func (session *Session) Accept() (net.Conn, error) { + stream, err := session.session.AcceptStream() + if err != nil { + return nil, err + } + return &streamConn{Conn: session.conn, stream: stream}, nil +} + +func (session *Session) Close() error { + if session.session == nil { + return nil + } + return session.session.Close() +} + +func (session *Session) IsClosed() bool { + if session.session == nil { + return true + } + return session.session.IsClosed() +} + +func (session *Session) NumStreams() int { + return session.session.NumStreams() +} + +type streamConn struct { + net.Conn + stream *smux.Stream +} + +func (c *streamConn) Read(b []byte) (n int, err error) { + return c.stream.Read(b) +} + +func (c *streamConn) Write(b []byte) (n int, err error) { + return c.stream.Write(b) +} + +func (c *streamConn) Close() error { + return c.stream.Close() +} diff --git a/internal/util/pht/client.go b/internal/util/pht/client.go new file mode 100644 index 0000000..7c25423 --- /dev/null +++ b/internal/util/pht/client.go @@ -0,0 +1,105 @@ +package pht + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/http/httputil" + "strconv" + "strings" + + "github.com/go-gost/gost/v3/pkg/logger" +) + +type Client struct { + Host string + Client *http.Client + AuthorizePath string + PushPath string + PullPath string + TLSEnabled bool + Logger logger.Logger +} + +func (c *Client) Dial(ctx context.Context, addr string) (net.Conn, error) { + raddr, err := net.ResolveTCPAddr("tcp", addr) + if err != nil { + c.Logger.Error(err) + return nil, err + } + + if c.Host != "" { + addr = net.JoinHostPort(c.Host, strconv.Itoa(raddr.Port)) + } + + token, err := c.authorize(ctx, addr) + if err != nil { + c.Logger.Error(err) + return nil, err + } + + cn := &clientConn{ + client: c.Client, + rxc: make(chan []byte, 128), + closed: make(chan struct{}), + localAddr: &net.TCPAddr{}, + remoteAddr: raddr, + logger: c.Logger, + } + + scheme := "http" + if c.TLSEnabled { + scheme = "https" + } + cn.pushURL = fmt.Sprintf("%s://%s%s?token=%s", scheme, addr, c.PushPath, token) + cn.pullURL = fmt.Sprintf("%s://%s%s?token=%s", scheme, addr, c.PullPath, token) + + go cn.readLoop() + + return cn, nil +} + +func (c *Client) authorize(ctx context.Context, addr string) (token string, err error) { + var url string + if c.TLSEnabled { + url = fmt.Sprintf("https://%s%s", addr, c.AuthorizePath) + } else { + url = fmt.Sprintf("http://%s%s", addr, c.AuthorizePath) + } + r, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return + } + + if c.Logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpRequest(r, false) + c.Logger.Debug(string(dump)) + } + + resp, err := c.Client.Do(r) + if err != nil { + return + } + defer resp.Body.Close() + + if c.Logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpResponse(resp, false) + c.Logger.Debug(string(dump)) + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return + } + + if strings.HasPrefix(string(data), "token=") { + token = strings.TrimPrefix(string(data), "token=") + } + if token == "" { + err = errors.New("authorize failed") + } + return +} diff --git a/internal/util/pht/conn.go b/internal/util/pht/conn.go new file mode 100644 index 0000000..c1ac10c --- /dev/null +++ b/internal/util/pht/conn.go @@ -0,0 +1,176 @@ +package pht + +import ( + "bufio" + "bytes" + "encoding/base64" + "errors" + "net" + "net/http" + "net/http/httputil" + "time" + + "github.com/go-gost/gost/v3/pkg/logger" +) + +type clientConn struct { + client *http.Client + pushURL string + pullURL string + buf []byte + rxc chan []byte + closed chan struct{} + localAddr net.Addr + remoteAddr net.Addr + logger logger.Logger +} + +func (c *clientConn) Read(b []byte) (n int, err error) { + if len(c.buf) == 0 { + select { + case c.buf = <-c.rxc: + case <-c.closed: + err = net.ErrClosed + return + } + } + + n = copy(b, c.buf) + c.buf = c.buf[n:] + + return +} + +func (c *clientConn) Write(b []byte) (n int, err error) { + if len(b) == 0 { + return + } + + buf := bytes.NewBufferString(base64.StdEncoding.EncodeToString(b)) + buf.WriteByte('\n') + + r, err := http.NewRequest(http.MethodPost, c.pushURL, buf) + if err != nil { + return + } + + if c.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpRequest(r, false) + c.logger.Debug(string(dump)) + } + + resp, err := c.client.Do(r) + if err != nil { + return + } + defer resp.Body.Close() + + if c.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpResponse(resp, false) + c.logger.Debug(string(dump)) + } + + if resp.StatusCode != http.StatusOK { + err = errors.New(resp.Status) + return + } + + n = len(b) + return +} + +func (c *clientConn) readLoop() { + defer c.Close() + + for { + err := func() error { + r, err := http.NewRequest(http.MethodGet, c.pullURL, nil) + if err != nil { + return err + } + + if c.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpRequest(r, false) + c.logger.Debug(string(dump)) + } + + resp, err := c.client.Do(r) + if err != nil { + return err + } + defer resp.Body.Close() + + if c.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpResponse(resp, false) + c.logger.Debug(string(dump)) + } + + if resp.StatusCode != http.StatusOK { + return errors.New(resp.Status) + } + + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + b, err := base64.StdEncoding.DecodeString(scanner.Text()) + if err != nil { + return err + } + select { + case c.rxc <- b: + case <-c.closed: + return net.ErrClosed + } + } + + return scanner.Err() + }() + + if err != nil { + c.logger.Error(err) + return + } + } +} + +func (c *clientConn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *clientConn) RemoteAddr() net.Addr { + return c.remoteAddr +} + +func (c *clientConn) Close() error { + select { + case <-c.closed: + default: + close(c.closed) + } + return nil +} + +func (c *clientConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (c *clientConn) SetWriteDeadline(t time.Time) error { + return nil +} + +func (c *clientConn) SetDeadline(t time.Time) error { + return nil +} + +type serverConn struct { + net.Conn + remoteAddr net.Addr + localAddr net.Addr +} + +func (c *serverConn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *serverConn) RemoteAddr() net.Addr { + return c.remoteAddr +} diff --git a/internal/util/pht/server.go b/internal/util/pht/server.go new file mode 100644 index 0000000..9933c70 --- /dev/null +++ b/internal/util/pht/server.go @@ -0,0 +1,344 @@ +package pht + +import ( + "bufio" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "net" + "net/http" + "net/http/httputil" + "os" + "strings" + "sync" + "time" + + "github.com/go-gost/gost/v3/pkg/common/bufpool" + "github.com/go-gost/gost/v3/pkg/logger" + "github.com/lucas-clemente/quic-go" + "github.com/lucas-clemente/quic-go/http3" + "github.com/rs/xid" +) + +const ( + defaultBacklog = 128 +) + +type serverOptions struct { + authorizePath string + pushPath string + pullPath string + backlog int + tlsEnabled bool + tlsConfig *tls.Config + logger logger.Logger +} + +type ServerOption func(opts *serverOptions) + +func PathServerOption(authorizePath, pushPath, pullPath string) ServerOption { + return func(opts *serverOptions) { + opts.authorizePath = authorizePath + opts.pullPath = pullPath + opts.pushPath = pushPath + } +} + +func BacklogServerOption(backlog int) ServerOption { + return func(opts *serverOptions) { + opts.backlog = backlog + } +} + +func TLSConfigServerOption(tlsConfig *tls.Config) ServerOption { + return func(opts *serverOptions) { + opts.tlsConfig = tlsConfig + } +} + +func EnableTLSServerOption(enable bool) ServerOption { + return func(opts *serverOptions) { + opts.tlsEnabled = enable + } +} + +func LoggerServerOption(logger logger.Logger) ServerOption { + return func(opts *serverOptions) { + opts.logger = logger + } +} + +// TODO: remove stale clients from conns +type Server struct { + addr net.Addr + httpServer *http.Server + http3Server *http3.Server + cqueue chan net.Conn + conns sync.Map + closed chan struct{} + + options serverOptions +} + +func NewServer(addr string, opts ...ServerOption) *Server { + var options serverOptions + for _, opt := range opts { + opt(&options) + } + if options.backlog <= 0 { + options.backlog = defaultBacklog + } + + s := &Server{ + httpServer: &http.Server{ + Addr: addr, + ReadHeaderTimeout: 30 * time.Second, + }, + cqueue: make(chan net.Conn, options.backlog), + closed: make(chan struct{}), + options: options, + } + + mux := http.NewServeMux() + mux.HandleFunc(options.authorizePath, s.handleAuthorize) + mux.HandleFunc(options.pushPath, s.handlePush) + mux.HandleFunc(options.pullPath, s.handlePull) + s.httpServer.Handler = mux + + return s +} + +func NewHTTP3Server(addr string, quicConfig *quic.Config, opts ...ServerOption) *Server { + var options serverOptions + for _, opt := range opts { + opt(&options) + } + if options.backlog <= 0 { + options.backlog = defaultBacklog + } + + s := &Server{ + http3Server: &http3.Server{ + Server: &http.Server{ + Addr: addr, + TLSConfig: options.tlsConfig, + ReadHeaderTimeout: 30 * time.Second, + }, + QuicConfig: quicConfig, + }, + cqueue: make(chan net.Conn, options.backlog), + closed: make(chan struct{}), + options: options, + } + + mux := http.NewServeMux() + mux.HandleFunc(options.authorizePath, s.handleAuthorize) + mux.HandleFunc(options.pushPath, s.handlePush) + mux.HandleFunc(options.pullPath, s.handlePull) + s.http3Server.Handler = mux + + return s +} + +func (s *Server) ListenAndServe() error { + if s.http3Server != nil { + addr, err := net.ResolveUDPAddr("udp", s.http3Server.Addr) + if err != nil { + return err + } + + s.addr = addr + return s.http3Server.ListenAndServe() + } + + ln, err := net.Listen("tcp", s.httpServer.Addr) + if err != nil { + s.options.logger.Error(err) + return err + } + + s.addr = ln.Addr() + if s.options.tlsEnabled { + s.httpServer.TLSConfig = s.options.tlsConfig + ln = tls.NewListener(ln, s.options.tlsConfig) + } + + return s.httpServer.Serve(ln) +} + +func (s *Server) Accept() (conn net.Conn, err error) { + select { + case conn = <-s.cqueue: + case <-s.closed: + err = http.ErrServerClosed + } + return +} + +func (s *Server) Close() error { + select { + case <-s.closed: + return http.ErrServerClosed + default: + close(s.closed) + + if s.http3Server != nil { + return s.http3Server.Close() + } + return s.httpServer.Close() + } +} + +func (s *Server) handleAuthorize(w http.ResponseWriter, r *http.Request) { + if s.options.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpRequest(r, false) + s.options.logger.Debug(string(dump)) + } + + raddr, _ := net.ResolveTCPAddr("tcp", r.RemoteAddr) + if raddr == nil { + raddr = &net.TCPAddr{} + } + + // connection id + cid := xid.New().String() + + c1, c2 := net.Pipe() + c := &serverConn{ + Conn: c1, + localAddr: s.addr, + remoteAddr: raddr, + } + + select { + case s.cqueue <- c: + default: + c.Close() + s.options.logger.Warnf("connection queue is full, client %s discarded", r.RemoteAddr) + w.WriteHeader(http.StatusTooManyRequests) + return + } + + w.Write([]byte(fmt.Sprintf("token=%s", cid))) + s.conns.Store(cid, c2) +} + +func (s *Server) handlePush(w http.ResponseWriter, r *http.Request) { + if s.options.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpRequest(r, false) + s.options.logger.Debug(string(dump)) + } + + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusBadRequest) + return + } + + if err := r.ParseForm(); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + cid := r.Form.Get("token") + v, ok := s.conns.Load(cid) + if !ok { + w.WriteHeader(http.StatusForbidden) + return + } + conn := v.(net.Conn) + + br := bufio.NewReader(r.Body) + data, err := br.ReadString('\n') + if err != nil { + s.options.logger.Error(err) + conn.Close() + s.conns.Delete(cid) + w.WriteHeader(http.StatusBadRequest) + return + } + + data = strings.TrimSuffix(data, "\n") + if len(data) == 0 { + return + } + + b, err := base64.StdEncoding.DecodeString(data) + if err != nil { + s.options.logger.Error(err) + s.conns.Delete(cid) + conn.Close() + w.WriteHeader(http.StatusBadRequest) + return + } + + conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) + defer conn.SetWriteDeadline(time.Time{}) + + if _, err := conn.Write(b); err != nil { + s.options.logger.Error(err) + s.conns.Delete(cid) + conn.Close() + w.WriteHeader(http.StatusGone) + } +} + +func (s *Server) handlePull(w http.ResponseWriter, r *http.Request) { + if s.options.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpRequest(r, false) + s.options.logger.Debug(string(dump)) + } + + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusBadRequest) + return + } + + if err := r.ParseForm(); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + cid := r.Form.Get("token") + v, ok := s.conns.Load(cid) + if !ok { + w.WriteHeader(http.StatusForbidden) + return + } + + conn := v.(net.Conn) + + w.WriteHeader(http.StatusOK) + if fw, ok := w.(http.Flusher); ok { + fw.Flush() + } + + b := bufpool.Get(4096) + defer bufpool.Put(b) + + for { + conn.SetReadDeadline(time.Now().Add(10 * time.Second)) + n, err := conn.Read(*b) + if err != nil { + if !errors.Is(err, os.ErrDeadlineExceeded) { + s.options.logger.Error(err) + s.conns.Delete(cid) + conn.Close() + } else { + (*b)[0] = '\n' + w.Write((*b)[:1]) + } + return + } + + bw := bufio.NewWriter(w) + bw.WriteString(base64.StdEncoding.EncodeToString((*b)[:n])) + bw.WriteString("\n") + if err := bw.Flush(); err != nil { + return + } + if fw, ok := w.(http.Flusher); ok { + fw.Flush() + } + } +} diff --git a/internal/util/quic/conn.go b/internal/util/quic/conn.go new file mode 100644 index 0000000..0fe3ed3 --- /dev/null +++ b/internal/util/quic/conn.go @@ -0,0 +1,90 @@ +package quic + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "io" + "net" +) + +type cipherConn struct { + net.PacketConn + key []byte +} + +func CipherPacketConn(conn net.PacketConn, key []byte) net.PacketConn { + return &cipherConn{ + PacketConn: conn, + key: key, + } +} + +func (conn *cipherConn) ReadFrom(data []byte) (n int, addr net.Addr, err error) { + n, addr, err = conn.PacketConn.ReadFrom(data) + if err != nil { + return + } + b, err := conn.decrypt(data[:n]) + if err != nil { + return + } + + copy(data, b) + + return len(b), addr, nil +} + +func (conn *cipherConn) WriteTo(data []byte, addr net.Addr) (n int, err error) { + b, err := conn.encrypt(data) + if err != nil { + return + } + + _, err = conn.PacketConn.WriteTo(b, addr) + if err != nil { + return + } + + return len(b), nil +} + +func (conn *cipherConn) encrypt(data []byte) ([]byte, error) { + c, err := aes.NewCipher(conn.key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, data, nil), nil +} + +func (conn *cipherConn) decrypt(data []byte) ([]byte, error) { + c, err := aes.NewCipher(conn.key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return nil, errors.New("ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + return gcm.Open(nil, nonce, ciphertext, nil) +} diff --git a/internal/util/relay/conn.go b/internal/util/relay/conn.go new file mode 100644 index 0000000..4b97e29 --- /dev/null +++ b/internal/util/relay/conn.go @@ -0,0 +1,172 @@ +package relay + +import ( + "bytes" + "net" + + "github.com/go-gost/gosocks5" + "github.com/go-gost/gost/v3/pkg/common/bufpool" +) + +type udpTunConn struct { + net.Conn + taddr net.Addr +} + +func UDPTunClientConn(c net.Conn, targetAddr net.Addr) net.Conn { + return &udpTunConn{ + Conn: c, + taddr: targetAddr, + } +} + +func UDPTunClientPacketConn(c net.Conn) net.PacketConn { + return &udpTunConn{ + Conn: c, + } +} + +func UDPTunServerConn(c net.Conn) net.PacketConn { + return &udpTunConn{ + Conn: c, + } +} + +func (c *udpTunConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + socksAddr := gosocks5.Addr{} + header := gosocks5.UDPHeader{ + Addr: &socksAddr, + } + dgram := gosocks5.UDPDatagram{ + Header: &header, + Data: b, + } + _, err = dgram.ReadFrom(c.Conn) + if err != nil { + return + } + + n = len(dgram.Data) + if n > len(b) { + n = copy(b, dgram.Data) + } + addr, err = net.ResolveUDPAddr("udp", socksAddr.String()) + + return +} + +func (c *udpTunConn) Read(b []byte) (n int, err error) { + n, _, err = c.ReadFrom(b) + return +} + +func (c *udpTunConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + socksAddr := gosocks5.Addr{} + if err = socksAddr.ParseFrom(addr.String()); err != nil { + return + } + + header := gosocks5.UDPHeader{ + Addr: &socksAddr, + } + dgram := gosocks5.UDPDatagram{ + Header: &header, + Data: b, + } + dgram.Header.Rsv = uint16(len(dgram.Data)) + dgram.Header.Frag = 0xff // UDP tun relay flag, used by shadowsocks + _, err = dgram.WriteTo(c.Conn) + n = len(b) + + return +} + +func (c *udpTunConn) Write(b []byte) (n int, err error) { + return c.WriteTo(b, c.taddr) +} + +var ( + DefaultBufferSize = 4096 +) + +type udpConn struct { + net.PacketConn + raddr net.Addr + taddr net.Addr + bufferSize int +} + +func UDPConn(c net.PacketConn, bufferSize int) net.PacketConn { + return &udpConn{ + PacketConn: c, + bufferSize: bufferSize, + } +} + +// ReadFrom reads an UDP datagram. +// NOTE: for server side, +// the returned addr is the target address the client want to relay to. +func (c *udpConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + rbuf := bufpool.Get(c.bufferSize) + defer bufpool.Put(rbuf) + + n, c.raddr, err = c.PacketConn.ReadFrom(*rbuf) + if err != nil { + return + } + + socksAddr := gosocks5.Addr{} + header := gosocks5.UDPHeader{ + Addr: &socksAddr, + } + hlen, err := header.ReadFrom(bytes.NewReader((*rbuf)[:n])) + if err != nil { + return + } + n = copy(b, (*rbuf)[hlen:n]) + + addr, err = net.ResolveUDPAddr("udp", socksAddr.String()) + return +} + +func (c *udpConn) Read(b []byte) (n int, err error) { + n, _, err = c.ReadFrom(b) + return +} + +func (c *udpConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + wbuf := bufpool.Get(c.bufferSize) + defer bufpool.Put(wbuf) + + socksAddr := gosocks5.Addr{} + if err = socksAddr.ParseFrom(addr.String()); err != nil { + return + } + + header := gosocks5.UDPHeader{ + Addr: &socksAddr, + } + dgram := gosocks5.UDPDatagram{ + Header: &header, + Data: b, + } + + buf := bytes.NewBuffer((*wbuf)[:0]) + _, err = dgram.WriteTo(buf) + if err != nil { + return + } + + _, err = c.PacketConn.WriteTo(buf.Bytes(), c.raddr) + n = len(b) + + return +} + +func (c *udpConn) Write(b []byte) (n int, err error) { + return c.WriteTo(b, c.taddr) +} + +func (c *udpConn) RemoteAddr() net.Addr { + return c.raddr +} diff --git a/internal/util/ss/conn.go b/internal/util/ss/conn.go new file mode 100644 index 0000000..8bc2785 --- /dev/null +++ b/internal/util/ss/conn.go @@ -0,0 +1,96 @@ +package ss + +import ( + "bytes" + "net" + + "github.com/go-gost/gosocks5" + "github.com/go-gost/gost/v3/pkg/common/bufpool" +) + +var ( + DefaultBufferSize = 4096 +) + +var ( + _ net.PacketConn = (*UDPConn)(nil) + _ net.Conn = (*UDPConn)(nil) +) + +type UDPConn struct { + net.PacketConn + raddr net.Addr + taddr net.Addr + bufferSize int +} + +func UDPClientConn(c net.PacketConn, remoteAddr, targetAddr net.Addr, bufferSize int) *UDPConn { + return &UDPConn{ + PacketConn: c, + raddr: remoteAddr, + taddr: targetAddr, + bufferSize: bufferSize, + } +} + +func UDPServerConn(c net.PacketConn, remoteAddr net.Addr, bufferSize int) *UDPConn { + return &UDPConn{ + PacketConn: c, + raddr: remoteAddr, + bufferSize: bufferSize, + } +} + +func (c *UDPConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + rbuf := bufpool.Get(c.bufferSize) + defer bufpool.Put(rbuf) + + n, _, err = c.PacketConn.ReadFrom(*rbuf) + if err != nil { + return + } + + saddr := gosocks5.Addr{} + addrLen, err := saddr.ReadFrom(bytes.NewReader((*rbuf)[:n])) + if err != nil { + return + } + + n = copy(b, (*rbuf)[addrLen:n]) + addr, err = net.ResolveUDPAddr("udp", saddr.String()) + + return +} + +func (c *UDPConn) Read(b []byte) (n int, err error) { + n, _, err = c.ReadFrom(b) + return +} + +func (c *UDPConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + wbuf := bufpool.Get(c.bufferSize) + defer bufpool.Put(wbuf) + + socksAddr := gosocks5.Addr{} + if err = socksAddr.ParseFrom(addr.String()); err != nil { + return + } + + addrLen, err := socksAddr.Encode(*wbuf) + if err != nil { + return + } + + n = copy((*wbuf)[addrLen:], b) + _, err = c.PacketConn.WriteTo((*wbuf)[:addrLen+n], c.raddr) + + return +} + +func (c *UDPConn) Write(b []byte) (n int, err error) { + return c.WriteTo(b, c.taddr) +} + +func (c *UDPConn) RemoteAddr() net.Addr { + return c.raddr +} diff --git a/internal/util/ss/ss.go b/internal/util/ss/ss.go new file mode 100644 index 0000000..1a26bb3 --- /dev/null +++ b/internal/util/ss/ss.go @@ -0,0 +1,60 @@ +package ss + +import ( + "bytes" + "net" + + "github.com/shadowsocks/go-shadowsocks2/core" + ss "github.com/shadowsocks/shadowsocks-go/shadowsocks" +) + +type shadowCipher struct { + cipher *ss.Cipher +} + +func (c *shadowCipher) StreamConn(conn net.Conn) net.Conn { + return ss.NewConn(conn, c.cipher.Copy()) +} + +func (c *shadowCipher) PacketConn(conn net.PacketConn) net.PacketConn { + return ss.NewSecurePacketConn(conn, c.cipher.Copy()) +} + +func ShadowCipher(method, password string, key string) (core.Cipher, error) { + if method == "" && password == "" { + return nil, nil + } + + c, _ := ss.NewCipher(method, password) + if c != nil { + return &shadowCipher{cipher: c}, nil + } + + return core.PickCipher(method, []byte(key), password) +} + +// Due to in/out byte length is inconsistent of the shadowsocks.Conn.Write, +// we wrap around it to make io.Copy happy. +type shadowConn struct { + net.Conn + wbuf *bytes.Buffer +} + +func ShadowConn(conn net.Conn, header []byte) net.Conn { + return &shadowConn{ + Conn: conn, + wbuf: bytes.NewBuffer(header), + } +} + +func (c *shadowConn) Write(b []byte) (n int, err error) { + n = len(b) // force byte length consistent + if c.wbuf.Len() > 0 { + c.wbuf.Write(b) // append the data to the cached header + _, err = c.Conn.Write(c.wbuf.Bytes()) + c.wbuf.Reset() + return + } + _, err = c.Conn.Write(b) + return +} diff --git a/internal/util/ssh/conn.go b/internal/util/ssh/conn.go new file mode 100644 index 0000000..7cb9382 --- /dev/null +++ b/internal/util/ssh/conn.go @@ -0,0 +1,48 @@ +package ssh + +import ( + "net" + + "golang.org/x/crypto/ssh" +) + +// a dummy ssh client conn used by client connector +type ClientConn struct { + net.Conn + client *ssh.Client +} + +func NewClientConn(conn net.Conn, client *ssh.Client) net.Conn { + return &ClientConn{ + Conn: conn, + client: client, + } +} + +func (c *ClientConn) Client() *ssh.Client { + return c.client +} + +type sshConn struct { + channel ssh.Channel + net.Conn +} + +func NewConn(conn net.Conn, channel ssh.Channel) net.Conn { + return &sshConn{ + Conn: conn, + channel: channel, + } +} + +func (c *sshConn) Read(b []byte) (n int, err error) { + return c.channel.Read(b) +} + +func (c *sshConn) Write(b []byte) (n int, err error) { + return c.channel.Write(b) +} + +func (c *sshConn) Close() error { + return c.channel.Close() +} diff --git a/internal/util/ssh/ssh.go b/internal/util/ssh/ssh.go new file mode 100644 index 0000000..28ca2b6 --- /dev/null +++ b/internal/util/ssh/ssh.go @@ -0,0 +1,75 @@ +package ssh + +import ( + "errors" + "fmt" + "io/ioutil" + + "github.com/go-gost/gost/v3/pkg/auth" + "golang.org/x/crypto/ssh" +) + +const ( + GostSSHTunnelRequest = "gost-tunnel" // extended request type for ssh tunnel +) + +var ( + ErrSessionDead = errors.New("session is dead") +) + +// PasswordCallbackFunc is a callback function used by SSH server. +// It authenticates user using a password. +type PasswordCallbackFunc func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) + +func PasswordCallback(au auth.Authenticator) PasswordCallbackFunc { + if au == nil { + return nil + } + return func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) { + if au.Authenticate(conn.User(), string(password)) { + return nil, nil + } + return nil, fmt.Errorf("password rejected for %s", conn.User()) + } +} + +// PublicKeyCallbackFunc is a callback function used by SSH server. +// It offers a public key for authentication. +type PublicKeyCallbackFunc func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) + +func PublicKeyCallback(keys map[string]bool) PublicKeyCallbackFunc { + if len(keys) == 0 { + return nil + } + + return func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { + if keys[string(pubKey.Marshal())] { + return &ssh.Permissions{ + // Record the public key used for authentication. + Extensions: map[string]string{ + "pubkey-fp": ssh.FingerprintSHA256(pubKey), + }, + }, nil + } + return nil, fmt.Errorf("unknown public key for %q", c.User()) + } +} + +// ParseSSHAuthorizedKeysFile parses ssh authorized keys file. +func ParseAuthorizedKeysFile(name string) (map[string]bool, error) { + authorizedKeysBytes, err := ioutil.ReadFile(name) + if err != nil { + return nil, err + } + authorizedKeysMap := make(map[string]bool) + for len(authorizedKeysBytes) > 0 { + pubKey, _, _, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes) + if err != nil { + return nil, err + } + authorizedKeysMap[string(pubKey.Marshal())] = true + authorizedKeysBytes = rest + } + + return authorizedKeysMap, nil +} diff --git a/internal/util/sshd/conn.go b/internal/util/sshd/conn.go new file mode 100644 index 0000000..592a961 --- /dev/null +++ b/internal/util/sshd/conn.go @@ -0,0 +1,118 @@ +package sshd + +import ( + "context" + "errors" + "net" + "time" + + "golang.org/x/crypto/ssh" +) + +type DirectForwardConn struct { + conn ssh.Conn + channel ssh.Channel + dstAddr string +} + +func NewDirectForwardConn(conn ssh.Conn, channel ssh.Channel, dstAddr string) net.Conn { + return &DirectForwardConn{ + conn: conn, + channel: channel, + dstAddr: dstAddr, + } +} + +func (c *DirectForwardConn) Read(b []byte) (n int, err error) { + return c.channel.Read(b) +} + +func (c *DirectForwardConn) Write(b []byte) (n int, err error) { + return c.channel.Write(b) +} + +func (c *DirectForwardConn) Close() error { + return c.channel.Close() +} + +func (c *DirectForwardConn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *DirectForwardConn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +func (c *DirectForwardConn) SetDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "nop", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *DirectForwardConn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "nop", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *DirectForwardConn) SetWriteDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "nop", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *DirectForwardConn) DstAddr() string { + return c.dstAddr +} + +type RemoteForwardConn struct { + ctx context.Context + conn ssh.Conn + req *ssh.Request +} + +func NewRemoteForwardConn(ctx context.Context, conn ssh.Conn, req *ssh.Request) net.Conn { + return &RemoteForwardConn{ + ctx: ctx, + conn: conn, + req: req, + } +} + +func (c *RemoteForwardConn) Conn() ssh.Conn { + return c.conn +} + +func (c *RemoteForwardConn) Request() *ssh.Request { + return c.req +} + +func (c *RemoteForwardConn) Read(b []byte) (n int, err error) { + return 0, &net.OpError{Op: "read", Net: "nop", Source: nil, Addr: nil, Err: errors.New("read not supported")} +} + +func (c *RemoteForwardConn) Write(b []byte) (n int, err error) { + return 0, &net.OpError{Op: "write", Net: "nop", Source: nil, Addr: nil, Err: errors.New("write not supported")} +} + +func (c *RemoteForwardConn) Close() error { + return &net.OpError{Op: "close", Net: "nop", Source: nil, Addr: nil, Err: errors.New("close not supported")} +} + +func (c *RemoteForwardConn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *RemoteForwardConn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +func (c *RemoteForwardConn) SetDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "nop", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *RemoteForwardConn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "nop", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *RemoteForwardConn) SetWriteDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "nop", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *RemoteForwardConn) Done() <-chan struct{} { + return c.ctx.Done() +} diff --git a/internal/util/tap/config.go b/internal/util/tap/config.go new file mode 100644 index 0000000..77895ce --- /dev/null +++ b/internal/util/tap/config.go @@ -0,0 +1,9 @@ +package tap + +type Config struct { + Name string + Net string + MTU int + Routes []string + Gateway string +} diff --git a/internal/util/tap/conn.go b/internal/util/tap/conn.go new file mode 100644 index 0000000..453444f --- /dev/null +++ b/internal/util/tap/conn.go @@ -0,0 +1,61 @@ +package tap + +import ( + "errors" + "net" + "time" + + "github.com/songgao/water" +) + +type Conn struct { + config *Config + ifce *water.Interface + laddr net.Addr + raddr net.Addr +} + +func NewConn(config *Config, ifce *water.Interface, laddr, raddr net.Addr) *Conn { + return &Conn{ + config: config, + ifce: ifce, + laddr: laddr, + raddr: raddr, + } +} + +func (c *Conn) Config() *Config { + return c.config +} + +func (c *Conn) Read(b []byte) (n int, err error) { + return c.ifce.Read(b) +} + +func (c *Conn) Write(b []byte) (n int, err error) { + return c.ifce.Write(b) +} + +func (c *Conn) Close() (err error) { + return c.ifce.Close() +} + +func (c *Conn) LocalAddr() net.Addr { + return c.laddr +} + +func (c *Conn) RemoteAddr() net.Addr { + return c.raddr +} + +func (c *Conn) SetDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "tuntap", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *Conn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "tuntap", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *Conn) SetWriteDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "tuntap", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} diff --git a/internal/util/tun/config.go b/internal/util/tun/config.go new file mode 100644 index 0000000..6448cae --- /dev/null +++ b/internal/util/tun/config.go @@ -0,0 +1,19 @@ +package tun + +import "net" + +// Route is an IP routing entry +type Route struct { + Net net.IPNet + Gateway net.IP +} + +type Config struct { + Name string + Net string + // peer addr of point-to-point on MacOS + Peer string + MTU int + Gateway string + Routes []Route +} diff --git a/internal/util/tun/conn.go b/internal/util/tun/conn.go new file mode 100644 index 0000000..05477f9 --- /dev/null +++ b/internal/util/tun/conn.go @@ -0,0 +1,61 @@ +package tun + +import ( + "errors" + "net" + "time" + + "github.com/songgao/water" +) + +type Conn struct { + config *Config + ifce *water.Interface + laddr net.Addr + raddr net.Addr +} + +func NewConn(config *Config, ifce *water.Interface, laddr, raddr net.Addr) *Conn { + return &Conn{ + config: config, + ifce: ifce, + laddr: laddr, + raddr: raddr, + } +} + +func (c *Conn) Config() *Config { + return c.config +} + +func (c *Conn) Read(b []byte) (n int, err error) { + return c.ifce.Read(b) +} + +func (c *Conn) Write(b []byte) (n int, err error) { + return c.ifce.Write(b) +} + +func (c *Conn) Close() (err error) { + return c.ifce.Close() +} + +func (c *Conn) LocalAddr() net.Addr { + return c.laddr +} + +func (c *Conn) RemoteAddr() net.Addr { + return c.raddr +} + +func (c *Conn) SetDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "tuntap", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *Conn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "tuntap", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *Conn) SetWriteDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "tuntap", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} diff --git a/internal/util/ws/ws.go b/internal/util/ws/ws.go new file mode 100644 index 0000000..da4beaa --- /dev/null +++ b/internal/util/ws/ws.go @@ -0,0 +1,56 @@ +package ws + +import ( + "net" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +type WebsocketConn interface { + net.Conn + WriteMessage(int, []byte) error + ReadMessage() (int, []byte, error) +} + +type websocketConn struct { + *websocket.Conn + rb []byte + mux sync.Mutex +} + +func Conn(conn *websocket.Conn) WebsocketConn { + return &websocketConn{ + Conn: conn, + } +} + +func (c *websocketConn) Read(b []byte) (n int, err error) { + if len(c.rb) == 0 { + _, c.rb, err = c.Conn.ReadMessage() + } + n = copy(b, c.rb) + c.rb = c.rb[n:] + return +} + +func (c *websocketConn) Write(b []byte) (n int, err error) { + err = c.WriteMessage(websocket.BinaryMessage, b) + n = len(b) + return +} + +func (c *websocketConn) WriteMessage(messageType int, data []byte) error { + c.mux.Lock() + defer c.mux.Unlock() + + return c.Conn.WriteMessage(messageType, data) +} + +func (c *websocketConn) SetDeadline(t time.Time) error { + if err := c.SetReadDeadline(t); err != nil { + return err + } + return c.SetWriteDeadline(t) +} diff --git a/listener/dns/listener.go b/listener/dns/listener.go new file mode 100644 index 0000000..74cc6a4 --- /dev/null +++ b/listener/dns/listener.go @@ -0,0 +1,210 @@ +package dns + +import ( + "bytes" + "encoding/base64" + "errors" + "io/ioutil" + "net" + "net/http" + "strings" + + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "github.com/miekg/dns" +) + +func init() { + registry.ListenerRegistry().Register("dns", NewListener) +} + +type dnsListener struct { + addr net.Addr + server Server + cqueue chan net.Conn + errChan chan error + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &dnsListener{ + logger: options.Logger, + options: options, + } +} + +func (l *dnsListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + l.addr, err = net.ResolveTCPAddr("tcp", l.options.Addr) + if err != nil { + return err + } + + switch strings.ToLower(l.md.mode) { + case "tcp": + l.server = &dns.Server{ + Net: "tcp", + Addr: l.options.Addr, + Handler: l, + ReadTimeout: l.md.readTimeout, + WriteTimeout: l.md.writeTimeout, + } + case "tls": + l.server = &dns.Server{ + Net: "tcp-tls", + Addr: l.options.Addr, + Handler: l, + TLSConfig: l.options.TLSConfig, + ReadTimeout: l.md.readTimeout, + WriteTimeout: l.md.writeTimeout, + } + case "https": + l.server = &dohServer{ + addr: l.options.Addr, + tlsConfig: l.options.TLSConfig, + server: &http.Server{ + Handler: l, + ReadTimeout: l.md.readTimeout, + WriteTimeout: l.md.writeTimeout, + }, + } + default: + l.addr, err = net.ResolveUDPAddr("udp", l.options.Addr) + l.server = &dns.Server{ + Net: "udp", + Addr: l.options.Addr, + Handler: l, + UDPSize: l.md.readBufferSize, + ReadTimeout: l.md.readTimeout, + WriteTimeout: l.md.writeTimeout, + } + } + + if err != nil { + return + } + + l.cqueue = make(chan net.Conn, l.md.backlog) + l.errChan = make(chan error, 1) + + go func() { + err := l.server.ListenAndServe() + if err != nil { + l.errChan <- err + } + close(l.errChan) + }() + return +} + +func (l *dnsListener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.cqueue: + conn = metrics.WrapConn(l.options.Service, conn) + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *dnsListener) Close() error { + return l.server.Shutdown() +} + +func (l *dnsListener) Addr() net.Addr { + return l.addr +} + +func (l *dnsListener) ServeDNS(w dns.ResponseWriter, m *dns.Msg) { + b, err := m.Pack() + if err != nil { + l.logger.Error(err) + return + } + if err := l.serve(w, b); err != nil { + l.logger.Error(err) + } +} + +// Based on https://github.com/semihalev/sdns +func (l *dnsListener) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var buf []byte + var err error + switch r.Method { + case http.MethodGet: + buf, err = base64.RawURLEncoding.DecodeString(r.URL.Query().Get("dns")) + if len(buf) == 0 || err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + case http.MethodPost: + if ct := r.Header.Get("Content-Type"); ct != "application/dns-message" { + l.logger.Errorf("unsupported media type: %s", ct) + http.Error(w, http.StatusText(http.StatusUnsupportedMediaType), http.StatusUnsupportedMediaType) + return + } + + buf, err = ioutil.ReadAll(r.Body) + if err != nil { + l.logger.Error(err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + default: + l.logger.Errorf("method not allowd: %s", r.Method) + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + mq := &dns.Msg{} + if err := mq.Unpack(buf); err != nil { + l.logger.Error(err) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + w.Header().Set("Server", "SDNS") + w.Header().Set("Content-Type", "application/dns-message") + + raddr, _ := net.ResolveTCPAddr("tcp", r.RemoteAddr) + if raddr == nil { + raddr = &net.TCPAddr{} + } + if err := l.serve(&dohResponseWriter{raddr: raddr, ResponseWriter: w}, buf); err != nil { + l.logger.Error(err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } +} + +func (l *dnsListener) serve(w ResponseWriter, msg []byte) (err error) { + conn := &serverConn{ + r: bytes.NewReader(msg), + w: w, + laddr: l.addr, + closed: make(chan struct{}), + } + + select { + case l.cqueue <- conn: + default: + l.logger.Warnf("connection queue is full, client %s discarded", w.RemoteAddr()) + return errors.New("connection queue is full") + } + + return conn.Wait() +} diff --git a/listener/dns/metadata.go b/listener/dns/metadata.go new file mode 100644 index 0000000..538faf7 --- /dev/null +++ b/listener/dns/metadata.go @@ -0,0 +1,41 @@ +package dns + +import ( + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultBacklog = 128 +) + +type metadata struct { + mode string + readBufferSize int + readTimeout time.Duration + writeTimeout time.Duration + backlog int +} + +func (l *dnsListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + backlog = "backlog" + mode = "mode" + readBufferSize = "readBufferSize" + readTimeout = "readTimeout" + writeTimeout = "writeTimeout" + ) + + l.md.mode = mdata.GetString(md, mode) + l.md.readBufferSize = mdata.GetInt(md, readBufferSize) + l.md.readTimeout = mdata.GetDuration(md, readTimeout) + l.md.writeTimeout = mdata.GetDuration(md, writeTimeout) + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + return +} diff --git a/listener/dns/server.go b/listener/dns/server.go new file mode 100644 index 0000000..5717438 --- /dev/null +++ b/listener/dns/server.go @@ -0,0 +1,110 @@ +package dns + +import ( + "context" + "crypto/tls" + "errors" + "io" + "net" + "net/http" + "time" +) + +type Server interface { + ListenAndServe() error + Shutdown() error +} + +type dohServer struct { + addr string + tlsConfig *tls.Config + server *http.Server +} + +func (s *dohServer) ListenAndServe() error { + ln, err := net.Listen("tcp", s.addr) + if err != nil { + return err + } + ln = tls.NewListener(ln, s.tlsConfig) + return s.server.Serve(ln) +} + +func (s *dohServer) Shutdown() error { + return s.server.Shutdown(context.Background()) +} + +type ResponseWriter interface { + io.Writer + RemoteAddr() net.Addr +} + +type dohResponseWriter struct { + raddr net.Addr + http.ResponseWriter +} + +func (w *dohResponseWriter) RemoteAddr() net.Addr { + return w.raddr +} + +type serverConn struct { + r io.Reader + w ResponseWriter + laddr net.Addr + closed chan struct{} +} + +func (c *serverConn) Read(b []byte) (n int, err error) { + select { + case <-c.closed: + err = io.ErrClosedPipe + return + default: + return c.r.Read(b) + } +} + +func (c *serverConn) Write(b []byte) (n int, err error) { + select { + case <-c.closed: + err = io.ErrClosedPipe + return + default: + return c.w.Write(b) + } +} + +func (c *serverConn) Close() error { + select { + case <-c.closed: + default: + close(c.closed) + } + return nil +} + +func (c *serverConn) Wait() error { + <-c.closed + return nil +} + +func (c *serverConn) LocalAddr() net.Addr { + return c.laddr +} + +func (c *serverConn) RemoteAddr() net.Addr { + return c.w.RemoteAddr() +} + +func (c *serverConn) SetDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "dns", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *serverConn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "dns", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *serverConn) SetWriteDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "dns", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} diff --git a/listener/ftcp/conn.go b/listener/ftcp/conn.go new file mode 100644 index 0000000..df02303 --- /dev/null +++ b/listener/ftcp/conn.go @@ -0,0 +1,124 @@ +package ftcp + +import ( + "errors" + "net" + "sync" + "sync/atomic" + "time" +) + +// serverConn is a server side connection for UDP client peer, it implements net.Conn and net.PacketConn. +type serverConn struct { + pc net.PacketConn + raddr net.Addr + rc chan []byte // data receive queue + fresh int32 + closed chan struct{} + closeMutex sync.Mutex + config *serverConnConfig +} + +type serverConnConfig struct { + ttl time.Duration + qsize int + onClose func() +} + +func newServerConn(conn net.PacketConn, raddr net.Addr, cfg *serverConnConfig) *serverConn { + if conn == nil || raddr == nil { + return nil + } + + if cfg == nil { + cfg = &serverConnConfig{} + } + c := &serverConn{ + pc: conn, + raddr: raddr, + rc: make(chan []byte, cfg.qsize), + closed: make(chan struct{}), + config: cfg, + } + go c.ttlWait() + return c +} + +func (c *serverConn) send(b []byte) error { + select { + case c.rc <- b: + return nil + default: + return errors.New("queue is full") + } +} + +func (c *serverConn) Read(b []byte) (n int, err error) { + select { + case bb := <-c.rc: + n = copy(b, bb) + atomic.StoreInt32(&c.fresh, 1) + case <-c.closed: + err = errors.New("read from closed connection") + return + } + + return +} + +func (c *serverConn) Write(b []byte) (n int, err error) { + return c.pc.WriteTo(b, c.raddr) +} + +func (c *serverConn) Close() error { + c.closeMutex.Lock() + defer c.closeMutex.Unlock() + + select { + case <-c.closed: + return errors.New("connection is closed") + default: + if c.config.onClose != nil { + c.config.onClose() + } + close(c.closed) + } + return nil +} + +func (c *serverConn) LocalAddr() net.Addr { + return c.pc.LocalAddr() +} + +func (c *serverConn) RemoteAddr() net.Addr { + return c.raddr +} + +func (c *serverConn) SetDeadline(t time.Time) error { + return c.pc.SetDeadline(t) +} + +func (c *serverConn) SetReadDeadline(t time.Time) error { + return c.pc.SetReadDeadline(t) +} + +func (c *serverConn) SetWriteDeadline(t time.Time) error { + return c.pc.SetWriteDeadline(t) +} + +func (c *serverConn) ttlWait() { + ticker := time.NewTicker(c.config.ttl) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if !atomic.CompareAndSwapInt32(&c.fresh, 1, 0) { + c.Close() + return + } + case <-c.closed: + return + } + } +} diff --git a/listener/ftcp/listener.go b/listener/ftcp/listener.go new file mode 100644 index 0000000..639188a --- /dev/null +++ b/listener/ftcp/listener.go @@ -0,0 +1,159 @@ +package ftcp + +import ( + "net" + "sync" + "sync/atomic" + + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "github.com/xtaci/tcpraw" +) + +func init() { + registry.ListenerRegistry().Register("ftcp", NewListener) +} + +type ftcpListener struct { + conn net.PacketConn + connChan chan net.Conn + errChan chan error + connPool connPool + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &ftcpListener{ + logger: options.Logger, + options: options, + } +} + +func (l *ftcpListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + l.conn, err = tcpraw.Listen("tcp", l.options.Addr) + if err != nil { + return + } + + l.connChan = make(chan net.Conn, l.md.connQueueSize) + l.errChan = make(chan error, 1) + + go l.listenLoop() + + return +} + +func (l *ftcpListener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.connChan: + conn = metrics.WrapConn(l.options.Service, conn) + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *ftcpListener) Close() error { + err := l.conn.Close() + l.connPool.Range(func(k any, v *serverConn) bool { + v.Close() + return true + }) + return err +} + +func (l *ftcpListener) Addr() net.Addr { + return l.conn.LocalAddr() +} + +func (l *ftcpListener) listenLoop() { + for { + b := make([]byte, l.md.readBufferSize) + + n, raddr, err := l.conn.ReadFrom(b) + if err != nil { + l.logger.Error("accept:", err) + l.errChan <- err + close(l.errChan) + return + } + + conn, ok := l.connPool.Get(raddr.String()) + if !ok { + conn = newServerConn(l.conn, raddr, + &serverConnConfig{ + ttl: l.md.ttl, + qsize: l.md.readQueueSize, + onClose: func() { + l.connPool.Delete(raddr.String()) + }, + }) + + select { + case l.connChan <- conn: + l.connPool.Set(raddr.String(), conn) + default: + conn.Close() + l.logger.Error("connection queue is full") + } + } + + if err := conn.send(b[:n]); err != nil { + l.logger.Warn("data discarded:", err) + } + l.logger.Debug("recv", n) + } +} + +func (l *ftcpListener) parseMetadata(md md.Metadata) (err error) { + return +} + +type connPool struct { + size int64 + m sync.Map +} + +func (p *connPool) Get(key any) (conn *serverConn, ok bool) { + v, ok := p.m.Load(key) + if ok { + conn, ok = v.(*serverConn) + } + return +} + +func (p *connPool) Set(key any, conn *serverConn) { + p.m.Store(key, conn) + atomic.AddInt64(&p.size, 1) +} + +func (p *connPool) Delete(key any) { + p.m.Delete(key) + atomic.AddInt64(&p.size, -1) +} + +func (p *connPool) Range(f func(key any, value *serverConn) bool) { + p.m.Range(func(k, v any) bool { + return f(k, v.(*serverConn)) + }) +} + +func (p *connPool) Size() int64 { + return atomic.LoadInt64(&p.size) +} diff --git a/listener/ftcp/metadata.go b/listener/ftcp/metadata.go new file mode 100644 index 0000000..694c856 --- /dev/null +++ b/listener/ftcp/metadata.go @@ -0,0 +1,22 @@ +package ftcp + +import "time" + +const ( + defaultTTL = 60 * time.Second + defaultReadBufferSize = 1024 + defaultReadQueueSize = 128 + defaultConnQueueSize = 128 +) + +const ( + addr = "addr" +) + +type metadata struct { + ttl time.Duration + + readBufferSize int + readQueueSize int + connQueueSize int +} diff --git a/listener/grpc/listener.go b/listener/grpc/listener.go new file mode 100644 index 0000000..10e89fd --- /dev/null +++ b/listener/grpc/listener.go @@ -0,0 +1,100 @@ +package grpc + +import ( + "net" + + "github.com/go-gost/gost/v3/pkg/common/admission" + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + pb "github.com/go-gost/x/internal/util/grpc/proto" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func init() { + registry.ListenerRegistry().Register("grpc", NewListener) +} + +type grpcListener struct { + addr net.Addr + server *grpc.Server + cqueue chan net.Conn + errChan chan error + md metadata + logger logger.Logger + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &grpcListener{ + logger: options.Logger, + options: options, + } +} + +func (l *grpcListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + ln, err := net.Listen("tcp", l.options.Addr) + if err != nil { + return + } + ln = metrics.WrapListener(l.options.Service, ln) + ln = admission.WrapListener(l.options.Admission, ln) + + var opts []grpc.ServerOption + if !l.md.insecure { + opts = append(opts, grpc.Creds(credentials.NewTLS(l.options.TLSConfig))) + } + + l.server = grpc.NewServer(opts...) + l.addr = ln.Addr() + l.cqueue = make(chan net.Conn, l.md.backlog) + l.errChan = make(chan error, 1) + + pb.RegisterGostTunelServer(l.server, &server{ + cqueue: l.cqueue, + localAddr: l.addr, + logger: l.options.Logger, + }) + + go func() { + err := l.server.Serve(ln) + if err != nil { + l.errChan <- err + } + close(l.errChan) + }() + + return +} + +func (l *grpcListener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.cqueue: + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *grpcListener) Close() error { + l.server.Stop() + return nil +} + +func (l *grpcListener) Addr() net.Addr { + return l.addr +} diff --git a/listener/grpc/metadata.go b/listener/grpc/metadata.go new file mode 100644 index 0000000..6c0e6d4 --- /dev/null +++ b/listener/grpc/metadata.go @@ -0,0 +1,29 @@ +package grpc + +import ( + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultBacklog = 128 +) + +type metadata struct { + backlog int + insecure bool +} + +func (l *grpcListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + backlog = "backlog" + insecure = "grpcInsecure" + ) + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + l.md.insecure = mdata.GetBool(md, insecure) + return +} diff --git a/listener/grpc/server.go b/listener/grpc/server.go new file mode 100644 index 0000000..4ab6524 --- /dev/null +++ b/listener/grpc/server.go @@ -0,0 +1,124 @@ +package grpc + +import ( + "errors" + "io" + "net" + "time" + + "github.com/go-gost/gost/v3/pkg/logger" + pb "github.com/go-gost/x/internal/util/grpc/proto" + "google.golang.org/grpc/peer" +) + +type server struct { + cqueue chan net.Conn + localAddr net.Addr + pb.UnimplementedGostTunelServer + logger logger.Logger +} + +func (s *server) Tunnel(srv pb.GostTunel_TunnelServer) error { + c := &conn{ + s: srv, + localAddr: s.localAddr, + remoteAddr: &net.TCPAddr{}, + closed: make(chan struct{}), + } + if p, ok := peer.FromContext(srv.Context()); ok { + c.remoteAddr = p.Addr + } + + select { + case s.cqueue <- c: + default: + c.Close() + s.logger.Warnf("connection queue is full, client discarded") + } + + <-c.closed + + return nil +} + +type conn struct { + s pb.GostTunel_TunnelServer + rb []byte + localAddr net.Addr + remoteAddr net.Addr + closed chan struct{} +} + +func (c *conn) Read(b []byte) (n int, err error) { + select { + case <-c.s.Context().Done(): + err = c.s.Context().Err() + return + case <-c.closed: + err = io.ErrClosedPipe + return + default: + } + + if len(c.rb) == 0 { + chunk, err := c.s.Recv() + if err != nil { + return 0, err + } + c.rb = chunk.Data + } + + n = copy(b, c.rb) + c.rb = c.rb[n:] + return +} + +func (c *conn) Write(b []byte) (n int, err error) { + select { + case <-c.s.Context().Done(): + err = c.s.Context().Err() + return + case <-c.closed: + err = io.ErrClosedPipe + return + default: + } + + if err = c.s.Send(&pb.Chunk{ + Data: b, + }); err != nil { + return + } + n = len(b) + return +} + +func (c *conn) Close() error { + select { + case <-c.closed: + default: + close(c.closed) + } + + return nil +} + +func (c *conn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *conn) RemoteAddr() net.Addr { + return c.remoteAddr +} + +func (c *conn) SetDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "grpc", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *conn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "grpc", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *conn) SetWriteDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "grpc", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} diff --git a/listener/http2/conn.go b/listener/http2/conn.go new file mode 100644 index 0000000..c145860 --- /dev/null +++ b/listener/http2/conn.go @@ -0,0 +1,54 @@ +package http2 + +import ( + "errors" + "net" + "net/http" + "time" +) + +// a dummy HTTP2 server conn used by HTTP2 handler +type conn struct { + r *http.Request + w http.ResponseWriter + closed chan struct{} +} + +func (c *conn) 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 *conn) 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 *conn) Close() error { + select { + case <-c.closed: + default: + close(c.closed) + } + return nil +} + +func (c *conn) LocalAddr() net.Addr { + addr, _ := net.ResolveTCPAddr("tcp", c.r.Host) + return addr +} + +func (c *conn) RemoteAddr() net.Addr { + addr, _ := net.ResolveTCPAddr("tcp", c.r.RemoteAddr) + return addr +} + +func (c *conn) SetDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *conn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *conn) SetWriteDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} diff --git a/listener/http2/h2/conn.go b/listener/http2/h2/conn.go new file mode 100644 index 0000000..10f8d4c --- /dev/null +++ b/listener/http2/h2/conn.go @@ -0,0 +1,89 @@ +package h2 + +import ( + "errors" + "io" + "net" + "net/http" + "time" +) + +// HTTP2 connection, wrapped up just like a net.Conn +type conn struct { + r io.Reader + w io.Writer + remoteAddr net.Addr + localAddr net.Addr + closed chan struct{} +} + +func (c *conn) Read(b []byte) (n int, err error) { + return c.r.Read(b) +} + +func (c *conn) Write(b []byte) (n int, err error) { + return c.w.Write(b) +} + +func (c *conn) Close() (err error) { + select { + case <-c.closed: + return + default: + close(c.closed) + } + 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 *conn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *conn) RemoteAddr() net.Addr { + return c.remoteAddr +} + +func (c *conn) SetDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *conn) SetReadDeadline(t time.Time) error { + return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} + +func (c *conn) 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) + // log.Log("[http2]", err) + 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/listener/http2/h2/listener.go b/listener/http2/h2/listener.go new file mode 100644 index 0000000..92540c0 --- /dev/null +++ b/listener/http2/h2/listener.go @@ -0,0 +1,178 @@ +package h2 + +import ( + "crypto/tls" + "errors" + "net" + "net/http" + "net/http/httputil" + + "github.com/go-gost/gost/v3/pkg/common/admission" + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +func init() { + registry.ListenerRegistry().Register("h2c", NewListener) + registry.ListenerRegistry().Register("h2", NewTLSListener) +} + +type h2Listener struct { + server *http.Server + addr net.Addr + cqueue chan net.Conn + errChan chan error + logger logger.Logger + md metadata + h2c bool + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &h2Listener{ + h2c: true, + logger: options.Logger, + options: options, + } +} + +func NewTLSListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &h2Listener{ + logger: options.Logger, + options: options, + } +} + +func (l *h2Listener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + l.server = &http.Server{ + Addr: l.options.Addr, + } + + ln, err := net.Listen("tcp", l.options.Addr) + if err != nil { + return err + } + l.addr = ln.Addr() + ln = metrics.WrapListener(l.options.Service, ln) + ln = admission.WrapListener(l.options.Admission, ln) + + if l.h2c { + l.server.Handler = h2c.NewHandler( + http.HandlerFunc(l.handleFunc), &http2.Server{}) + } else { + l.server.Handler = http.HandlerFunc(l.handleFunc) + l.server.TLSConfig = l.options.TLSConfig + if err := http2.ConfigureServer(l.server, nil); err != nil { + ln.Close() + return err + } + ln = tls.NewListener(ln, l.options.TLSConfig) + } + + l.cqueue = make(chan net.Conn, l.md.backlog) + l.errChan = make(chan error, 1) + + go func() { + if err := l.server.Serve(ln); err != nil { + l.logger.Error(err) + } + }() + + return +} + +func (l *h2Listener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.cqueue: + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *h2Listener) Addr() net.Addr { + return l.addr +} + +func (l *h2Listener) Close() (err error) { + select { + case <-l.errChan: + default: + err = l.server.Close() + l.errChan <- err + close(l.errChan) + } + return nil +} + +func (l *h2Listener) handleFunc(w http.ResponseWriter, r *http.Request) { + if l.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpRequest(r, false) + l.logger.Debug(string(dump)) + } + conn, err := l.upgrade(w, r) + if err != nil { + l.logger.Error(err) + return + } + select { + case l.cqueue <- conn: + default: + conn.Close() + l.logger.Warnf("connection queue is full, client %s discarded", r.RemoteAddr) + } + + <-conn.closed // NOTE: we need to wait for streaming end, or the connection will be closed +} + +func (l *h2Listener) upgrade(w http.ResponseWriter, r *http.Request) (*conn, error) { + if l.md.path == "" && r.Method != http.MethodConnect { + w.WriteHeader(http.StatusMethodNotAllowed) + return nil, errors.New("method not allowed") + } + + if l.md.path != "" && r.RequestURI != l.md.path { + w.WriteHeader(http.StatusBadRequest) + return nil, errors.New("bad request") + } + + 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, + } + } + return &conn{ + r: r.Body, + w: flushWriter{w}, + localAddr: l.addr, + remoteAddr: remoteAddr, + closed: make(chan struct{}), + }, nil +} diff --git a/listener/http2/h2/metadata.go b/listener/http2/h2/metadata.go new file mode 100644 index 0000000..3c8ced4 --- /dev/null +++ b/listener/http2/h2/metadata.go @@ -0,0 +1,29 @@ +package h2 + +import ( + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultBacklog = 128 +) + +type metadata struct { + path string + backlog int +} + +func (l *h2Listener) parseMetadata(md mdata.Metadata) (err error) { + const ( + path = "path" + backlog = "backlog" + ) + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + l.md.path = mdata.GetString(md, path) + return +} diff --git a/listener/http2/listener.go b/listener/http2/listener.go new file mode 100644 index 0000000..a725c64 --- /dev/null +++ b/listener/http2/listener.go @@ -0,0 +1,120 @@ +package http2 + +import ( + "crypto/tls" + "net" + "net/http" + + "github.com/go-gost/gost/v3/pkg/common/admission" + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + http2_util "github.com/go-gost/x/internal/util/http2" + "golang.org/x/net/http2" +) + +func init() { + registry.ListenerRegistry().Register("http2", NewListener) +} + +type http2Listener struct { + server *http.Server + addr net.Addr + cqueue chan net.Conn + errChan chan error + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &http2Listener{ + logger: options.Logger, + options: options, + } +} + +func (l *http2Listener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + l.server = &http.Server{ + Addr: l.options.Addr, + Handler: http.HandlerFunc(l.handleFunc), + TLSConfig: l.options.TLSConfig, + } + if err := http2.ConfigureServer(l.server, nil); err != nil { + return err + } + + ln, err := net.Listen("tcp", l.options.Addr) + if err != nil { + return err + } + l.addr = ln.Addr() + ln = metrics.WrapListener(l.options.Service, ln) + ln = admission.WrapListener(l.options.Admission, ln) + + ln = tls.NewListener( + ln, + l.options.TLSConfig, + ) + + l.cqueue = make(chan net.Conn, l.md.backlog) + l.errChan = make(chan error, 1) + + go func() { + if err := l.server.Serve(ln); err != nil { + l.logger.Error(err) + } + }() + + return +} + +func (l *http2Listener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.cqueue: + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *http2Listener) Addr() net.Addr { + return l.addr +} + +func (l *http2Listener) Close() (err error) { + select { + case <-l.errChan: + default: + err = l.server.Close() + l.errChan <- err + close(l.errChan) + } + return nil +} + +func (l *http2Listener) handleFunc(w http.ResponseWriter, r *http.Request) { + raddr, _ := net.ResolveTCPAddr("tcp", r.RemoteAddr) + conn := http2_util.NewServerConn(w, r, l.addr, raddr) + select { + case l.cqueue <- conn: + default: + l.logger.Warnf("connection queue is full, client %s discarded", r.RemoteAddr) + return + } + + <-conn.Done() +} diff --git a/listener/http2/metadata.go b/listener/http2/metadata.go new file mode 100644 index 0000000..97f2661 --- /dev/null +++ b/listener/http2/metadata.go @@ -0,0 +1,25 @@ +package http2 + +import ( + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultBacklog = 128 +) + +type metadata struct { + backlog int +} + +func (l *http2Listener) parseMetadata(md mdata.Metadata) (err error) { + const ( + backlog = "backlog" + ) + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + return +} diff --git a/listener/http3/listener.go b/listener/http3/listener.go new file mode 100644 index 0000000..60ace98 --- /dev/null +++ b/listener/http3/listener.go @@ -0,0 +1,81 @@ +package http3 + +import ( + "net" + + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + pht_util "github.com/go-gost/x/internal/util/pht" + "github.com/lucas-clemente/quic-go" +) + +func init() { + registry.ListenerRegistry().Register("http3", NewListener) + registry.ListenerRegistry().Register("h3", NewListener) +} + +type http3Listener struct { + addr net.Addr + server *pht_util.Server + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &http3Listener{ + logger: options.Logger, + options: options, + } +} + +func (l *http3Listener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + l.addr, err = net.ResolveUDPAddr("udp", l.options.Addr) + if err != nil { + return + } + + l.server = pht_util.NewHTTP3Server( + l.options.Addr, + &quic.Config{}, + pht_util.TLSConfigServerOption(l.options.TLSConfig), + pht_util.BacklogServerOption(l.md.backlog), + pht_util.PathServerOption(l.md.authorizePath, l.md.pushPath, l.md.pullPath), + pht_util.LoggerServerOption(l.options.Logger), + ) + + go func() { + if err := l.server.ListenAndServe(); err != nil { + l.logger.Error(err) + } + }() + + return +} + +func (l *http3Listener) Accept() (conn net.Conn, err error) { + conn, err = l.server.Accept() + if err != nil { + return + } + return metrics.WrapConn(l.options.Service, conn), nil +} + +func (l *http3Listener) Addr() net.Addr { + return l.addr +} + +func (l *http3Listener) Close() (err error) { + return l.server.Close() +} diff --git a/listener/http3/metadata.go b/listener/http3/metadata.go new file mode 100644 index 0000000..e2da944 --- /dev/null +++ b/listener/http3/metadata.go @@ -0,0 +1,51 @@ +package http3 + +import ( + "strings" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultAuthorizePath = "/authorize" + defaultPushPath = "/push" + defaultPullPath = "/pull" + defaultBacklog = 128 +) + +type metadata struct { + authorizePath string + pushPath string + pullPath string + backlog int +} + +func (l *http3Listener) parseMetadata(md mdata.Metadata) (err error) { + const ( + authorizePath = "authorizePath" + pushPath = "pushPath" + pullPath = "pullPath" + + backlog = "backlog" + ) + + l.md.authorizePath = mdata.GetString(md, authorizePath) + if !strings.HasPrefix(l.md.authorizePath, "/") { + l.md.authorizePath = defaultAuthorizePath + } + l.md.pushPath = mdata.GetString(md, pushPath) + if !strings.HasPrefix(l.md.pushPath, "/") { + l.md.pushPath = defaultPushPath + } + l.md.pullPath = mdata.GetString(md, pullPath) + if !strings.HasPrefix(l.md.pullPath, "/") { + l.md.pullPath = defaultPullPath + } + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + return +} diff --git a/listener/icmp/conn.go b/listener/icmp/conn.go new file mode 100644 index 0000000..ee1a26c --- /dev/null +++ b/listener/icmp/conn.go @@ -0,0 +1,21 @@ +package quic + +import ( + "net" + + "github.com/lucas-clemente/quic-go" +) + +type quicConn struct { + quic.Stream + laddr net.Addr + raddr net.Addr +} + +func (c *quicConn) LocalAddr() net.Addr { + return c.laddr +} + +func (c *quicConn) RemoteAddr() net.Addr { + return c.raddr +} diff --git a/listener/icmp/listener.go b/listener/icmp/listener.go new file mode 100644 index 0000000..8ecb5f2 --- /dev/null +++ b/listener/icmp/listener.go @@ -0,0 +1,147 @@ +package quic + +import ( + "context" + "net" + + "github.com/go-gost/gost/v3/pkg/common/admission" + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + icmp_pkg "github.com/go-gost/x/internal/util/icmp" + "github.com/lucas-clemente/quic-go" + "golang.org/x/net/icmp" +) + +func init() { + registry.ListenerRegistry().Register("icmp", NewListener) +} + +type icmpListener struct { + ln quic.Listener + cqueue chan net.Conn + errChan chan error + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &icmpListener{ + logger: options.Logger, + options: options, + } +} + +func (l *icmpListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + addr := l.options.Addr + if host, _, err := net.SplitHostPort(addr); err == nil { + addr = host + } + + var conn net.PacketConn + conn, err = icmp.ListenPacket("ip4:icmp", addr) + if err != nil { + return + } + conn = icmp_pkg.ServerConn(conn) + conn = metrics.WrapPacketConn(l.options.Service, conn) + conn = admission.WrapPacketConn(l.options.Admission, conn) + + config := &quic.Config{ + KeepAlive: l.md.keepAlive, + HandshakeIdleTimeout: l.md.handshakeTimeout, + MaxIdleTimeout: l.md.maxIdleTimeout, + Versions: []quic.VersionNumber{ + quic.Version1, + quic.VersionDraft29, + }, + } + + tlsCfg := l.options.TLSConfig + tlsCfg.NextProtos = []string{"http/3", "quic/v1"} + + ln, err := quic.Listen(conn, tlsCfg, config) + if err != nil { + return + } + + l.ln = ln + l.cqueue = make(chan net.Conn, l.md.backlog) + l.errChan = make(chan error, 1) + + go l.listenLoop() + + return +} + +func (l *icmpListener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.cqueue: + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *icmpListener) Close() error { + return l.ln.Close() +} + +func (l *icmpListener) Addr() net.Addr { + return l.ln.Addr() +} + +func (l *icmpListener) listenLoop() { + for { + ctx := context.Background() + session, err := l.ln.Accept(ctx) + if err != nil { + l.logger.Error("accept: ", err) + l.errChan <- err + close(l.errChan) + return + } + l.logger.Infof("new client session: %v", session.RemoteAddr()) + go l.mux(ctx, session) + } +} + +func (l *icmpListener) mux(ctx context.Context, session quic.Session) { + defer session.CloseWithError(0, "closed") + + for { + stream, err := session.AcceptStream(ctx) + if err != nil { + l.logger.Error("accept stream: ", err) + return + } + + conn := &quicConn{ + Stream: stream, + laddr: session.LocalAddr(), + raddr: session.RemoteAddr(), + } + select { + case l.cqueue <- conn: + case <-stream.Context().Done(): + stream.Close() + default: + stream.Close() + l.logger.Warnf("connection queue is full, client %s discarded", session.RemoteAddr()) + } + } +} diff --git a/listener/icmp/metadata.go b/listener/icmp/metadata.go new file mode 100644 index 0000000..c39ce53 --- /dev/null +++ b/listener/icmp/metadata.go @@ -0,0 +1,41 @@ +package quic + +import ( + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultBacklog = 128 +) + +type metadata struct { + keepAlive bool + handshakeTimeout time.Duration + maxIdleTimeout time.Duration + + cipherKey []byte + backlog int +} + +func (l *icmpListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + keepAlive = "keepAlive" + handshakeTimeout = "handshakeTimeout" + maxIdleTimeout = "maxIdleTimeout" + + backlog = "backlog" + ) + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + l.md.keepAlive = mdata.GetBool(md, keepAlive) + l.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + l.md.maxIdleTimeout = mdata.GetDuration(md, maxIdleTimeout) + + return +} diff --git a/listener/kcp/listener.go b/listener/kcp/listener.go new file mode 100644 index 0000000..d434f68 --- /dev/null +++ b/listener/kcp/listener.go @@ -0,0 +1,179 @@ +package kcp + +import ( + "net" + "time" + + "github.com/go-gost/gost/v3/pkg/common/admission" + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + kcp_util "github.com/go-gost/x/internal/util/kcp" + "github.com/xtaci/kcp-go/v5" + "github.com/xtaci/smux" + "github.com/xtaci/tcpraw" +) + +func init() { + registry.ListenerRegistry().Register("kcp", NewListener) +} + +type kcpListener struct { + conn net.PacketConn + ln *kcp.Listener + cqueue chan net.Conn + errChan chan error + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &kcpListener{ + logger: options.Logger, + options: options, + } +} + +func (l *kcpListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + config := l.md.config + config.Init() + + var conn net.PacketConn + if config.TCP { + conn, err = tcpraw.Listen("tcp", l.options.Addr) + } else { + var udpAddr *net.UDPAddr + udpAddr, err = net.ResolveUDPAddr("udp", l.options.Addr) + if err != nil { + return + } + conn, err = net.ListenUDP("udp", udpAddr) + } + if err != nil { + return + } + + conn = metrics.WrapUDPConn(l.options.Service, conn) + conn = admission.WrapUDPConn(l.options.Admission, conn) + + ln, err := kcp.ServeConn( + kcp_util.BlockCrypt(config.Key, config.Crypt, kcp_util.DefaultSalt), + config.DataShard, config.ParityShard, conn) + if err != nil { + return + } + + if config.DSCP > 0 { + if er := ln.SetDSCP(config.DSCP); er != nil { + l.logger.Warn(er) + } + } + if er := ln.SetReadBuffer(config.SockBuf); er != nil { + l.logger.Warn(er) + } + if er := ln.SetWriteBuffer(config.SockBuf); er != nil { + l.logger.Warn(er) + } + + l.ln = ln + l.conn = conn + l.cqueue = make(chan net.Conn, l.md.backlog) + l.errChan = make(chan error, 1) + + go l.listenLoop() + + return +} + +func (l *kcpListener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.cqueue: + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *kcpListener) Close() error { + l.conn.Close() + return l.ln.Close() +} + +func (l *kcpListener) Addr() net.Addr { + return l.ln.Addr() +} + +func (l *kcpListener) listenLoop() { + for { + conn, err := l.ln.AcceptKCP() + if err != nil { + l.logger.Error("accept:", err) + l.errChan <- err + close(l.errChan) + return + } + + conn.SetStreamMode(true) + conn.SetWriteDelay(false) + conn.SetNoDelay( + l.md.config.NoDelay, + l.md.config.Interval, + l.md.config.Resend, + l.md.config.NoCongestion, + ) + conn.SetMtu(l.md.config.MTU) + conn.SetWindowSize(l.md.config.SndWnd, l.md.config.RcvWnd) + conn.SetACKNoDelay(l.md.config.AckNodelay) + go l.mux(conn) + } +} + +func (l *kcpListener) mux(conn net.Conn) { + defer conn.Close() + + smuxConfig := smux.DefaultConfig() + smuxConfig.MaxReceiveBuffer = l.md.config.SockBuf + smuxConfig.KeepAliveInterval = time.Duration(l.md.config.KeepAlive) * time.Second + + if !l.md.config.NoComp { + conn = kcp_util.CompStreamConn(conn) + } + + mux, err := smux.Server(conn, smuxConfig) + if err != nil { + l.logger.Error(err) + return + } + defer mux.Close() + + for { + stream, err := mux.AcceptStream() + if err != nil { + l.logger.Error("accept stream: ", err) + return + } + + select { + case l.cqueue <- stream: + case <-stream.GetDieCh(): + stream.Close() + default: + stream.Close() + l.logger.Warnf("connection queue is full, client %s discarded", stream.RemoteAddr()) + } + } +} diff --git a/listener/kcp/metadata.go b/listener/kcp/metadata.go new file mode 100644 index 0000000..60b8bed --- /dev/null +++ b/listener/kcp/metadata.go @@ -0,0 +1,47 @@ +package kcp + +import ( + "encoding/json" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" + kcp_util "github.com/go-gost/x/internal/util/kcp" +) + +const ( + defaultBacklog = 128 +) + +type metadata struct { + config *kcp_util.Config + backlog int +} + +func (l *kcpListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + backlog = "backlog" + config = "config" + ) + + if m := mdata.GetStringMap(md, config); len(m) > 0 { + b, err := json.Marshal(m) + if err != nil { + return err + } + cfg := &kcp_util.Config{} + if err := json.Unmarshal(b, cfg); err != nil { + return err + } + l.md.config = cfg + } + + if l.md.config == nil { + l.md.config = kcp_util.DefaultConfig + } + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + return +} diff --git a/listener/mtls/listener.go b/listener/mtls/listener.go new file mode 100644 index 0000000..59a4dc8 --- /dev/null +++ b/listener/mtls/listener.go @@ -0,0 +1,129 @@ +package mtls + +import ( + "crypto/tls" + "net" + + "github.com/go-gost/gost/v3/pkg/common/admission" + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + "github.com/xtaci/smux" +) + +func init() { + registry.ListenerRegistry().Register("mtls", NewListener) +} + +type mtlsListener struct { + net.Listener + cqueue chan net.Conn + errChan chan error + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &mtlsListener{ + logger: options.Logger, + options: options, + } +} + +func (l *mtlsListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + ln, err := net.Listen("tcp", l.options.Addr) + if err != nil { + return + } + + ln = metrics.WrapListener(l.options.Service, ln) + ln = admission.WrapListener(l.options.Admission, ln) + l.Listener = tls.NewListener(ln, l.options.TLSConfig) + + l.cqueue = make(chan net.Conn, l.md.backlog) + l.errChan = make(chan error, 1) + + go l.listenLoop() + + return +} + +func (l *mtlsListener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.cqueue: + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *mtlsListener) listenLoop() { + for { + conn, err := l.Listener.Accept() + if err != nil { + l.errChan <- err + close(l.errChan) + return + } + go l.mux(conn) + } +} + +func (l *mtlsListener) mux(conn net.Conn) { + defer conn.Close() + + smuxConfig := smux.DefaultConfig() + smuxConfig.KeepAliveDisabled = l.md.muxKeepAliveDisabled + if l.md.muxKeepAliveInterval > 0 { + smuxConfig.KeepAliveInterval = l.md.muxKeepAliveInterval + } + if l.md.muxKeepAliveTimeout > 0 { + smuxConfig.KeepAliveTimeout = l.md.muxKeepAliveTimeout + } + if l.md.muxMaxFrameSize > 0 { + smuxConfig.MaxFrameSize = l.md.muxMaxFrameSize + } + if l.md.muxMaxReceiveBuffer > 0 { + smuxConfig.MaxReceiveBuffer = l.md.muxMaxReceiveBuffer + } + if l.md.muxMaxStreamBuffer > 0 { + smuxConfig.MaxStreamBuffer = l.md.muxMaxStreamBuffer + } + session, err := smux.Server(conn, smuxConfig) + if err != nil { + l.logger.Error(err) + return + } + defer session.Close() + + for { + stream, err := session.AcceptStream() + if err != nil { + l.logger.Error("accept stream: ", err) + return + } + + select { + case l.cqueue <- stream: + case <-stream.GetDieCh(): + stream.Close() + default: + stream.Close() + l.logger.Warnf("connection queue is full, client %s discarded", stream.RemoteAddr()) + } + } +} diff --git a/listener/mtls/metadata.go b/listener/mtls/metadata.go new file mode 100644 index 0000000..23b25c1 --- /dev/null +++ b/listener/mtls/metadata.go @@ -0,0 +1,49 @@ +package mtls + +import ( + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultBacklog = 128 +) + +type metadata struct { + muxKeepAliveDisabled bool + muxKeepAliveInterval time.Duration + muxKeepAliveTimeout time.Duration + muxMaxFrameSize int + muxMaxReceiveBuffer int + muxMaxStreamBuffer int + + backlog int +} + +func (l *mtlsListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + backlog = "backlog" + + muxKeepAliveDisabled = "muxKeepAliveDisabled" + muxKeepAliveInterval = "muxKeepAliveInterval" + muxKeepAliveTimeout = "muxKeepAliveTimeout" + muxMaxFrameSize = "muxMaxFrameSize" + muxMaxReceiveBuffer = "muxMaxReceiveBuffer" + muxMaxStreamBuffer = "muxMaxStreamBuffer" + ) + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + l.md.muxKeepAliveDisabled = mdata.GetBool(md, muxKeepAliveDisabled) + l.md.muxKeepAliveInterval = mdata.GetDuration(md, muxKeepAliveInterval) + l.md.muxKeepAliveTimeout = mdata.GetDuration(md, muxKeepAliveTimeout) + l.md.muxMaxFrameSize = mdata.GetInt(md, muxMaxFrameSize) + l.md.muxMaxReceiveBuffer = mdata.GetInt(md, muxMaxReceiveBuffer) + l.md.muxMaxStreamBuffer = mdata.GetInt(md, muxMaxStreamBuffer) + + return +} diff --git a/listener/mws/listener.go b/listener/mws/listener.go new file mode 100644 index 0000000..6f30d4a --- /dev/null +++ b/listener/mws/listener.go @@ -0,0 +1,194 @@ +package mws + +import ( + "crypto/tls" + "net" + "net/http" + "net/http/httputil" + + "github.com/go-gost/gost/v3/pkg/common/admission" + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + ws_util "github.com/go-gost/x/internal/util/ws" + "github.com/gorilla/websocket" + "github.com/xtaci/smux" +) + +func init() { + registry.ListenerRegistry().Register("mws", NewListener) + registry.ListenerRegistry().Register("mwss", NewTLSListener) +} + +type mwsListener struct { + addr net.Addr + upgrader *websocket.Upgrader + srv *http.Server + cqueue chan net.Conn + errChan chan error + tlsEnabled bool + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &mwsListener{ + logger: options.Logger, + options: options, + } +} + +func NewTLSListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &mwsListener{ + tlsEnabled: true, + logger: options.Logger, + options: options, + } +} + +func (l *mwsListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + l.upgrader = &websocket.Upgrader{ + HandshakeTimeout: l.md.handshakeTimeout, + ReadBufferSize: l.md.readBufferSize, + WriteBufferSize: l.md.writeBufferSize, + EnableCompression: l.md.enableCompression, + CheckOrigin: func(r *http.Request) bool { return true }, + } + + path := l.md.path + if path == "" { + path = defaultPath + } + mux := http.NewServeMux() + mux.Handle(path, http.HandlerFunc(l.upgrade)) + l.srv = &http.Server{ + Addr: l.options.Addr, + Handler: mux, + ReadHeaderTimeout: l.md.readHeaderTimeout, + } + + l.cqueue = make(chan net.Conn, l.md.backlog) + l.errChan = make(chan error, 1) + + ln, err := net.Listen("tcp", l.options.Addr) + if err != nil { + return + } + ln = metrics.WrapListener(l.options.Service, ln) + ln = admission.WrapListener(l.options.Admission, ln) + + if l.tlsEnabled { + ln = tls.NewListener(ln, l.options.TLSConfig) + } + + l.addr = ln.Addr() + + go func() { + err := l.srv.Serve(ln) + if err != nil { + l.errChan <- err + } + close(l.errChan) + }() + + return +} + +func (l *mwsListener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.cqueue: + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *mwsListener) Close() error { + return l.srv.Close() +} + +func (l *mwsListener) Addr() net.Addr { + return l.addr +} + +func (l *mwsListener) upgrade(w http.ResponseWriter, r *http.Request) { + if l.logger.IsLevelEnabled(logger.DebugLevel) { + log := l.logger.WithFields(map[string]any{ + "local": l.addr.String(), + "remote": r.RemoteAddr, + }) + dump, _ := httputil.DumpRequest(r, false) + log.Debug(string(dump)) + } + + conn, err := l.upgrader.Upgrade(w, r, l.md.header) + if err != nil { + l.logger.Error(err) + return + } + + l.mux(ws_util.Conn(conn)) +} + +func (l *mwsListener) mux(conn net.Conn) { + defer conn.Close() + + smuxConfig := smux.DefaultConfig() + smuxConfig.KeepAliveDisabled = l.md.muxKeepAliveDisabled + if l.md.muxKeepAliveInterval > 0 { + smuxConfig.KeepAliveInterval = l.md.muxKeepAliveInterval + } + if l.md.muxKeepAliveTimeout > 0 { + smuxConfig.KeepAliveTimeout = l.md.muxKeepAliveTimeout + } + if l.md.muxMaxFrameSize > 0 { + smuxConfig.MaxFrameSize = l.md.muxMaxFrameSize + } + if l.md.muxMaxReceiveBuffer > 0 { + smuxConfig.MaxReceiveBuffer = l.md.muxMaxReceiveBuffer + } + if l.md.muxMaxStreamBuffer > 0 { + smuxConfig.MaxStreamBuffer = l.md.muxMaxStreamBuffer + } + session, err := smux.Server(conn, smuxConfig) + if err != nil { + l.logger.Error(err) + return + } + defer session.Close() + + for { + stream, err := session.AcceptStream() + if err != nil { + l.logger.Error("accept stream: ", err) + return + } + + select { + case l.cqueue <- stream: + case <-stream.GetDieCh(): + stream.Close() + default: + stream.Close() + l.logger.Warnf("connection queue is full, client %s discarded", stream.RemoteAddr()) + } + } +} diff --git a/listener/mws/metadata.go b/listener/mws/metadata.go new file mode 100644 index 0000000..e88861b --- /dev/null +++ b/listener/mws/metadata.go @@ -0,0 +1,85 @@ +package mws + +import ( + "net/http" + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultPath = "/ws" + defaultBacklog = 128 +) + +type metadata struct { + path string + backlog int + header http.Header + + handshakeTimeout time.Duration + readHeaderTimeout time.Duration + readBufferSize int + writeBufferSize int + enableCompression bool + + muxKeepAliveDisabled bool + muxKeepAliveInterval time.Duration + muxKeepAliveTimeout time.Duration + muxMaxFrameSize int + muxMaxReceiveBuffer int + muxMaxStreamBuffer int +} + +func (l *mwsListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + path = "path" + backlog = "backlog" + header = "header" + + handshakeTimeout = "handshakeTimeout" + readHeaderTimeout = "readHeaderTimeout" + readBufferSize = "readBufferSize" + writeBufferSize = "writeBufferSize" + enableCompression = "enableCompression" + + muxKeepAliveDisabled = "muxKeepAliveDisabled" + muxKeepAliveInterval = "muxKeepAliveInterval" + muxKeepAliveTimeout = "muxKeepAliveTimeout" + muxMaxFrameSize = "muxMaxFrameSize" + muxMaxReceiveBuffer = "muxMaxReceiveBuffer" + muxMaxStreamBuffer = "muxMaxStreamBuffer" + ) + + l.md.path = mdata.GetString(md, path) + if l.md.path == "" { + l.md.path = defaultPath + } + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + l.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + l.md.readHeaderTimeout = mdata.GetDuration(md, readHeaderTimeout) + l.md.readBufferSize = mdata.GetInt(md, readBufferSize) + l.md.writeBufferSize = mdata.GetInt(md, writeBufferSize) + l.md.enableCompression = mdata.GetBool(md, enableCompression) + + l.md.muxKeepAliveDisabled = mdata.GetBool(md, muxKeepAliveDisabled) + l.md.muxKeepAliveInterval = mdata.GetDuration(md, muxKeepAliveInterval) + l.md.muxKeepAliveTimeout = mdata.GetDuration(md, muxKeepAliveTimeout) + l.md.muxMaxFrameSize = mdata.GetInt(md, muxMaxFrameSize) + l.md.muxMaxReceiveBuffer = mdata.GetInt(md, muxMaxReceiveBuffer) + l.md.muxMaxStreamBuffer = mdata.GetInt(md, muxMaxStreamBuffer) + + if mm := mdata.GetStringMapString(md, header); len(mm) > 0 { + hd := http.Header{} + for k, v := range mm { + hd.Add(k, v) + } + l.md.header = hd + } + return +} diff --git a/listener/obfs/http/conn.go b/listener/obfs/http/conn.go new file mode 100644 index 0000000..a5c632b --- /dev/null +++ b/listener/obfs/http/conn.go @@ -0,0 +1,145 @@ +package http + +import ( + "bufio" + "bytes" + "crypto/sha1" + "encoding/base64" + "errors" + "io" + "net" + "net/http" + "net/http/httputil" + "sync" + "time" + + "github.com/go-gost/gost/v3/pkg/logger" +) + +type obfsHTTPConn struct { + net.Conn + rbuf bytes.Buffer + wbuf bytes.Buffer + handshaked bool + handshakeMutex sync.Mutex + header http.Header + logger logger.Logger +} + +func (c *obfsHTTPConn) Handshake() (err error) { + c.handshakeMutex.Lock() + defer c.handshakeMutex.Unlock() + + if c.handshaked { + return nil + } + + if err = c.handshake(); err != nil { + return + } + + c.handshaked = true + return nil +} + +func (c *obfsHTTPConn) handshake() (err error) { + br := bufio.NewReader(c.Conn) + r, err := http.ReadRequest(br) + if err != nil { + return + } + + if c.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpRequest(r, false) + c.logger.Debug(string(dump)) + } + + if r.ContentLength > 0 { + _, err = io.Copy(&c.rbuf, r.Body) + } else { + var b []byte + b, err = br.Peek(br.Buffered()) + if len(b) > 0 { + _, err = c.rbuf.Write(b) + } + } + if err != nil { + c.logger.Error(err) + return + } + + resp := http.Response{ + StatusCode: http.StatusOK, + ProtoMajor: 1, + ProtoMinor: 1, + Header: c.header, + } + if resp.Header == nil { + resp.Header = http.Header{} + } + resp.Header.Set("Date", time.Now().Format(time.RFC1123)) + + if r.Method != http.MethodGet || r.Header.Get("Upgrade") != "websocket" { + resp.StatusCode = http.StatusBadRequest + + if c.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpResponse(&resp, false) + c.logger.Debug(string(dump)) + } + + resp.Write(c.Conn) + return errors.New("bad request") + } + + resp.StatusCode = http.StatusSwitchingProtocols + resp.Header.Set("Connection", "Upgrade") + resp.Header.Set("Upgrade", "websocket") + resp.Header.Set("Sec-WebSocket-Accept", c.computeAcceptKey(r.Header.Get("Sec-WebSocket-Key"))) + + if c.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpResponse(&resp, false) + c.logger.Debug(string(dump)) + } + + if c.rbuf.Len() > 0 { + // cache the response header if there are extra data in the request body. + resp.Write(&c.wbuf) + return + } + + err = resp.Write(c.Conn) + return +} + +func (c *obfsHTTPConn) Read(b []byte) (n int, err error) { + if err = c.Handshake(); err != nil { + return + } + + if c.rbuf.Len() > 0 { + return c.rbuf.Read(b) + } + return c.Conn.Read(b) +} + +func (c *obfsHTTPConn) Write(b []byte) (n int, err error) { + if err = c.Handshake(); err != nil { + return + } + if c.wbuf.Len() > 0 { + c.wbuf.Write(b) // append the data to the cached header + _, err = c.wbuf.WriteTo(c.Conn) + n = len(b) // exclude the header length + return + } + return c.Conn.Write(b) +} + +var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + +func (c *obfsHTTPConn) computeAcceptKey(challengeKey string) string { + h := sha1.New() + h.Write([]byte(challengeKey)) + h.Write(keyGUID) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} diff --git a/listener/obfs/http/listener.go b/listener/obfs/http/listener.go new file mode 100644 index 0000000..80bfeba --- /dev/null +++ b/listener/obfs/http/listener.go @@ -0,0 +1,63 @@ +package http + +import ( + "net" + + "github.com/go-gost/gost/v3/pkg/common/admission" + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" +) + +func init() { + registry.ListenerRegistry().Register("ohttp", NewListener) +} + +type obfsListener struct { + net.Listener + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &obfsListener{ + logger: options.Logger, + options: options, + } +} + +func (l *obfsListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + ln, err := net.Listen("tcp", l.options.Addr) + if err != nil { + return + } + ln = metrics.WrapListener(l.options.Service, ln) + ln = admission.WrapListener(l.options.Admission, ln) + + l.Listener = ln + return +} + +func (l *obfsListener) Accept() (net.Conn, error) { + c, err := l.Listener.Accept() + if err != nil { + return nil, err + } + + return &obfsHTTPConn{ + Conn: c, + header: l.md.header, + logger: l.logger, + }, nil +} diff --git a/listener/obfs/http/metadata.go b/listener/obfs/http/metadata.go new file mode 100644 index 0000000..61012a6 --- /dev/null +++ b/listener/obfs/http/metadata.go @@ -0,0 +1,26 @@ +package http + +import ( + "net/http" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { + header http.Header +} + +func (l *obfsListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + header = "header" + ) + + if mm := mdata.GetStringMapString(md, header); len(mm) > 0 { + hd := http.Header{} + for k, v := range mm { + hd.Add(k, v) + } + l.md.header = hd + } + return +} diff --git a/listener/obfs/tls/conn.go b/listener/obfs/tls/conn.go new file mode 100644 index 0000000..70786bc --- /dev/null +++ b/listener/obfs/tls/conn.go @@ -0,0 +1,161 @@ +package tls + +import ( + "bytes" + "crypto/rand" + "crypto/tls" + "net" + "sync" + "time" + + dissector "github.com/go-gost/tls-dissector" +) + +const ( + maxTLSDataLen = 16384 +) + +type obfsTLSConn struct { + net.Conn + rbuf bytes.Buffer + wbuf bytes.Buffer + handshaked bool + handshakeMutex sync.Mutex +} + +func (c *obfsTLSConn) Handshake() (err error) { + c.handshakeMutex.Lock() + defer c.handshakeMutex.Unlock() + + if c.handshaked { + return + } + + if err = c.handshake(); err != nil { + return + } + + c.handshaked = true + return nil +} + +func (c *obfsTLSConn) handshake() error { + record := &dissector.Record{} + if _, err := record.ReadFrom(c.Conn); err != nil { + // log.Log(err) + return err + } + if record.Type != dissector.Handshake { + return dissector.ErrBadType + } + + clientMsg := &dissector.ClientHelloMsg{} + if err := clientMsg.Decode(record.Opaque); err != nil { + // log.Log(err) + return err + } + + for _, ext := range clientMsg.Extensions { + if ext.Type() == dissector.ExtSessionTicket { + b, err := ext.Encode() + if err != nil { + // log.Log(err) + return err + } + c.rbuf.Write(b) + break + } + } + + serverMsg := &dissector.ServerHelloMsg{ + Version: tls.VersionTLS12, + SessionID: clientMsg.SessionID, + CipherSuite: 0xcca8, + CompressionMethod: 0x00, + Extensions: []dissector.Extension{ + &dissector.RenegotiationInfoExtension{}, + &dissector.ExtendedMasterSecretExtension{}, + &dissector.ECPointFormatsExtension{ + Formats: []uint8{0x00}, + }, + }, + } + + serverMsg.Random.Time = uint32(time.Now().Unix()) + rand.Read(serverMsg.Random.Opaque[:]) + b, err := serverMsg.Encode() + if err != nil { + return err + } + + record = &dissector.Record{ + Type: dissector.Handshake, + Version: tls.VersionTLS10, + Opaque: b, + } + + if _, err := record.WriteTo(&c.wbuf); err != nil { + return err + } + + record = &dissector.Record{ + Type: dissector.ChangeCipherSpec, + Version: tls.VersionTLS12, + Opaque: []byte{0x01}, + } + if _, err := record.WriteTo(&c.wbuf); err != nil { + return err + } + return nil +} + +func (c *obfsTLSConn) Read(b []byte) (n int, err error) { + if err = c.Handshake(); err != nil { + return + } + + if c.rbuf.Len() > 0 { + return c.rbuf.Read(b) + } + record := &dissector.Record{} + if _, err = record.ReadFrom(c.Conn); err != nil { + return + } + n = copy(b, record.Opaque) + _, err = c.rbuf.Write(record.Opaque[n:]) + return +} + +func (c *obfsTLSConn) Write(b []byte) (n int, err error) { + if err = c.Handshake(); err != nil { + return + } + n = len(b) + + for len(b) > 0 { + data := b + if len(b) > maxTLSDataLen { + data = b[:maxTLSDataLen] + b = b[maxTLSDataLen:] + } else { + b = b[:0] + } + record := &dissector.Record{ + Type: dissector.AppData, + Version: tls.VersionTLS12, + Opaque: data, + } + + if c.wbuf.Len() > 0 { + record.Type = dissector.Handshake + record.WriteTo(&c.wbuf) + _, err = c.wbuf.WriteTo(c.Conn) + return + } + + if _, err = record.WriteTo(c.Conn); err != nil { + return + } + } + return +} diff --git a/listener/obfs/tls/listener.go b/listener/obfs/tls/listener.go new file mode 100644 index 0000000..e9a449d --- /dev/null +++ b/listener/obfs/tls/listener.go @@ -0,0 +1,61 @@ +package tls + +import ( + "net" + + "github.com/go-gost/gost/v3/pkg/common/admission" + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" +) + +func init() { + registry.ListenerRegistry().Register("otls", NewListener) +} + +type obfsListener struct { + net.Listener + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &obfsListener{ + logger: options.Logger, + options: options, + } +} + +func (l *obfsListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + ln, err := net.Listen("tcp", l.options.Addr) + if err != nil { + return + } + ln = metrics.WrapListener(l.options.Service, ln) + ln = admission.WrapListener(l.options.Admission, ln) + + l.Listener = ln + return +} + +func (l *obfsListener) Accept() (net.Conn, error) { + c, err := l.Listener.Accept() + if err != nil { + return nil, err + } + + return &obfsTLSConn{ + Conn: c, + }, nil +} diff --git a/listener/obfs/tls/metadata.go b/listener/obfs/tls/metadata.go new file mode 100644 index 0000000..b4ee45b --- /dev/null +++ b/listener/obfs/tls/metadata.go @@ -0,0 +1,12 @@ +package tls + +import ( + md "github.com/go-gost/gost/v3/pkg/metadata" +) + +type metadata struct { +} + +func (l *obfsListener) parseMetadata(md md.Metadata) (err error) { + return +} diff --git a/listener/pht/listener.go b/listener/pht/listener.go new file mode 100644 index 0000000..2e5582c --- /dev/null +++ b/listener/pht/listener.go @@ -0,0 +1,96 @@ +// plain http tunnel + +package pht + +import ( + "net" + + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + pht_util "github.com/go-gost/x/internal/util/pht" +) + +func init() { + registry.ListenerRegistry().Register("pht", NewListener) + registry.ListenerRegistry().Register("phts", NewTLSListener) +} + +type phtListener struct { + addr net.Addr + tlsEnabled bool + server *pht_util.Server + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &phtListener{ + logger: options.Logger, + options: options, + } +} + +func NewTLSListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &phtListener{ + tlsEnabled: true, + logger: options.Logger, + options: options, + } +} + +func (l *phtListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + l.addr, err = net.ResolveTCPAddr("tcp", l.options.Addr) + if err != nil { + return + } + + l.server = pht_util.NewServer( + l.options.Addr, + pht_util.TLSConfigServerOption(l.options.TLSConfig), + pht_util.EnableTLSServerOption(l.tlsEnabled), + pht_util.BacklogServerOption(l.md.backlog), + pht_util.PathServerOption(l.md.authorizePath, l.md.pushPath, l.md.pullPath), + pht_util.LoggerServerOption(l.options.Logger), + ) + + go func() { + if err := l.server.ListenAndServe(); err != nil { + l.logger.Error(err) + } + }() + + return +} + +func (l *phtListener) Accept() (conn net.Conn, err error) { + conn, err = l.server.Accept() + if err != nil { + return + } + conn = metrics.WrapConn(l.options.Service, conn) + return +} + +func (l *phtListener) Addr() net.Addr { + return l.addr +} + +func (l *phtListener) Close() (err error) { + return l.server.Close() +} diff --git a/listener/pht/metadata.go b/listener/pht/metadata.go new file mode 100644 index 0000000..301529b --- /dev/null +++ b/listener/pht/metadata.go @@ -0,0 +1,51 @@ +package pht + +import ( + "strings" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultAuthorizePath = "/authorize" + defaultPushPath = "/push" + defaultPullPath = "/pull" + defaultBacklog = 128 +) + +type metadata struct { + authorizePath string + pushPath string + pullPath string + backlog int +} + +func (l *phtListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + authorizePath = "authorizePath" + pushPath = "pushPath" + pullPath = "pullPath" + + backlog = "backlog" + ) + + l.md.authorizePath = mdata.GetString(md, authorizePath) + if !strings.HasPrefix(l.md.authorizePath, "/") { + l.md.authorizePath = defaultAuthorizePath + } + l.md.pushPath = mdata.GetString(md, pushPath) + if !strings.HasPrefix(l.md.pushPath, "/") { + l.md.pushPath = defaultPushPath + } + l.md.pullPath = mdata.GetString(md, pullPath) + if !strings.HasPrefix(l.md.pullPath, "/") { + l.md.pullPath = defaultPullPath + } + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + return +} diff --git a/listener/quic/conn.go b/listener/quic/conn.go new file mode 100644 index 0000000..ee1a26c --- /dev/null +++ b/listener/quic/conn.go @@ -0,0 +1,21 @@ +package quic + +import ( + "net" + + "github.com/lucas-clemente/quic-go" +) + +type quicConn struct { + quic.Stream + laddr net.Addr + raddr net.Addr +} + +func (c *quicConn) LocalAddr() net.Addr { + return c.laddr +} + +func (c *quicConn) RemoteAddr() net.Addr { + return c.raddr +} diff --git a/listener/quic/listener.go b/listener/quic/listener.go new file mode 100644 index 0000000..0262166 --- /dev/null +++ b/listener/quic/listener.go @@ -0,0 +1,151 @@ +package quic + +import ( + "context" + "net" + + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + quic_util "github.com/go-gost/x/internal/util/quic" + "github.com/lucas-clemente/quic-go" +) + +func init() { + registry.ListenerRegistry().Register("quic", NewListener) +} + +type quicListener struct { + ln quic.Listener + cqueue chan net.Conn + errChan chan error + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &quicListener{ + logger: options.Logger, + options: options, + } +} + +func (l *quicListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + addr := l.options.Addr + if _, _, err := net.SplitHostPort(addr); err != nil { + addr = net.JoinHostPort(addr, "0") + } + + var laddr *net.UDPAddr + laddr, err = net.ResolveUDPAddr("udp", addr) + if err != nil { + return + } + + var conn net.PacketConn + conn, err = net.ListenUDP("udp", laddr) + if err != nil { + return + } + if l.md.cipherKey != nil { + conn = quic_util.CipherPacketConn(conn, l.md.cipherKey) + } + + config := &quic.Config{ + KeepAlive: l.md.keepAlive, + HandshakeIdleTimeout: l.md.handshakeTimeout, + MaxIdleTimeout: l.md.maxIdleTimeout, + Versions: []quic.VersionNumber{ + quic.Version1, + quic.VersionDraft29, + }, + } + + tlsCfg := l.options.TLSConfig + tlsCfg.NextProtos = []string{"http/3", "quic/v1"} + + ln, err := quic.Listen(conn, tlsCfg, config) + if err != nil { + return + } + + l.ln = ln + l.cqueue = make(chan net.Conn, l.md.backlog) + l.errChan = make(chan error, 1) + + go l.listenLoop() + + return +} + +func (l *quicListener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.cqueue: + conn = metrics.WrapConn(l.options.Service, conn) + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *quicListener) Close() error { + return l.ln.Close() +} + +func (l *quicListener) Addr() net.Addr { + return l.ln.Addr() +} + +func (l *quicListener) listenLoop() { + for { + ctx := context.Background() + session, err := l.ln.Accept(ctx) + if err != nil { + l.logger.Error("accept:", err) + l.errChan <- err + close(l.errChan) + return + } + go l.mux(ctx, session) + } +} + +func (l *quicListener) mux(ctx context.Context, session quic.Session) { + defer session.CloseWithError(0, "closed") + + for { + stream, err := session.AcceptStream(ctx) + if err != nil { + l.logger.Error("accept stream:", err) + return + } + + conn := &quicConn{ + Stream: stream, + laddr: session.LocalAddr(), + raddr: session.RemoteAddr(), + } + select { + case l.cqueue <- conn: + case <-stream.Context().Done(): + stream.Close() + default: + stream.Close() + l.logger.Warnf("connection queue is full, client %s discarded", session.RemoteAddr()) + } + } +} diff --git a/listener/quic/metadata.go b/listener/quic/metadata.go new file mode 100644 index 0000000..fc88cd7 --- /dev/null +++ b/listener/quic/metadata.go @@ -0,0 +1,46 @@ +package quic + +import ( + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultBacklog = 128 +) + +type metadata struct { + keepAlive bool + handshakeTimeout time.Duration + maxIdleTimeout time.Duration + + cipherKey []byte + backlog int +} + +func (l *quicListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + keepAlive = "keepAlive" + handshakeTimeout = "handshakeTimeout" + maxIdleTimeout = "maxIdleTimeout" + + backlog = "backlog" + cipherKey = "cipherKey" + ) + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + if key := mdata.GetString(md, cipherKey); key != "" { + l.md.cipherKey = []byte(key) + } + + l.md.keepAlive = mdata.GetBool(md, keepAlive) + l.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + l.md.maxIdleTimeout = mdata.GetDuration(md, maxIdleTimeout) + + return +} diff --git a/listener/redirect/udp/conn.go b/listener/redirect/udp/conn.go new file mode 100644 index 0000000..2780d3c --- /dev/null +++ b/listener/redirect/udp/conn.go @@ -0,0 +1,42 @@ +package udp + +import ( + "net" + "sync" + "time" + + "github.com/go-gost/gost/v3/pkg/common/bufpool" +) + +type redirConn struct { + net.Conn + buf []byte + ttl time.Duration + once sync.Once +} + +func (c *redirConn) Read(b []byte) (n int, err error) { + if c.ttl > 0 { + c.SetReadDeadline(time.Now().Add(c.ttl)) + defer c.SetReadDeadline(time.Time{}) + } + + c.once.Do(func() { + n = copy(b, c.buf) + bufpool.Put(&c.buf) + c.buf = nil + }) + + if n == 0 { + n, err = c.Conn.Read(b) + } + return +} + +func (c *redirConn) Write(b []byte) (n int, err error) { + if c.ttl > 0 { + c.SetWriteDeadline(time.Now().Add(c.ttl)) + defer c.SetWriteDeadline(time.Time{}) + } + return c.Conn.Write(b) +} diff --git a/listener/redirect/udp/listener.go b/listener/redirect/udp/listener.go new file mode 100644 index 0000000..613dc89 --- /dev/null +++ b/listener/redirect/udp/listener.go @@ -0,0 +1,68 @@ +package udp + +import ( + "net" + + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" +) + +func init() { + registry.ListenerRegistry().Register("redu", NewListener) +} + +type redirectListener struct { + ln *net.UDPConn + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &redirectListener{ + logger: options.Logger, + options: options, + } +} + +func (l *redirectListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + laddr, err := net.ResolveUDPAddr("udp", l.options.Addr) + if err != nil { + return + } + + ln, err := l.listenUDP(laddr) + if err != nil { + return + } + + l.ln = ln + return +} + +func (l *redirectListener) Accept() (conn net.Conn, err error) { + conn, err = l.accept() + if err != nil { + return + } + // conn = metrics.WrapConn(l.options.Service, conn) + return +} + +func (l *redirectListener) Addr() net.Addr { + return l.ln.LocalAddr() +} + +func (l *redirectListener) Close() error { + return l.ln.Close() +} diff --git a/listener/redirect/udp/listener_linux.go b/listener/redirect/udp/listener_linux.go new file mode 100644 index 0000000..042e003 --- /dev/null +++ b/listener/redirect/udp/listener_linux.go @@ -0,0 +1,37 @@ +package udp + +import ( + "net" + + "github.com/LiamHaworth/go-tproxy" + "github.com/go-gost/gost/v3/pkg/common/bufpool" +) + +func (l *redirectListener) listenUDP(addr *net.UDPAddr) (*net.UDPConn, error) { + return tproxy.ListenUDP("udp", addr) +} + +func (l *redirectListener) accept() (conn net.Conn, err error) { + b := bufpool.Get(l.md.readBufferSize) + + n, raddr, dstAddr, err := tproxy.ReadFromUDP(l.ln, *b) + if err != nil { + l.logger.Error(err) + return + } + + l.logger.Infof("%s >> %s", raddr.String(), dstAddr.String()) + + c, err := tproxy.DialUDP("udp", dstAddr, raddr) + if err != nil { + l.logger.Error(err) + return + } + + conn = &redirConn{ + Conn: c, + buf: (*b)[:n], + ttl: l.md.ttl, + } + return +} diff --git a/listener/redirect/udp/listener_other.go b/listener/redirect/udp/listener_other.go new file mode 100644 index 0000000..82a93ff --- /dev/null +++ b/listener/redirect/udp/listener_other.go @@ -0,0 +1,16 @@ +//go:build !linux + +package udp + +import ( + "errors" + "net" +) + +func (l *redirectListener) listenUDP(addr *net.UDPAddr) (*net.UDPConn, error) { + return nil, errors.New("UDP redirect is not available on non-linux platform") +} + +func (l *redirectListener) accept() (conn net.Conn, err error) { + return nil, errors.New("UDP redirect is not available on non-linux platform") +} diff --git a/listener/redirect/udp/metadata.go b/listener/redirect/udp/metadata.go new file mode 100644 index 0000000..a30fe0a --- /dev/null +++ b/listener/redirect/udp/metadata.go @@ -0,0 +1,36 @@ +package udp + +import ( + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultTTL = 60 * time.Second + defaultReadBufferSize = 1024 +) + +type metadata struct { + ttl time.Duration + readBufferSize int +} + +func (l *redirectListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + ttl = "ttl" + readBufferSize = "readBufferSize" + ) + + l.md.ttl = mdata.GetDuration(md, ttl) + if l.md.ttl <= 0 { + l.md.ttl = defaultTTL + } + + l.md.readBufferSize = mdata.GetInt(md, readBufferSize) + if l.md.readBufferSize <= 0 { + l.md.readBufferSize = defaultReadBufferSize + } + + return +} diff --git a/listener/ssh/listener.go b/listener/ssh/listener.go new file mode 100644 index 0000000..c28f54f --- /dev/null +++ b/listener/ssh/listener.go @@ -0,0 +1,148 @@ +package ssh + +import ( + "fmt" + "net" + "time" + + "github.com/go-gost/gost/v3/pkg/common/admission" + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + ssh_util "github.com/go-gost/x/internal/util/ssh" + "golang.org/x/crypto/ssh" +) + +func init() { + registry.ListenerRegistry().Register("ssh", NewListener) +} + +type sshListener struct { + net.Listener + config *ssh.ServerConfig + cqueue chan net.Conn + errChan chan error + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &sshListener{ + logger: options.Logger, + options: options, + } +} + +func (l *sshListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + ln, err := net.Listen("tcp", l.options.Addr) + if err != nil { + return err + } + + ln = metrics.WrapListener(l.options.Service, ln) + ln = admission.WrapListener(l.options.Admission, ln) + l.Listener = ln + + config := &ssh.ServerConfig{ + PasswordCallback: ssh_util.PasswordCallback(l.options.Auther), + PublicKeyCallback: ssh_util.PublicKeyCallback(l.md.authorizedKeys), + } + config.AddHostKey(l.md.signer) + if l.options.Auther == nil && len(l.md.authorizedKeys) == 0 { + config.NoClientAuth = true + } + + l.config = config + l.cqueue = make(chan net.Conn, l.md.backlog) + l.errChan = make(chan error, 1) + + go l.listenLoop() + + return +} + +func (l *sshListener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.cqueue: + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *sshListener) listenLoop() { + for { + conn, err := l.Listener.Accept() + if err != nil { + l.logger.Error("accept:", err) + l.errChan <- err + close(l.errChan) + return + } + go l.serveConn(conn) + } +} + +func (l *sshListener) serveConn(conn net.Conn) { + start := time.Now() + l.logger.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) + defer func() { + l.logger.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr()) + }() + + sc, chans, reqs, err := ssh.NewServerConn(conn, l.config) + if err != nil { + l.logger.Error(err) + conn.Close() + return + } + defer sc.Close() + + go ssh.DiscardRequests(reqs) + go func() { + for newChannel := range chans { + // Check the type of channel + t := newChannel.ChannelType() + switch t { + case ssh_util.GostSSHTunnelRequest: + channel, requests, err := newChannel.Accept() + if err != nil { + l.logger.Warnf("could not accept channel: %s", err.Error()) + continue + } + + go ssh.DiscardRequests(requests) + cc := ssh_util.NewConn(conn, channel) + select { + case l.cqueue <- cc: + default: + l.logger.Warnf("connection queue is full, client %s discarded", conn.RemoteAddr()) + newChannel.Reject(ssh.ResourceShortage, "connection queue is full") + cc.Close() + } + + default: + l.logger.Warnf("unsupported channel type: %s", t) + newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unsupported channel type: %s", t)) + } + } + }() + + sc.Wait() +} diff --git a/listener/ssh/metadata.go b/listener/ssh/metadata.go new file mode 100644 index 0000000..6921e4e --- /dev/null +++ b/listener/ssh/metadata.go @@ -0,0 +1,68 @@ +package ssh + +import ( + "io/ioutil" + + tls_util "github.com/go-gost/gost/v3/pkg/common/util/tls" + mdata "github.com/go-gost/gost/v3/pkg/metadata" + ssh_util "github.com/go-gost/x/internal/util/ssh" + "golang.org/x/crypto/ssh" +) + +const ( + defaultBacklog = 128 +) + +type metadata struct { + signer ssh.Signer + authorizedKeys map[string]bool + backlog int +} + +func (l *sshListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + authorizedKeys = "authorizedKeys" + privateKeyFile = "privateKeyFile" + passphrase = "passphrase" + backlog = "backlog" + ) + + if key := mdata.GetString(md, privateKeyFile); key != "" { + data, err := ioutil.ReadFile(key) + if err != nil { + return err + } + + pp := mdata.GetString(md, passphrase) + if pp == "" { + l.md.signer, err = ssh.ParsePrivateKey(data) + } else { + l.md.signer, err = ssh.ParsePrivateKeyWithPassphrase(data, []byte(pp)) + } + if err != nil { + return err + } + } + if l.md.signer == nil { + signer, err := ssh.NewSignerFromKey(tls_util.DefaultConfig.Clone().Certificates[0].PrivateKey) + if err != nil { + return err + } + l.md.signer = signer + } + + if name := mdata.GetString(md, authorizedKeys); name != "" { + m, err := ssh_util.ParseAuthorizedKeysFile(name) + if err != nil { + return err + } + l.md.authorizedKeys = m + } + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + return +} diff --git a/listener/sshd/listener.go b/listener/sshd/listener.go new file mode 100644 index 0000000..72dae64 --- /dev/null +++ b/listener/sshd/listener.go @@ -0,0 +1,199 @@ +package ssh + +import ( + "context" + "fmt" + "net" + "strconv" + "time" + + "github.com/go-gost/gost/v3/pkg/common/admission" + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + ssh_util "github.com/go-gost/x/internal/util/ssh" + sshd_util "github.com/go-gost/x/internal/util/sshd" + "golang.org/x/crypto/ssh" +) + +// Applicable SSH Request types for Port Forwarding - RFC 4254 7.X +const ( + DirectForwardRequest = "direct-tcpip" // RFC 4254 7.2 + RemoteForwardRequest = "tcpip-forward" // RFC 4254 7.1 +) + +func init() { + registry.ListenerRegistry().Register("sshd", NewListener) +} + +type sshdListener struct { + net.Listener + config *ssh.ServerConfig + cqueue chan net.Conn + errChan chan error + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &sshdListener{ + logger: options.Logger, + options: options, + } +} + +func (l *sshdListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + ln, err := net.Listen("tcp", l.options.Addr) + if err != nil { + return err + } + + ln = metrics.WrapListener(l.options.Service, ln) + ln = admission.WrapListener(l.options.Admission, ln) + l.Listener = ln + + config := &ssh.ServerConfig{ + PasswordCallback: ssh_util.PasswordCallback(l.options.Auther), + PublicKeyCallback: ssh_util.PublicKeyCallback(l.md.authorizedKeys), + } + config.AddHostKey(l.md.signer) + if l.options.Auther == nil && len(l.md.authorizedKeys) == 0 { + config.NoClientAuth = true + } + + l.config = config + l.cqueue = make(chan net.Conn, l.md.backlog) + l.errChan = make(chan error, 1) + + go l.listenLoop() + + return +} + +func (l *sshdListener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.cqueue: + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *sshdListener) listenLoop() { + for { + conn, err := l.Listener.Accept() + if err != nil { + l.logger.Error("accept:", err) + l.errChan <- err + close(l.errChan) + return + } + go l.serveConn(conn) + } +} + +func (l *sshdListener) serveConn(conn net.Conn) { + start := time.Now() + l.logger.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) + defer func() { + l.logger.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr()) + }() + + sc, chans, reqs, err := ssh.NewServerConn(conn, l.config) + if err != nil { + l.logger.Error(err) + conn.Close() + return + } + defer sc.Close() + + go func() { + for newChannel := range chans { + // Check the type of channel + t := newChannel.ChannelType() + switch t { + case DirectForwardRequest: + channel, requests, err := newChannel.Accept() + if err != nil { + l.logger.Warnf("could not accept channel: %s", err.Error()) + continue + } + p := directForward{} + ssh.Unmarshal(newChannel.ExtraData(), &p) + + l.logger.Debug(p.String()) + + if p.Host1 == "" { + p.Host1 = "" + } + + go ssh.DiscardRequests(requests) + cc := sshd_util.NewDirectForwardConn(sc, channel, net.JoinHostPort(p.Host1, strconv.Itoa(int(p.Port1)))) + + select { + case l.cqueue <- cc: + default: + l.logger.Warnf("connection queue is full, client %s discarded", conn.RemoteAddr()) + newChannel.Reject(ssh.ResourceShortage, "connection queue is full") + cc.Close() + } + + default: + l.logger.Warnf("unsupported channel type: %s", t) + newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unsupported channel type: %s", t)) + } + } + }() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + for req := range reqs { + switch req.Type { + case RemoteForwardRequest: + cc := sshd_util.NewRemoteForwardConn(ctx, sc, req) + + select { + case l.cqueue <- cc: + default: + l.logger.Warnf("connection queue is full, client %s discarded", conn.RemoteAddr()) + req.Reply(false, []byte("connection queue is full")) + cc.Close() + } + default: + l.logger.Warnf("unsupported request type: %s, want reply: %v", req.Type, req.WantReply) + req.Reply(false, nil) + } + } + }() + sc.Wait() +} + +// directForward is structure for RFC 4254 7.2 - can be used for "forwarded-tcpip" and "direct-tcpip" +type directForward struct { + Host1 string + Port1 uint32 + Host2 string + Port2 uint32 +} + +func (p directForward) String() string { + return fmt.Sprintf("%s:%d -> %s:%d", p.Host2, p.Port2, p.Host1, p.Port1) +} diff --git a/listener/sshd/metadata.go b/listener/sshd/metadata.go new file mode 100644 index 0000000..5c11e10 --- /dev/null +++ b/listener/sshd/metadata.go @@ -0,0 +1,68 @@ +package ssh + +import ( + "io/ioutil" + + tls_util "github.com/go-gost/gost/v3/pkg/common/util/tls" + mdata "github.com/go-gost/gost/v3/pkg/metadata" + ssh_util "github.com/go-gost/x/internal/util/ssh" + "golang.org/x/crypto/ssh" +) + +const ( + defaultBacklog = 128 +) + +type metadata struct { + signer ssh.Signer + authorizedKeys map[string]bool + backlog int +} + +func (l *sshdListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + authorizedKeys = "authorizedKeys" + privateKeyFile = "privateKeyFile" + passphrase = "passphrase" + backlog = "backlog" + ) + + if key := mdata.GetString(md, privateKeyFile); key != "" { + data, err := ioutil.ReadFile(key) + if err != nil { + return err + } + + pp := mdata.GetString(md, passphrase) + if pp == "" { + l.md.signer, err = ssh.ParsePrivateKey(data) + } else { + l.md.signer, err = ssh.ParsePrivateKeyWithPassphrase(data, []byte(pp)) + } + if err != nil { + return err + } + } + if l.md.signer == nil { + signer, err := ssh.NewSignerFromKey(tls_util.DefaultConfig.Clone().Certificates[0].PrivateKey) + if err != nil { + return err + } + l.md.signer = signer + } + + if name := mdata.GetString(md, authorizedKeys); name != "" { + m, err := ssh_util.ParseAuthorizedKeysFile(name) + if err != nil { + return err + } + l.md.authorizedKeys = m + } + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + return +} diff --git a/listener/tap/listener.go b/listener/tap/listener.go new file mode 100644 index 0000000..ae04bbc --- /dev/null +++ b/listener/tap/listener.go @@ -0,0 +1,96 @@ +package tap + +import ( + "net" + + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + tap_util "github.com/go-gost/x/internal/util/tap" +) + +func init() { + registry.ListenerRegistry().Register("tap", NewListener) +} + +type tapListener struct { + saddr string + addr net.Addr + cqueue chan net.Conn + closed chan struct{} + logger logger.Logger + md metadata +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := &listener.Options{} + for _, opt := range opts { + opt(options) + } + return &tapListener{ + saddr: options.Addr, + logger: options.Logger, + } +} + +func (l *tapListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + l.addr, err = net.ResolveUDPAddr("udp", l.saddr) + if err != nil { + return + } + + ifce, ip, err := l.createTap() + if err != nil { + if ifce != nil { + ifce.Close() + } + return + } + + itf, err := net.InterfaceByName(ifce.Name()) + if err != nil { + return + } + + addrs, _ := itf.Addrs() + l.logger.Infof("name: %s, mac: %s, mtu: %d, addrs: %s", + itf.Name, itf.HardwareAddr, itf.MTU, addrs) + + l.cqueue = make(chan net.Conn, 1) + l.closed = make(chan struct{}) + + conn := tap_util.NewConn(l.md.config, ifce, l.addr, &net.IPAddr{IP: ip}) + + l.cqueue <- conn + + return +} + +func (l *tapListener) Accept() (net.Conn, error) { + select { + case conn := <-l.cqueue: + return conn, nil + case <-l.closed: + } + + return nil, listener.ErrClosed +} + +func (l *tapListener) Addr() net.Addr { + return l.addr +} + +func (l *tapListener) Close() error { + select { + case <-l.closed: + return net.ErrClosed + default: + close(l.closed) + } + return nil +} diff --git a/listener/tap/metadata.go b/listener/tap/metadata.go new file mode 100644 index 0000000..08d5381 --- /dev/null +++ b/listener/tap/metadata.go @@ -0,0 +1,44 @@ +package tap + +import ( + mdata "github.com/go-gost/gost/v3/pkg/metadata" + tap_util "github.com/go-gost/x/internal/util/tap" +) + +const ( + DefaultMTU = 1350 +) + +type metadata struct { + config *tap_util.Config +} + +func (l *tapListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + name = "name" + netKey = "net" + mtu = "mtu" + routes = "routes" + gateway = "gw" + ) + + config := &tap_util.Config{ + Name: mdata.GetString(md, name), + Net: mdata.GetString(md, netKey), + MTU: mdata.GetInt(md, mtu), + Gateway: mdata.GetString(md, gateway), + } + if config.MTU <= 0 { + config.MTU = DefaultMTU + } + + for _, s := range mdata.GetStrings(md, routes) { + if s != "" { + config.Routes = append(config.Routes, s) + } + } + + l.md.config = config + + return +} diff --git a/listener/tap/tap_darwin.go b/listener/tap/tap_darwin.go new file mode 100644 index 0000000..69cfe1a --- /dev/null +++ b/listener/tap/tap_darwin.go @@ -0,0 +1,13 @@ +package tap + +import ( + "errors" + "net" + + "github.com/songgao/water" +) + +func (l *tapListener) createTap() (ifce *water.Interface, ip net.IP, err error) { + err = errors.New("tap is not supported on darwin") + return +} diff --git a/listener/tap/tap_linux.go b/listener/tap/tap_linux.go new file mode 100644 index 0000000..dac7f10 --- /dev/null +++ b/listener/tap/tap_linux.go @@ -0,0 +1,69 @@ +package tap + +import ( + "net" + + "github.com/docker/libcontainer/netlink" + "github.com/milosgajdos/tenus" + "github.com/songgao/water" +) + +func (l *tapListener) createTap() (ifce *water.Interface, ip net.IP, err error) { + var ipNet *net.IPNet + if l.md.config.Net != "" { + ip, ipNet, err = net.ParseCIDR(l.md.config.Net) + if err != nil { + return + } + } + + ifce, err = water.New(water.Config{ + DeviceType: water.TAP, + PlatformSpecificParams: water.PlatformSpecificParams{ + Name: l.md.config.Name, + }, + }) + if err != nil { + return + } + + link, err := tenus.NewLinkFrom(ifce.Name()) + if err != nil { + return + } + + l.logger.Debugf("ip link set dev %s mtu %d", ifce.Name(), l.md.config.MTU) + + if err = link.SetLinkMTU(l.md.config.MTU); err != nil { + return + } + + if l.md.config.Net != "" { + l.logger.Debugf("ip address add %s dev %s", l.md.config.Net, ifce.Name()) + + if err = link.SetLinkIp(ip, ipNet); err != nil { + return + } + } + + l.logger.Debugf("ip link set dev %s up", ifce.Name()) + if err = link.SetLinkUp(); err != nil { + return + } + + if err = l.addRoutes(ifce.Name(), l.md.config.Gateway, l.md.config.Routes...); err != nil { + return + } + + return +} + +func (l *tapListener) addRoutes(ifName string, gw string, routes ...string) error { + for _, route := range routes { + l.logger.Debugf("ip route add %s via %s dev %s", route, gw, ifName) + if err := netlink.AddRoute(route, "", gw, ifName); err != nil { + return err + } + } + return nil +} diff --git a/listener/tap/tap_unix.go b/listener/tap/tap_unix.go new file mode 100644 index 0000000..cebd27d --- /dev/null +++ b/listener/tap/tap_unix.go @@ -0,0 +1,61 @@ +//go:build !linux && !windows && !darwin + +package tap + +import ( + "fmt" + "net" + "os/exec" + "strings" + + "github.com/songgao/water" +) + +func (l *tapListener) createTap() (ifce *water.Interface, ip net.IP, err error) { + ip, _, _ = net.ParseCIDR(l.md.config.Net) + + ifce, err = water.New(water.Config{ + DeviceType: water.TAP, + }) + if err != nil { + return + } + + var cmd string + if l.md.config.Net != "" { + cmd = fmt.Sprintf("ifconfig %s inet %s mtu %d up", ifce.Name(), l.md.config.Net, l.md.config.MTU) + } else { + cmd = fmt.Sprintf("ifconfig %s mtu %d up", ifce.Name(), l.md.config.MTU) + } + l.logger.Debug(cmd) + + args := strings.Split(cmd, " ") + if er := exec.Command(args[0], args[1:]...).Run(); er != nil { + err = fmt.Errorf("%s: %v", cmd, er) + return + } + + if err = l.addRoutes(ifce.Name(), l.md.config.Gateway, l.md.config.Routes...); err != nil { + return + } + + return +} + +func (l *tapListener) addRoutes(ifName string, gw string, routes ...string) error { + for _, route := range routes { + if route == "" { + continue + } + cmd := fmt.Sprintf("route add -net %s dev %s", route, ifName) + if gw != "" { + cmd += " gw " + gw + } + l.logger.Debug(cmd) + args := strings.Split(cmd, " ") + if er := exec.Command(args[0], args[1:]...).Run(); er != nil { + return fmt.Errorf("%s: %v", cmd, er) + } + } + return nil +} diff --git a/listener/tap/tap_windows.go b/listener/tap/tap_windows.go new file mode 100644 index 0000000..e3a8936 --- /dev/null +++ b/listener/tap/tap_windows.go @@ -0,0 +1,75 @@ +package tap + +import ( + "fmt" + "net" + "os/exec" + "strings" + + "github.com/songgao/water" +) + +func (l *tapListener) createTap() (ifce *water.Interface, ip net.IP, err error) { + ip, ipNet, _ := net.ParseCIDR(l.md.config.Net) + + ifce, err = water.New(water.Config{ + DeviceType: water.TAP, + PlatformSpecificParams: water.PlatformSpecificParams{ + ComponentID: "tap0901", + InterfaceName: l.md.config.Name, + Network: l.md.config.Net, + }, + }) + if err != nil { + return + } + + if ip != nil && ipNet != nil { + cmd := fmt.Sprintf("netsh interface ip set address name=%s "+ + "source=static addr=%s mask=%s gateway=none", + ifce.Name(), ip.String(), ipMask(ipNet.Mask)) + l.logger.Debug(cmd) + + args := strings.Split(cmd, " ") + if er := exec.Command(args[0], args[1:]...).Run(); er != nil { + err = fmt.Errorf("%s: %v", cmd, er) + return + } + } + + if err = l.addRoutes(ifce.Name(), l.md.config.Gateway, l.md.config.Routes...); err != nil { + return + } + + return +} + +func (l *tapListener) addRoutes(ifName string, gw string, routes ...string) error { + for _, route := range routes { + l.deleteRoute(ifName, route) + + cmd := fmt.Sprintf("netsh interface ip add route prefix=%s interface=%s store=active", + route, ifName) + if gw != "" { + cmd += " nexthop=" + gw + } + l.logger.Debug(cmd) + args := strings.Split(cmd, " ") + if er := exec.Command(args[0], args[1:]...).Run(); er != nil { + return fmt.Errorf("%s: %v", cmd, er) + } + } + return nil +} + +func (l *tapListener) deleteRoute(ifName string, route string) error { + cmd := fmt.Sprintf("netsh interface ip delete route prefix=%s interface=%s store=active", + route, ifName) + l.logger.Debug(cmd) + args := strings.Split(cmd, " ") + return exec.Command(args[0], args[1:]...).Run() +} + +func ipMask(mask net.IPMask) string { + return fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3]) +} diff --git a/listener/tun/listener.go b/listener/tun/listener.go new file mode 100644 index 0000000..08a4540 --- /dev/null +++ b/listener/tun/listener.go @@ -0,0 +1,96 @@ +package tun + +import ( + "net" + + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + tun_util "github.com/go-gost/x/internal/util/tun" +) + +func init() { + registry.ListenerRegistry().Register("tun", NewListener) +} + +type tunListener struct { + addr net.Addr + cqueue chan net.Conn + closed chan struct{} + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &tunListener{ + logger: options.Logger, + options: options, + } +} + +func (l *tunListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + l.addr, err = net.ResolveUDPAddr("udp", l.options.Addr) + if err != nil { + return + } + + ifce, ip, err := l.createTun() + if err != nil { + if ifce != nil { + ifce.Close() + } + return + } + + itf, err := net.InterfaceByName(ifce.Name()) + if err != nil { + return + } + + addrs, _ := itf.Addrs() + l.logger.Infof("name: %s, net: %s, mtu: %d, addrs: %s", + itf.Name, ip, itf.MTU, addrs) + + l.cqueue = make(chan net.Conn, 1) + l.closed = make(chan struct{}) + + conn := tun_util.NewConn(l.md.config, ifce, l.addr, &net.IPAddr{IP: ip}) + + l.cqueue <- conn + + return +} + +func (l *tunListener) Accept() (net.Conn, error) { + select { + case conn := <-l.cqueue: + return conn, nil + case <-l.closed: + } + + return nil, listener.ErrClosed +} + +func (l *tunListener) Addr() net.Addr { + return l.addr +} + +func (l *tunListener) Close() error { + select { + case <-l.closed: + return net.ErrClosed + default: + close(l.closed) + } + return nil +} diff --git a/listener/tun/metadata.go b/listener/tun/metadata.go new file mode 100644 index 0000000..8418b4a --- /dev/null +++ b/listener/tun/metadata.go @@ -0,0 +1,63 @@ +package tun + +import ( + "net" + "strings" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" + tun_util "github.com/go-gost/x/internal/util/tun" +) + +const ( + DefaultMTU = 1350 +) + +type metadata struct { + config *tun_util.Config +} + +func (l *tunListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + name = "name" + netKey = "net" + peer = "peer" + mtu = "mtu" + routes = "routes" + gateway = "gw" + ) + + config := &tun_util.Config{ + Name: mdata.GetString(md, name), + Net: mdata.GetString(md, netKey), + Peer: mdata.GetString(md, peer), + MTU: mdata.GetInt(md, mtu), + Gateway: mdata.GetString(md, gateway), + } + if config.MTU <= 0 { + config.MTU = DefaultMTU + } + + gw := net.ParseIP(config.Gateway) + + for _, s := range mdata.GetStrings(md, routes) { + ss := strings.SplitN(s, " ", 2) + if len(ss) == 2 { + var route tun_util.Route + _, ipNet, _ := net.ParseCIDR(strings.TrimSpace(ss[0])) + if ipNet == nil { + continue + } + route.Net = *ipNet + route.Gateway = net.ParseIP(ss[1]) + if route.Gateway == nil { + route.Gateway = gw + } + + config.Routes = append(config.Routes, route) + } + } + + l.md.config = config + + return +} diff --git a/listener/tun/tun_darwin.go b/listener/tun/tun_darwin.go new file mode 100644 index 0000000..32db1e7 --- /dev/null +++ b/listener/tun/tun_darwin.go @@ -0,0 +1,57 @@ +package tun + +import ( + "fmt" + "net" + "os/exec" + "strings" + + tun_util "github.com/go-gost/x/internal/util/tun" + "github.com/songgao/water" +) + +func (l *tunListener) createTun() (ifce *water.Interface, ip net.IP, err error) { + ip, _, err = net.ParseCIDR(l.md.config.Net) + if err != nil { + return + } + + ifce, err = water.New(water.Config{ + DeviceType: water.TUN, + }) + if err != nil { + return + } + + peer := l.md.config.Peer + if peer == "" { + peer = ip.String() + } + + cmd := fmt.Sprintf("ifconfig %s inet %s %s mtu %d up", + ifce.Name(), l.md.config.Net, l.md.config.Peer, l.md.config.MTU) + l.logger.Debug(cmd) + + args := strings.Split(cmd, " ") + if err = exec.Command(args[0], args[1:]...).Run(); err != nil { + return + } + + if err = l.addRoutes(ifce.Name(), l.md.config.Routes...); err != nil { + return + } + + return +} + +func (l *tunListener) addRoutes(ifName string, routes ...tun_util.Route) error { + for _, route := range routes { + cmd := fmt.Sprintf("route add -net %s -interface %s", route.Net.String(), ifName) + l.logger.Debug(cmd) + args := strings.Split(cmd, " ") + if err := exec.Command(args[0], args[1:]...).Run(); err != nil { + return err + } + } + return nil +} diff --git a/listener/tun/tun_linux.go b/listener/tun/tun_linux.go new file mode 100644 index 0000000..5dc546a --- /dev/null +++ b/listener/tun/tun_linux.go @@ -0,0 +1,67 @@ +package tun + +import ( + "errors" + "net" + "syscall" + + "github.com/docker/libcontainer/netlink" + tun_util "github.com/go-gost/x/internal/util/tun" + "github.com/milosgajdos/tenus" + "github.com/songgao/water" +) + +func (l *tunListener) createTun() (ifce *water.Interface, ip net.IP, err error) { + ip, ipNet, err := net.ParseCIDR(l.md.config.Net) + if err != nil { + return + } + + ifce, err = water.New(water.Config{ + DeviceType: water.TUN, + PlatformSpecificParams: water.PlatformSpecificParams{ + Name: l.md.config.Name, + }, + }) + if err != nil { + return + } + + link, err := tenus.NewLinkFrom(ifce.Name()) + if err != nil { + return + } + + l.logger.Debugf("ip link set dev %s mtu %d", ifce.Name(), l.md.config.MTU) + + if err = link.SetLinkMTU(l.md.config.MTU); err != nil { + return + } + + l.logger.Debugf("ip address add %s dev %s", l.md.config.Net, ifce.Name()) + + if err = link.SetLinkIp(ip, ipNet); err != nil { + return + } + + l.logger.Debugf("ip link set dev %s up", ifce.Name()) + if err = link.SetLinkUp(); err != nil { + return + } + + if err = l.addRoutes(ifce.Name(), l.md.config.Routes...); err != nil { + return + } + + return +} + +func (l *tunListener) addRoutes(ifName string, routes ...tun_util.Route) error { + for _, route := range routes { + l.logger.Debugf("ip route add %s dev %s", route.Net.String(), ifName) + if err := netlink.AddRoute(route.Net.String(), "", "", ifName); err != nil && !errors.Is(err, syscall.EEXIST) { + return err + } + } + return nil +} diff --git a/listener/tun/tun_unix.go b/listener/tun/tun_unix.go new file mode 100644 index 0000000..72b2531 --- /dev/null +++ b/listener/tun/tun_unix.go @@ -0,0 +1,55 @@ +//go:build !linux && !windows && !darwin + +package tun + +import ( + "fmt" + "net" + "os/exec" + "strings" + + tun_util "github.com/go-gost/x/internal/util/tun" + "github.com/songgao/water" +) + +func (l *tunListener) createTun() (ifce *water.Interface, ip net.IP, err error) { + ip, _, err = net.ParseCIDR(l.md.config.Net) + if err != nil { + return + } + + ifce, err = water.New(water.Config{ + DeviceType: water.TUN, + }) + if err != nil { + return + } + + cmd := fmt.Sprintf("ifconfig %s inet %s mtu %d up", + ifce.Name(), l.md.config.Net, l.md.config.MTU) + l.logger.Debug(cmd) + + args := strings.Split(cmd, " ") + if er := exec.Command(args[0], args[1:]...).Run(); er != nil { + err = fmt.Errorf("%s: %v", cmd, er) + return + } + + if err = l.addRoutes(ifce.Name(), l.md.config.Routes...); err != nil { + return + } + + return +} + +func (l *tunListener) addRoutes(ifName string, routes ...tun_util.Route) error { + for _, route := range routes { + cmd := fmt.Sprintf("route add -net %s -interface %s", route.Net.String(), ifName) + l.logger.Debug(cmd) + args := strings.Split(cmd, " ") + if er := exec.Command(args[0], args[1:]...).Run(); er != nil { + return fmt.Errorf("%s: %v", cmd, er) + } + } + return nil +} diff --git a/listener/tun/tun_windows.go b/listener/tun/tun_windows.go new file mode 100644 index 0000000..8bbe7f4 --- /dev/null +++ b/listener/tun/tun_windows.go @@ -0,0 +1,77 @@ +package tun + +import ( + "fmt" + "net" + "os/exec" + "strings" + + tun_util "github.com/go-gost/x/internal/util/tun" + "github.com/songgao/water" +) + +func (l *tunListener) createTun() (ifce *water.Interface, ip net.IP, err error) { + ip, ipNet, err := net.ParseCIDR(l.md.config.Net) + if err != nil { + return + } + + ifce, err = water.New(water.Config{ + DeviceType: water.TUN, + PlatformSpecificParams: water.PlatformSpecificParams{ + ComponentID: "tap0901", + InterfaceName: l.md.config.Name, + Network: l.md.config.Net, + }, + }) + if err != nil { + return + } + + cmd := fmt.Sprintf("netsh interface ip set address name=%s "+ + "source=static addr=%s mask=%s gateway=none", + ifce.Name(), ip.String(), ipMask(ipNet.Mask)) + l.logger.Debug(cmd) + + args := strings.Split(cmd, " ") + if er := exec.Command(args[0], args[1:]...).Run(); er != nil { + err = fmt.Errorf("%s: %v", cmd, er) + return + } + + if err = l.addRoutes(ifce.Name(), l.md.config.Gateway, l.md.config.Routes...); err != nil { + return + } + + return +} + +func (l *tunListener) addRoutes(ifName string, gw string, routes ...tun_util.Route) error { + for _, route := range routes { + l.deleteRoute(ifName, route.Net.String()) + + cmd := fmt.Sprintf("netsh interface ip add route prefix=%s interface=%s store=active", + route.Net.String(), ifName) + if gw != "" { + cmd += " nexthop=" + gw + } + l.logger.Debug(cmd) + args := strings.Split(cmd, " ") + if er := exec.Command(args[0], args[1:]...).Run(); er != nil { + return fmt.Errorf("%s: %v", cmd, er) + } + } + return nil +} + +func (l *tunListener) deleteRoute(ifName string, route string) error { + cmd := fmt.Sprintf("netsh interface ip delete route prefix=%s interface=%s store=active", + route, ifName) + l.logger.Debug(cmd) + args := strings.Split(cmd, " ") + return exec.Command(args[0], args[1:]...).Run() +} + +func ipMask(mask net.IPMask) string { + return fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3]) +} diff --git a/listener/ws/listener.go b/listener/ws/listener.go new file mode 100644 index 0000000..1d088c4 --- /dev/null +++ b/listener/ws/listener.go @@ -0,0 +1,149 @@ +package ws + +import ( + "crypto/tls" + "net" + "net/http" + "net/http/httputil" + + "github.com/go-gost/gost/v3/pkg/common/admission" + "github.com/go-gost/gost/v3/pkg/common/metrics" + "github.com/go-gost/gost/v3/pkg/listener" + "github.com/go-gost/gost/v3/pkg/logger" + md "github.com/go-gost/gost/v3/pkg/metadata" + "github.com/go-gost/gost/v3/pkg/registry" + ws_util "github.com/go-gost/x/internal/util/ws" + "github.com/gorilla/websocket" +) + +func init() { + registry.ListenerRegistry().Register("ws", NewListener) + registry.ListenerRegistry().Register("wss", NewTLSListener) +} + +type wsListener struct { + addr net.Addr + upgrader *websocket.Upgrader + srv *http.Server + tlsEnabled bool + cqueue chan net.Conn + errChan chan error + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &wsListener{ + logger: options.Logger, + options: options, + } +} + +func NewTLSListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &wsListener{ + tlsEnabled: true, + logger: options.Logger, + options: options, + } +} + +func (l *wsListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + l.upgrader = &websocket.Upgrader{ + HandshakeTimeout: l.md.handshakeTimeout, + ReadBufferSize: l.md.readBufferSize, + WriteBufferSize: l.md.writeBufferSize, + EnableCompression: l.md.enableCompression, + CheckOrigin: func(r *http.Request) bool { return true }, + } + + mux := http.NewServeMux() + mux.Handle(l.md.path, http.HandlerFunc(l.upgrade)) + l.srv = &http.Server{ + Addr: l.options.Addr, + Handler: mux, + ReadHeaderTimeout: l.md.readHeaderTimeout, + } + + l.cqueue = make(chan net.Conn, l.md.backlog) + l.errChan = make(chan error, 1) + + ln, err := net.Listen("tcp", l.options.Addr) + if err != nil { + return + } + ln = metrics.WrapListener(l.options.Service, ln) + ln = admission.WrapListener(l.options.Admission, ln) + + if l.tlsEnabled { + ln = tls.NewListener(ln, l.options.TLSConfig) + } + + l.addr = ln.Addr() + + go func() { + err := l.srv.Serve(ln) + if err != nil { + l.errChan <- err + } + close(l.errChan) + }() + + return +} + +func (l *wsListener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.cqueue: + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *wsListener) Close() error { + return l.srv.Close() +} + +func (l *wsListener) Addr() net.Addr { + return l.addr +} + +func (l *wsListener) upgrade(w http.ResponseWriter, r *http.Request) { + if l.logger.IsLevelEnabled(logger.DebugLevel) { + log := l.logger.WithFields(map[string]any{ + "local": l.addr.String(), + "remote": r.RemoteAddr, + }) + dump, _ := httputil.DumpRequest(r, false) + log.Debug(string(dump)) + } + + conn, err := l.upgrader.Upgrade(w, r, l.md.header) + if err != nil { + l.logger.Error(err) + return + } + + select { + case l.cqueue <- ws_util.Conn(conn): + default: + conn.Close() + l.logger.Warnf("connection queue is full, client %s discarded", conn.RemoteAddr()) + } +} diff --git a/listener/ws/metadata.go b/listener/ws/metadata.go new file mode 100644 index 0000000..c0f60bd --- /dev/null +++ b/listener/ws/metadata.go @@ -0,0 +1,66 @@ +package ws + +import ( + "net/http" + "time" + + mdata "github.com/go-gost/gost/v3/pkg/metadata" +) + +const ( + defaultPath = "/ws" + defaultBacklog = 128 +) + +type metadata struct { + path string + backlog int + + handshakeTimeout time.Duration + readHeaderTimeout time.Duration + readBufferSize int + writeBufferSize int + enableCompression bool + + header http.Header +} + +func (l *wsListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + path = "path" + backlog = "backlog" + + handshakeTimeout = "handshakeTimeout" + readHeaderTimeout = "readHeaderTimeout" + readBufferSize = "readBufferSize" + writeBufferSize = "writeBufferSize" + enableCompression = "enableCompression" + + header = "header" + ) + + l.md.path = mdata.GetString(md, path) + if l.md.path == "" { + l.md.path = defaultPath + } + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + l.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + l.md.readHeaderTimeout = mdata.GetDuration(md, readHeaderTimeout) + l.md.readBufferSize = mdata.GetInt(md, readBufferSize) + l.md.writeBufferSize = mdata.GetInt(md, writeBufferSize) + l.md.enableCompression = mdata.GetBool(md, enableCompression) + + if mm := mdata.GetStringMapString(md, header); len(mm) > 0 { + hd := http.Header{} + for k, v := range mm { + hd.Add(k, v) + } + l.md.header = hd + } + return +}