From c959fc2f7376ebbdbd147ed933701c7b844ab25e Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Wed, 3 Jan 2024 20:55:06 +0800 Subject: [PATCH 1/5] add observer --- admission/plugin/grpc.go | 67 ++++++ admission/{plugin.go => plugin/http.go} | 58 ----- api/config.go | 41 ++++ auth/auth.go | 2 +- auth/plugin/grpc.go | 72 ++++++ auth/{plugin.go => plugin/http.go} | 62 ----- bypass/plugin/grpc.go | 77 ++++++ bypass/{plugin.go => plugin/http.go} | 67 ------ config/config.go | 30 +++ config/parsing/admission/parse.go | 5 +- config/parsing/auth/parse.go | 5 +- config/parsing/bypass/parse.go | 5 +- config/parsing/hop/parse.go | 5 +- config/parsing/hosts/parse.go | 5 +- config/parsing/ingress/parse.go | 5 +- config/parsing/limiter/parse.go | 5 +- config/parsing/observer/parse.go | 39 +++ config/parsing/parse.go | 1 + config/parsing/recorder/parse.go | 5 +- config/parsing/resolver/parse.go | 5 +- config/parsing/router/parse.go | 5 +- config/parsing/sd/parse.go | 6 +- config/parsing/service/parse.go | 10 + go.mod | 4 +- go.sum | 8 +- handler/http/handler.go | 51 +++- handler/http2/handler.go | 47 +++- handler/relay/connect.go | 14 +- handler/relay/forward.go | 14 +- handler/relay/handler.go | 35 ++- handler/socks/v4/handler.go | 50 +++- handler/socks/v5/connect.go | 14 +- handler/socks/v5/handler.go | 36 +++ handler/socks/v5/udp.go | 15 +- handler/socks/v5/udp_tun.go | 12 + handler/tunnel/connect.go | 2 +- hop/plugin/grpc.go | 101 ++++++++ hop/{plugin.go => plugin/http.go} | 86 ------- hosts/plugin/grpc.go | 78 ++++++ hosts/{plugin.go => plugin/http.go} | 67 ------ ingress/plugin/grpc.go | 89 +++++++ ingress/{plugin.go => plugin/http.go} | 80 ------- internal/util/stats/stats.go | 62 +++++ limiter/traffic/plugin/grpc.go | 102 ++++++++ limiter/traffic/{plugin.go => plugin/http.go} | 97 +------- limiter/traffic/wrapper/io.go | 2 +- listener/dns/listener.go | 2 + listener/dtls/listener.go | 2 + listener/ftcp/listener.go | 2 + listener/grpc/listener.go | 2 + listener/http2/h2/listener.go | 2 + listener/http2/listener.go | 2 + listener/http3/h3/listener.go | 2 + listener/http3/wt/listener.go | 8 + listener/icmp/listener.go | 2 + listener/kcp/listener.go | 2 + listener/mtcp/listener.go | 2 + listener/mtls/listener.go | 2 + listener/mws/listener.go | 2 + listener/obfs/http/listener.go | 7 +- listener/obfs/tls/listener.go | 2 + listener/pht/listener.go | 2 + listener/quic/listener.go | 2 + listener/redirect/tcp/listener.go | 2 + listener/redirect/udp/listener.go | 2 + listener/rtcp/listener.go | 2 + listener/rudp/listener.go | 2 + listener/serial/listener.go | 2 + listener/ssh/listener.go | 2 + listener/sshd/listener.go | 2 + listener/tap/listener.go | 2 + listener/tcp/listener.go | 2 + listener/tls/listener.go | 2 + listener/tun/listener.go | 4 +- listener/udp/listener.go | 2 + listener/unix/listener.go | 2 + listener/ws/listener.go | 2 + observer/plugin/grpc.go | 98 ++++++++ observer/plugin/http.go | 134 +++++++++++ recorder/plugin/grpc.go | 76 ++++++ recorder/{plugin.go => plugin/http.go} | 66 ------ registry/observer.go | 39 +++ registry/registry.go | 12 +- resolver/plugin/grpc.go | 77 ++++++ resolver/{plugin.go => plugin/http.go} | 66 ------ router/plugin/grpc.go | 70 ++++++ router/{plugin.go => plugin/http.go} | 62 +---- sd/plugin/grpc.go | 134 +++++++++++ sd/{plugin.go => plugin/http.go} | 127 +--------- service/service.go | 116 ++++++++- service/status.go | 76 ++++++ stats/stats.go | 93 ++++++++ stats/wrapper/conn.go | 222 ++++++++++++++++++ stats/wrapper/io.go | 38 +++ stats/wrapper/listener.go | 32 +++ 95 files changed, 2371 insertions(+), 890 deletions(-) create mode 100644 admission/plugin/grpc.go rename admission/{plugin.go => plugin/http.go} (58%) create mode 100644 auth/plugin/grpc.go rename auth/{plugin.go => plugin/http.go} (57%) create mode 100644 bypass/plugin/grpc.go rename bypass/{plugin.go => plugin/http.go} (59%) create mode 100644 config/parsing/observer/parse.go create mode 100644 hop/plugin/grpc.go rename hop/{plugin.go => plugin/http.go} (60%) create mode 100644 hosts/plugin/grpc.go rename hosts/{plugin.go => plugin/http.go} (59%) create mode 100644 ingress/plugin/grpc.go rename ingress/{plugin.go => plugin/http.go} (63%) create mode 100644 internal/util/stats/stats.go create mode 100644 limiter/traffic/plugin/grpc.go rename limiter/traffic/{plugin.go => plugin/http.go} (59%) create mode 100644 observer/plugin/grpc.go create mode 100644 observer/plugin/http.go create mode 100644 recorder/plugin/grpc.go rename recorder/{plugin.go => plugin/http.go} (59%) create mode 100644 registry/observer.go create mode 100644 resolver/plugin/grpc.go rename resolver/{plugin.go => plugin/http.go} (60%) create mode 100644 router/plugin/grpc.go rename router/{plugin.go => plugin/http.go} (57%) create mode 100644 sd/plugin/grpc.go rename sd/{plugin.go => plugin/http.go} (62%) create mode 100644 service/status.go create mode 100644 stats/stats.go create mode 100644 stats/wrapper/conn.go create mode 100644 stats/wrapper/io.go create mode 100644 stats/wrapper/listener.go diff --git a/admission/plugin/grpc.go b/admission/plugin/grpc.go new file mode 100644 index 0000000..0882497 --- /dev/null +++ b/admission/plugin/grpc.go @@ -0,0 +1,67 @@ +package admission + +import ( + "context" + "io" + + "github.com/go-gost/core/admission" + "github.com/go-gost/core/logger" + "github.com/go-gost/plugin/admission/proto" + "github.com/go-gost/x/internal/plugin" + "google.golang.org/grpc" +) + +type grpcPlugin struct { + conn grpc.ClientConnInterface + client proto.AdmissionClient + log logger.Logger +} + +// NewGRPCPlugin creates an Admission plugin based on gRPC. +func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) admission.Admission { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "admission", + "admission": name, + }) + conn, err := plugin.NewGRPCConn(addr, &options) + if err != nil { + log.Error(err) + } + + p := &grpcPlugin{ + conn: conn, + log: log, + } + if conn != nil { + p.client = proto.NewAdmissionClient(conn) + } + return p +} + +func (p *grpcPlugin) Admit(ctx context.Context, addr string, opts ...admission.Option) bool { + if p.client == nil { + return false + } + + r, err := p.client.Admit(ctx, + &proto.AdmissionRequest{ + Addr: addr, + }) + if err != nil { + p.log.Error(err) + return false + } + return r.Ok +} + +func (p *grpcPlugin) Close() error { + if closer, ok := p.conn.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/admission/plugin.go b/admission/plugin/http.go similarity index 58% rename from admission/plugin.go rename to admission/plugin/http.go index 6942ab0..3cdfd13 100644 --- a/admission/plugin.go +++ b/admission/plugin/http.go @@ -4,71 +4,13 @@ import ( "bytes" "context" "encoding/json" - "io" "net/http" "github.com/go-gost/core/admission" "github.com/go-gost/core/logger" - "github.com/go-gost/plugin/admission/proto" "github.com/go-gost/x/internal/plugin" - "google.golang.org/grpc" ) -type grpcPlugin struct { - conn grpc.ClientConnInterface - client proto.AdmissionClient - log logger.Logger -} - -// NewGRPCPlugin creates an Admission plugin based on gRPC. -func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) admission.Admission { - var options plugin.Options - for _, opt := range opts { - opt(&options) - } - - log := logger.Default().WithFields(map[string]any{ - "kind": "admission", - "admission": name, - }) - conn, err := plugin.NewGRPCConn(addr, &options) - if err != nil { - log.Error(err) - } - - p := &grpcPlugin{ - conn: conn, - log: log, - } - if conn != nil { - p.client = proto.NewAdmissionClient(conn) - } - return p -} - -func (p *grpcPlugin) Admit(ctx context.Context, addr string, opts ...admission.Option) bool { - if p.client == nil { - return false - } - - r, err := p.client.Admit(ctx, - &proto.AdmissionRequest{ - Addr: addr, - }) - if err != nil { - p.log.Error(err) - return false - } - return r.Ok -} - -func (p *grpcPlugin) Close() error { - if closer, ok := p.conn.(io.Closer); ok { - return closer.Close() - } - return nil -} - type httpPluginRequest struct { Addr string `json:"addr"` } diff --git a/api/config.go b/api/config.go index affa7b3..3cb09ea 100644 --- a/api/config.go +++ b/api/config.go @@ -8,8 +8,15 @@ import ( "github.com/gin-gonic/gin" "github.com/go-gost/x/config" + "github.com/go-gost/x/registry" + "github.com/go-gost/x/service" + "github.com/go-gost/x/stats" ) +type serviceStatus interface { + Status() *service.Status +} + // swagger:parameters getConfigRequest type getConfigRequest struct { // output format, one of yaml|json, default is json. @@ -37,6 +44,40 @@ func getConfig(ctx *gin.Context) { var req getConfigRequest ctx.ShouldBindQuery(&req) + config.OnUpdate(func(c *config.Config) error { + for _, svc := range c.Services { + if svc == nil { + continue + } + s := registry.ServiceRegistry().Get(svc.Name) + ss, ok := s.(serviceStatus) + if ok && ss != nil { + status := ss.Status() + svc.Status = &config.ServiceStatus{ + CreateTime: status.CreateTime().Unix(), + State: string(status.State()), + } + if st := status.Stats(); st != nil { + svc.Status.Stats = &config.ServiceStats{ + TotalConns: st.Get(stats.KindTotalConns), + CurrentConns: st.Get(stats.KindCurrentConns), + TotalErrs: st.Get(stats.KindTotalErrs), + InputBytes: st.Get(stats.KindInputBytes), + OutputBytes: st.Get(stats.KindOutputBytes), + } + } + for _, ev := range status.Events() { + if !ev.Time.IsZero() { + svc.Status.Events = append(svc.Status.Events, config.ServiceEvent{ + Time: ev.Time.Unix(), + Msg: ev.Message, + }) + } + } + } + } + return nil + }) var resp getConfigResponse resp.Config = config.Global() diff --git a/auth/auth.go b/auth/auth.go index 62e9b89..ed6c506 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -110,7 +110,7 @@ func (p *authenticator) Authenticate(ctx context.Context, user, password string, } v, ok := p.kvs[user] - return "", ok && (v == "" || password == v) + return user, ok && (v == "" || password == v) } func (p *authenticator) periodReload(ctx context.Context) error { diff --git a/auth/plugin/grpc.go b/auth/plugin/grpc.go new file mode 100644 index 0000000..625327a --- /dev/null +++ b/auth/plugin/grpc.go @@ -0,0 +1,72 @@ +package auth + +import ( + "context" + "io" + + "github.com/go-gost/core/auth" + "github.com/go-gost/core/logger" + "github.com/go-gost/plugin/auth/proto" + ctxvalue "github.com/go-gost/x/internal/ctx" + "github.com/go-gost/x/internal/plugin" + "google.golang.org/grpc" +) + +type grpcPlugin struct { + conn grpc.ClientConnInterface + client proto.AuthenticatorClient + log logger.Logger +} + +// NewGRPCPlugin creates an Authenticator plugin based on gRPC. +func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) auth.Authenticator { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "auther", + "auther": name, + }) + conn, err := plugin.NewGRPCConn(addr, &options) + if err != nil { + log.Error(err) + } + + p := &grpcPlugin{ + conn: conn, + log: log, + } + + if conn != nil { + p.client = proto.NewAuthenticatorClient(conn) + } + return p +} + +// Authenticate checks the validity of the provided user-password pair. +func (p *grpcPlugin) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) { + if p.client == nil { + return "", false + } + + r, err := p.client.Authenticate(ctx, + &proto.AuthenticateRequest{ + Username: user, + Password: password, + Client: string(ctxvalue.ClientAddrFromContext(ctx)), + }) + if err != nil { + p.log.Error(err) + return "", false + } + return r.Id, r.Ok +} + +func (p *grpcPlugin) Close() error { + if closer, ok := p.conn.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/auth/plugin.go b/auth/plugin/http.go similarity index 57% rename from auth/plugin.go rename to auth/plugin/http.go index 8ada348..4b5bc5d 100644 --- a/auth/plugin.go +++ b/auth/plugin/http.go @@ -4,76 +4,14 @@ import ( "bytes" "context" "encoding/json" - "io" "net/http" "github.com/go-gost/core/auth" "github.com/go-gost/core/logger" - "github.com/go-gost/plugin/auth/proto" ctxvalue "github.com/go-gost/x/internal/ctx" "github.com/go-gost/x/internal/plugin" - "google.golang.org/grpc" ) -type grpcPlugin struct { - conn grpc.ClientConnInterface - client proto.AuthenticatorClient - log logger.Logger -} - -// NewGRPCPlugin creates an Authenticator plugin based on gRPC. -func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) auth.Authenticator { - var options plugin.Options - for _, opt := range opts { - opt(&options) - } - - log := logger.Default().WithFields(map[string]any{ - "kind": "auther", - "auther": name, - }) - conn, err := plugin.NewGRPCConn(addr, &options) - if err != nil { - log.Error(err) - } - - p := &grpcPlugin{ - conn: conn, - log: log, - } - - if conn != nil { - p.client = proto.NewAuthenticatorClient(conn) - } - return p -} - -// Authenticate checks the validity of the provided user-password pair. -func (p *grpcPlugin) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) { - if p.client == nil { - return "", false - } - - r, err := p.client.Authenticate(ctx, - &proto.AuthenticateRequest{ - Username: user, - Password: password, - Client: string(ctxvalue.ClientAddrFromContext(ctx)), - }) - if err != nil { - p.log.Error(err) - return "", false - } - return r.Id, r.Ok -} - -func (p *grpcPlugin) Close() error { - if closer, ok := p.conn.(io.Closer); ok { - return closer.Close() - } - return nil -} - type httpPluginRequest struct { Username string `json:"username"` Password string `json:"password"` diff --git a/bypass/plugin/grpc.go b/bypass/plugin/grpc.go new file mode 100644 index 0000000..1d8e8b3 --- /dev/null +++ b/bypass/plugin/grpc.go @@ -0,0 +1,77 @@ +package bypass + +import ( + "context" + "io" + + "github.com/go-gost/core/bypass" + "github.com/go-gost/core/logger" + "github.com/go-gost/plugin/bypass/proto" + ctxvalue "github.com/go-gost/x/internal/ctx" + "github.com/go-gost/x/internal/plugin" + "google.golang.org/grpc" +) + +type grpcPlugin struct { + conn grpc.ClientConnInterface + client proto.BypassClient + log logger.Logger +} + +// NewGRPCPlugin creates a Bypass plugin based on gRPC. +func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) bypass.Bypass { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "bypass", + "bypass": name, + }) + conn, err := plugin.NewGRPCConn(addr, &options) + if err != nil { + log.Error(err) + } + + p := &grpcPlugin{ + conn: conn, + log: log, + } + if conn != nil { + p.client = proto.NewBypassClient(conn) + } + return p +} + +func (p *grpcPlugin) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) bool { + if p.client == nil { + return true + } + + var options bypass.Options + for _, opt := range opts { + opt(&options) + } + + r, err := p.client.Bypass(ctx, + &proto.BypassRequest{ + Network: network, + Addr: addr, + Client: string(ctxvalue.ClientIDFromContext(ctx)), + Host: options.Host, + Path: options.Path, + }) + if err != nil { + p.log.Error(err) + return true + } + return r.Ok +} + +func (p *grpcPlugin) Close() error { + if closer, ok := p.conn.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/bypass/plugin.go b/bypass/plugin/http.go similarity index 59% rename from bypass/plugin.go rename to bypass/plugin/http.go index b18919c..9add03c 100644 --- a/bypass/plugin.go +++ b/bypass/plugin/http.go @@ -4,81 +4,14 @@ import ( "bytes" "context" "encoding/json" - "io" "net/http" "github.com/go-gost/core/bypass" "github.com/go-gost/core/logger" - "github.com/go-gost/plugin/bypass/proto" ctxvalue "github.com/go-gost/x/internal/ctx" "github.com/go-gost/x/internal/plugin" - "google.golang.org/grpc" ) -type grpcPlugin struct { - conn grpc.ClientConnInterface - client proto.BypassClient - log logger.Logger -} - -// NewGRPCPlugin creates a Bypass plugin based on gRPC. -func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) bypass.Bypass { - var options plugin.Options - for _, opt := range opts { - opt(&options) - } - - log := logger.Default().WithFields(map[string]any{ - "kind": "bypass", - "bypass": name, - }) - conn, err := plugin.NewGRPCConn(addr, &options) - if err != nil { - log.Error(err) - } - - p := &grpcPlugin{ - conn: conn, - log: log, - } - if conn != nil { - p.client = proto.NewBypassClient(conn) - } - return p -} - -func (p *grpcPlugin) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) bool { - if p.client == nil { - return true - } - - var options bypass.Options - for _, opt := range opts { - opt(&options) - } - - r, err := p.client.Bypass(ctx, - &proto.BypassRequest{ - Network: network, - Addr: addr, - Client: string(ctxvalue.ClientIDFromContext(ctx)), - Host: options.Host, - Path: options.Path, - }) - if err != nil { - p.log.Error(err) - return true - } - return r.Ok -} - -func (p *grpcPlugin) Close() error { - if closer, ok := p.conn.(io.Closer); ok { - return closer.Close() - } - return nil -} - type httpPluginRequest struct { Network string `json:"network"` Addr string `json:"addr"` diff --git a/config/config.go b/config/config.go index e5f08cb..7c6333f 100644 --- a/config/config.go +++ b/config/config.go @@ -312,6 +312,11 @@ type LimiterConfig struct { Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"` } +type ObserverConfig struct { + Name string `json:"name"` + Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"` +} + type ListenerConfig struct { Type string `json:"type"` Chain string `yaml:",omitempty" json:"chain,omitempty"` @@ -333,6 +338,7 @@ type HandlerConfig struct { Auth *AuthConfig `yaml:",omitempty" json:"auth,omitempty"` TLS *TLSConfig `yaml:",omitempty" json:"tls,omitempty"` Limiter string `yaml:",omitempty" json:"limiter,omitempty"` + Observer string `yaml:",omitempty" json:"observer,omitempty"` Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"` } @@ -403,11 +409,34 @@ type ServiceConfig struct { RLimiter string `yaml:"rlimiter,omitempty" json:"rlimiter,omitempty"` Logger string `yaml:",omitempty" json:"logger,omitempty"` Loggers []string `yaml:",omitempty" json:"loggers,omitempty"` + Observer string `yaml:",omitempty" json:"observer,omitempty"` Recorders []*RecorderObject `yaml:",omitempty" json:"recorders,omitempty"` Handler *HandlerConfig `yaml:",omitempty" json:"handler,omitempty"` Listener *ListenerConfig `yaml:",omitempty" json:"listener,omitempty"` Forwarder *ForwarderConfig `yaml:",omitempty" json:"forwarder,omitempty"` Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"` + // service status, read-only + Status *ServiceStatus `yaml:",omitempty" json:"status,omitempty"` +} + +type ServiceStatus struct { + CreateTime int64 `yaml:"createTime" json:"createTime"` + State string `yaml:"state" json:"state"` + Events []ServiceEvent `yaml:",omitempty" json:"events,omitempty"` + Stats *ServiceStats `yaml:",omitempty" json:"stats,omitempty"` +} + +type ServiceEvent struct { + Time int64 `yaml:"time" json:"time"` + Msg string `yaml:"msg" json:"msg"` +} + +type ServiceStats struct { + TotalConns uint64 `yaml:"totalConns" json:"totalConns"` + CurrentConns uint64 `yaml:"currentConns" json:"currentConns"` + TotalErrs uint64 `yaml:"totalErrs" json:"totalErrs"` + InputBytes uint64 `yaml:"inputBytes" json:"inputBytes"` + OutputBytes uint64 `yaml:"outputBytes" json:"outputBytes"` } type ChainConfig struct { @@ -475,6 +504,7 @@ type Config struct { Limiters []*LimiterConfig `yaml:",omitempty" json:"limiters,omitempty"` CLimiters []*LimiterConfig `yaml:"climiters,omitempty" json:"climiters,omitempty"` RLimiters []*LimiterConfig `yaml:"rlimiters,omitempty" json:"rlimiters,omitempty"` + Observers []*ObserverConfig `yaml:",omitempty" json:"observers,omitempty"` Loggers []*LoggerConfig `yaml:",omitempty" json:"loggers,omitempty"` TLS *TLSConfig `yaml:",omitempty" json:"tls,omitempty"` Log *LogConfig `yaml:",omitempty" json:"log,omitempty"` diff --git a/config/parsing/admission/parse.go b/config/parsing/admission/parse.go index 4de13b7..407d9d8 100644 --- a/config/parsing/admission/parse.go +++ b/config/parsing/admission/parse.go @@ -7,6 +7,7 @@ import ( "github.com/go-gost/core/admission" "github.com/go-gost/core/logger" xadmission "github.com/go-gost/x/admission" + admission_plugin "github.com/go-gost/x/admission/plugin" "github.com/go-gost/x/config" "github.com/go-gost/x/internal/loader" "github.com/go-gost/x/internal/plugin" @@ -28,13 +29,13 @@ func ParseAdmission(cfg *config.AdmissionConfig) admission.Admission { } switch strings.ToLower(cfg.Plugin.Type) { case "http": - return xadmission.NewHTTPPlugin( + return admission_plugin.NewHTTPPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TLSConfigOption(tlsCfg), plugin.TimeoutOption(cfg.Plugin.Timeout), ) default: - return xadmission.NewGRPCPlugin( + return admission_plugin.NewGRPCPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TokenOption(cfg.Plugin.Token), plugin.TLSConfigOption(tlsCfg), diff --git a/config/parsing/auth/parse.go b/config/parsing/auth/parse.go index 651dff0..3e3be91 100644 --- a/config/parsing/auth/parse.go +++ b/config/parsing/auth/parse.go @@ -7,6 +7,7 @@ import ( "github.com/go-gost/core/auth" "github.com/go-gost/core/logger" xauth "github.com/go-gost/x/auth" + auth_plugin "github.com/go-gost/x/auth/plugin" "github.com/go-gost/x/config" "github.com/go-gost/x/internal/loader" "github.com/go-gost/x/internal/plugin" @@ -28,13 +29,13 @@ func ParseAuther(cfg *config.AutherConfig) auth.Authenticator { } switch cfg.Plugin.Type { case "http": - return xauth.NewHTTPPlugin( + return auth_plugin.NewHTTPPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TLSConfigOption(tlsCfg), plugin.TimeoutOption(cfg.Plugin.Timeout), ) default: - return xauth.NewGRPCPlugin( + return auth_plugin.NewGRPCPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TokenOption(cfg.Plugin.Token), plugin.TLSConfigOption(tlsCfg), diff --git a/config/parsing/bypass/parse.go b/config/parsing/bypass/parse.go index f2b3419..77c47ff 100644 --- a/config/parsing/bypass/parse.go +++ b/config/parsing/bypass/parse.go @@ -7,6 +7,7 @@ import ( "github.com/go-gost/core/bypass" "github.com/go-gost/core/logger" xbypass "github.com/go-gost/x/bypass" + bypass_plugin "github.com/go-gost/x/bypass/plugin" "github.com/go-gost/x/config" "github.com/go-gost/x/internal/loader" "github.com/go-gost/x/internal/plugin" @@ -28,13 +29,13 @@ func ParseBypass(cfg *config.BypassConfig) bypass.Bypass { } switch strings.ToLower(cfg.Plugin.Type) { case "http": - return xbypass.NewHTTPPlugin( + return bypass_plugin.NewHTTPPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TLSConfigOption(tlsCfg), plugin.TimeoutOption(cfg.Plugin.Timeout), ) default: - return xbypass.NewGRPCPlugin( + return bypass_plugin.NewGRPCPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TokenOption(cfg.Plugin.Token), plugin.TLSConfigOption(tlsCfg), diff --git a/config/parsing/hop/parse.go b/config/parsing/hop/parse.go index 420e330..fadd088 100644 --- a/config/parsing/hop/parse.go +++ b/config/parsing/hop/parse.go @@ -13,6 +13,7 @@ import ( node_parser "github.com/go-gost/x/config/parsing/node" selector_parser "github.com/go-gost/x/config/parsing/selector" xhop "github.com/go-gost/x/hop" + hop_plugin "github.com/go-gost/x/hop/plugin" "github.com/go-gost/x/internal/loader" "github.com/go-gost/x/internal/plugin" ) @@ -32,13 +33,13 @@ func ParseHop(cfg *config.HopConfig, log logger.Logger) (hop.Hop, error) { } switch strings.ToLower(cfg.Plugin.Type) { case "http": - return xhop.NewHTTPPlugin( + return hop_plugin.NewHTTPPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TLSConfigOption(tlsCfg), plugin.TimeoutOption(cfg.Plugin.Timeout), ), nil default: - return xhop.NewGRPCPlugin( + return hop_plugin.NewGRPCPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TokenOption(cfg.Plugin.Token), plugin.TLSConfigOption(tlsCfg), diff --git a/config/parsing/hosts/parse.go b/config/parsing/hosts/parse.go index 7bcac0b..1ca517c 100644 --- a/config/parsing/hosts/parse.go +++ b/config/parsing/hosts/parse.go @@ -9,6 +9,7 @@ import ( "github.com/go-gost/core/logger" "github.com/go-gost/x/config" xhosts "github.com/go-gost/x/hosts" + hosts_plugin "github.com/go-gost/x/hosts/plugin" "github.com/go-gost/x/internal/loader" "github.com/go-gost/x/internal/plugin" ) @@ -28,13 +29,13 @@ func ParseHostMapper(cfg *config.HostsConfig) hosts.HostMapper { } switch strings.ToLower(cfg.Plugin.Type) { case "http": - return xhosts.NewHTTPPlugin( + return hosts_plugin.NewHTTPPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TLSConfigOption(tlsCfg), plugin.TimeoutOption(cfg.Plugin.Timeout), ) default: - return xhosts.NewGRPCPlugin( + return hosts_plugin.NewGRPCPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TokenOption(cfg.Plugin.Token), plugin.TLSConfigOption(tlsCfg), diff --git a/config/parsing/ingress/parse.go b/config/parsing/ingress/parse.go index f7dafc3..94e37f9 100644 --- a/config/parsing/ingress/parse.go +++ b/config/parsing/ingress/parse.go @@ -8,6 +8,7 @@ import ( "github.com/go-gost/core/logger" "github.com/go-gost/x/config" xingress "github.com/go-gost/x/ingress" + ingress_plugin "github.com/go-gost/x/ingress/plugin" "github.com/go-gost/x/internal/loader" "github.com/go-gost/x/internal/plugin" ) @@ -27,13 +28,13 @@ func ParseIngress(cfg *config.IngressConfig) ingress.Ingress { } switch strings.ToLower(cfg.Plugin.Type) { case "http": - return xingress.NewHTTPPlugin( + return ingress_plugin.NewHTTPPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TLSConfigOption(tlsCfg), plugin.TimeoutOption(cfg.Plugin.Timeout), ) default: - return xingress.NewGRPCPlugin( + return ingress_plugin.NewGRPCPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TokenOption(cfg.Plugin.Token), plugin.TLSConfigOption(tlsCfg), diff --git a/config/parsing/limiter/parse.go b/config/parsing/limiter/parse.go index 6004290..83068d3 100644 --- a/config/parsing/limiter/parse.go +++ b/config/parsing/limiter/parse.go @@ -14,6 +14,7 @@ import ( xconn "github.com/go-gost/x/limiter/conn" xrate "github.com/go-gost/x/limiter/rate" xtraffic "github.com/go-gost/x/limiter/traffic" + traffic_plugin "github.com/go-gost/x/limiter/traffic/plugin" ) func ParseTrafficLimiter(cfg *config.LimiterConfig) (lim traffic.TrafficLimiter) { @@ -31,13 +32,13 @@ func ParseTrafficLimiter(cfg *config.LimiterConfig) (lim traffic.TrafficLimiter) } switch strings.ToLower(cfg.Plugin.Type) { case "http": - return xtraffic.NewHTTPPlugin( + return traffic_plugin.NewHTTPPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TLSConfigOption(tlsCfg), plugin.TimeoutOption(cfg.Plugin.Timeout), ) default: - return xtraffic.NewGRPCPlugin( + return traffic_plugin.NewGRPCPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TokenOption(cfg.Plugin.Token), plugin.TLSConfigOption(tlsCfg), diff --git a/config/parsing/observer/parse.go b/config/parsing/observer/parse.go new file mode 100644 index 0000000..1666157 --- /dev/null +++ b/config/parsing/observer/parse.go @@ -0,0 +1,39 @@ +package observer + +import ( + "crypto/tls" + "strings" + + "github.com/go-gost/core/observer" + "github.com/go-gost/x/config" + "github.com/go-gost/x/internal/plugin" + observer_plugin "github.com/go-gost/x/observer/plugin" +) + +func ParseObserver(cfg *config.ObserverConfig) observer.Observer { + if cfg == nil || cfg.Plugin == nil { + return nil + } + + var tlsCfg *tls.Config + if cfg.Plugin.TLS != nil { + tlsCfg = &tls.Config{ + ServerName: cfg.Plugin.TLS.ServerName, + InsecureSkipVerify: !cfg.Plugin.TLS.Secure, + } + } + switch strings.ToLower(cfg.Plugin.Type) { + case "http": + return observer_plugin.NewHTTPPlugin( + cfg.Name, cfg.Plugin.Addr, + plugin.TLSConfigOption(tlsCfg), + plugin.TimeoutOption(cfg.Plugin.Timeout), + ) + default: + return observer_plugin.NewGRPCPlugin( + cfg.Name, cfg.Plugin.Addr, + plugin.TokenOption(cfg.Plugin.Token), + plugin.TLSConfigOption(tlsCfg), + ) + } +} diff --git a/config/parsing/parse.go b/config/parsing/parse.go index 624426e..864da50 100644 --- a/config/parsing/parse.go +++ b/config/parsing/parse.go @@ -10,6 +10,7 @@ const ( MDKeyPostUp = "postUp" MDKeyPostDown = "postDown" MDKeyIgnoreChain = "ignoreChain" + MDKeyEnableStats = "enableStats" MDKeyRecorderDirection = "direction" MDKeyRecorderTimestampFormat = "timeStampFormat" diff --git a/config/parsing/recorder/parse.go b/config/parsing/recorder/parse.go index de6d049..60d675f 100644 --- a/config/parsing/recorder/parse.go +++ b/config/parsing/recorder/parse.go @@ -8,6 +8,7 @@ import ( "github.com/go-gost/x/config" "github.com/go-gost/x/internal/plugin" xrecorder "github.com/go-gost/x/recorder" + recorder_plugin "github.com/go-gost/x/recorder/plugin" ) func ParseRecorder(cfg *config.RecorderConfig) (r recorder.Recorder) { @@ -25,13 +26,13 @@ func ParseRecorder(cfg *config.RecorderConfig) (r recorder.Recorder) { } switch strings.ToLower(cfg.Plugin.Type) { case "http": - return xrecorder.NewHTTPPlugin( + return recorder_plugin.NewHTTPPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TLSConfigOption(tlsCfg), plugin.TimeoutOption(cfg.Plugin.Timeout), ) default: - return xrecorder.NewGRPCPlugin( + return recorder_plugin.NewGRPCPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TokenOption(cfg.Plugin.Token), plugin.TLSConfigOption(tlsCfg), diff --git a/config/parsing/resolver/parse.go b/config/parsing/resolver/parse.go index 1dd3a1c..0d759d0 100644 --- a/config/parsing/resolver/parse.go +++ b/config/parsing/resolver/parse.go @@ -11,6 +11,7 @@ import ( "github.com/go-gost/x/internal/plugin" "github.com/go-gost/x/registry" xresolver "github.com/go-gost/x/resolver" + resolver_plugin "github.com/go-gost/x/resolver/plugin" ) func ParseResolver(cfg *config.ResolverConfig) (resolver.Resolver, error) { @@ -28,13 +29,13 @@ func ParseResolver(cfg *config.ResolverConfig) (resolver.Resolver, error) { } switch strings.ToLower(cfg.Plugin.Type) { case "http": - return xresolver.NewHTTPPlugin( + return resolver_plugin.NewHTTPPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TLSConfigOption(tlsCfg), plugin.TimeoutOption(cfg.Plugin.Timeout), ), nil default: - return xresolver.NewGRPCPlugin( + return resolver_plugin.NewGRPCPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TokenOption(cfg.Plugin.Token), plugin.TLSConfigOption(tlsCfg), diff --git a/config/parsing/router/parse.go b/config/parsing/router/parse.go index f3a563d..b23677f 100644 --- a/config/parsing/router/parse.go +++ b/config/parsing/router/parse.go @@ -11,6 +11,7 @@ import ( "github.com/go-gost/x/internal/loader" "github.com/go-gost/x/internal/plugin" xrouter "github.com/go-gost/x/router" + router_plugin "github.com/go-gost/x/router/plugin" ) func ParseRouter(cfg *config.RouterConfig) router.Router { @@ -28,13 +29,13 @@ func ParseRouter(cfg *config.RouterConfig) router.Router { } switch strings.ToLower(cfg.Plugin.Type) { case "http": - return xrouter.NewHTTPPlugin( + return router_plugin.NewHTTPPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TLSConfigOption(tlsCfg), plugin.TimeoutOption(cfg.Plugin.Timeout), ) default: - return xrouter.NewGRPCPlugin( + return router_plugin.NewGRPCPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TokenOption(cfg.Plugin.Token), plugin.TLSConfigOption(tlsCfg), diff --git a/config/parsing/sd/parse.go b/config/parsing/sd/parse.go index dba94f2..c46c33a 100644 --- a/config/parsing/sd/parse.go +++ b/config/parsing/sd/parse.go @@ -7,7 +7,7 @@ import ( "github.com/go-gost/core/sd" "github.com/go-gost/x/config" "github.com/go-gost/x/internal/plugin" - xsd "github.com/go-gost/x/sd" + sd_plugin "github.com/go-gost/x/sd/plugin" ) func ParseSD(cfg *config.SDConfig) sd.SD { @@ -24,13 +24,13 @@ func ParseSD(cfg *config.SDConfig) sd.SD { } switch strings.ToLower(cfg.Plugin.Type) { case "http": - return xsd.NewHTTPPlugin( + return sd_plugin.NewHTTPPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TLSConfigOption(tlsCfg), plugin.TimeoutOption(cfg.Plugin.Timeout), ) default: - return xsd.NewGRPCPlugin( + return sd_plugin.NewGRPCPlugin( cfg.Name, cfg.Plugin.Addr, plugin.TokenOption(cfg.Plugin.Token), plugin.TLSConfigOption(tlsCfg), diff --git a/config/parsing/service/parse.go b/config/parsing/service/parse.go index 24bf72b..09cf5eb 100644 --- a/config/parsing/service/parse.go +++ b/config/parsing/service/parse.go @@ -29,6 +29,7 @@ import ( "github.com/go-gost/x/metadata" "github.com/go-gost/x/registry" xservice "github.com/go-gost/x/service" + "github.com/go-gost/x/stats" ) func ParseService(cfg *config.ServiceConfig) (service.Service, error) { @@ -96,6 +97,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) { ifce := cfg.Interface var preUp, preDown, postUp, postDown []string var ignoreChain bool + var pStats *stats.Stats if cfg.Metadata != nil { md := metadata.NewMetadata(cfg.Metadata) ppv = mdutil.GetInt(md, parsing.MDKeyProxyProtocol) @@ -112,6 +114,10 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) { postUp = mdutil.GetStrings(md, parsing.MDKeyPostUp) postDown = mdutil.GetStrings(md, parsing.MDKeyPostDown) ignoreChain = mdutil.GetBool(md, parsing.MDKeyIgnoreChain) + + if mdutil.GetBool(md, parsing.MDKeyEnableStats) { + pStats = &stats.Stats{} + } } listenOpts := []listener.Option{ @@ -125,6 +131,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) { listener.LoggerOption(listenerLogger), listener.ServiceOption(cfg.Name), listener.ProxyProtocolOption(ppv), + listener.StatsOption(pStats), } if !ignoreChain { listenOpts = append(listenOpts, @@ -218,6 +225,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) { handler.TLSConfigOption(tlsConfig), handler.RateLimiterOption(registry.RateLimiterRegistry().Get(cfg.RLimiter)), handler.TrafficLimiterOption(registry.TrafficLimiterRegistry().Get(cfg.Handler.Limiter)), + handler.ObserverOption(registry.ObserverRegistry().Get(cfg.Handler.Observer)), handler.LoggerOption(handlerLogger), handler.ServiceOption(cfg.Name), ) @@ -249,6 +257,8 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) { xservice.PostUpOption(postUp), xservice.PostDownOption(postDown), xservice.RecordersOption(recorders...), + xservice.StatsOption(pStats), + xservice.ObserverOption(registry.ObserverRegistry().Get(cfg.Observer)), xservice.LoggerOption(serviceLogger), ) diff --git a/go.mod b/go.mod index a04b565..06a5b3b 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,10 @@ require ( github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d github.com/gin-contrib/cors v1.5.0 github.com/gin-gonic/gin v1.9.1 - github.com/go-gost/core v0.0.0-20231219132306-6b5c04b5e446 + github.com/go-gost/core v0.0.0-20240103125300-5a427b4eaf99 github.com/go-gost/gosocks4 v0.0.1 github.com/go-gost/gosocks5 v0.4.0 - github.com/go-gost/plugin v0.0.0-20231119084331-d49a1cb23b3b + github.com/go-gost/plugin v0.0.0-20240103125338-9c84e29cb81a github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7 github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451 github.com/go-redis/redis/v8 v8.11.5 diff --git a/go.sum b/go.sum index 01ee684..919025f 100644 --- a/go.sum +++ b/go.sum @@ -49,14 +49,14 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-gost/core v0.0.0-20231219132306-6b5c04b5e446 h1:H7VyfQOOAH7smDQ41O/mMClFv8MyflVk5AO9uIp7qXg= -github.com/go-gost/core v0.0.0-20231219132306-6b5c04b5e446/go.mod h1:ndkgWVYRLwupVaFFWv8ML1Nr8tD3xhHK245PLpUDg4E= +github.com/go-gost/core v0.0.0-20240103125300-5a427b4eaf99 h1:/0hmilnQBEDlOaRcO+TlwaHH8a5ig6nc2aAsU4FGZcw= +github.com/go-gost/core v0.0.0-20240103125300-5a427b4eaf99/go.mod h1:ndkgWVYRLwupVaFFWv8ML1Nr8tD3xhHK245PLpUDg4E= github.com/go-gost/gosocks4 v0.0.1 h1:+k1sec8HlELuQV7rWftIkmy8UijzUt2I6t+iMPlGB2s= github.com/go-gost/gosocks4 v0.0.1/go.mod h1:3B6L47HbU/qugDg4JnoFPHgJXE43Inz8Bah1QaN9qCc= github.com/go-gost/gosocks5 v0.4.0 h1:EIrOEkpJez4gwHrMa33frA+hHXJyevjp47thpMQsJzI= github.com/go-gost/gosocks5 v0.4.0/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4= -github.com/go-gost/plugin v0.0.0-20231119084331-d49a1cb23b3b h1:ZmnYutflq+KOZK+Px5RDckorDSxTYlkT4aQbjTC8/C4= -github.com/go-gost/plugin v0.0.0-20231119084331-d49a1cb23b3b/go.mod h1:qXr2Zm9Ex2ATqnWuNUzVZqySPMnuIihvblYZt4MlZLw= +github.com/go-gost/plugin v0.0.0-20240103125338-9c84e29cb81a h1:ME7P1Brcg4C640DSPqlvQr7JuvvQfJ8QpmS3yCFlK3A= +github.com/go-gost/plugin v0.0.0-20240103125338-9c84e29cb81a/go.mod h1:qXr2Zm9Ex2ATqnWuNUzVZqySPMnuIihvblYZt4MlZLw= github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7 h1:qAG1OyjvdA5h221CfFSS3J359V3d2E7dJWyP29QoDSI= github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7/go.mod h1:lcX+23LCQ3khIeASBo+tJ/WbwXFO32/N5YN6ucuYTG8= github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451 h1:xj8gUZGYO3nb5+6Bjw9+tsFkA9sYynrOvDvvC4uDV2I= diff --git a/handler/http/handler.go b/handler/http/handler.go index e6534eb..4ddb4f9 100644 --- a/handler/http/handler.go +++ b/handler/http/handler.go @@ -19,13 +19,16 @@ import ( "github.com/asaskevich/govalidator" "github.com/go-gost/core/chain" "github.com/go-gost/core/handler" - "github.com/go-gost/core/limiter/traffic" + traffic "github.com/go-gost/core/limiter/traffic" "github.com/go-gost/core/logger" md "github.com/go-gost/core/metadata" ctxvalue "github.com/go-gost/x/internal/ctx" netpkg "github.com/go-gost/x/internal/net" - "github.com/go-gost/x/limiter/traffic/wrapper" + stats_util "github.com/go-gost/x/internal/util/stats" + traffic_wrapper "github.com/go-gost/x/limiter/traffic/wrapper" "github.com/go-gost/x/registry" + "github.com/go-gost/x/stats" + stats_wrapper "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -36,6 +39,8 @@ type httpHandler struct { router *chain.Router md metadata options handler.Options + stats *stats_util.HandlerStats + cancel context.CancelFunc } func NewHandler(opts ...handler.Option) handler.Handler { @@ -46,6 +51,7 @@ func NewHandler(opts ...handler.Option) handler.Handler { return &httpHandler{ options: options, + stats: stats_util.NewHandlerStats(options.Service), } } @@ -59,6 +65,13 @@ func (h *httpHandler) Init(md md.Metadata) error { h.router = chain.NewRouter(chain.LoggerRouterOption(h.options.Logger)) } + ctx, cancel := context.WithCancel(context.Background()) + h.cancel = cancel + + if h.options.Observer != nil { + go h.observeStats(ctx) + } + return nil } @@ -93,6 +106,13 @@ func (h *httpHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler return h.handleRequest(ctx, conn, req, log) } +func (h *httpHandler) Close() error { + if h.cancel != nil { + h.cancel() + } + return nil +} + func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *http.Request, log logger.Logger) error { if !req.URL.IsAbs() && govalidator.IsDNSName(req.Host) { req.URL.Scheme = "http" @@ -221,12 +241,19 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt } } - rw := wrapper.WrapReadWriter(h.options.Limiter, conn, conn.RemoteAddr().String(), + rw := traffic_wrapper.WrapReadWriter(h.options.Limiter, conn, traffic.NetworkOption(network), traffic.AddrOption(addr), traffic.ClientOption(clientID), traffic.SrcOption(conn.RemoteAddr().String()), ) + if h.options.Observer != nil { + pstats := h.stats.Stats(clientID) + pstats.Add(stats.KindTotalConns, 1) + pstats.Add(stats.KindCurrentConns, 1) + defer pstats.Add(stats.KindCurrentConns, -1) + rw = stats_wrapper.WrapReadWriter(rw, pstats) + } start := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), addr) @@ -378,3 +405,21 @@ func (h *httpHandler) checkRateLimit(addr net.Addr) bool { return true } + +func (h *httpHandler) observeStats(ctx context.Context) { + if h.options.Observer == nil { + return + } + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + h.options.Observer.Observe(ctx, h.stats.Events()) + case <-ctx.Done(): + return + } + } +} diff --git a/handler/http2/handler.go b/handler/http2/handler.go index 8dce6aa..0af0776 100644 --- a/handler/http2/handler.go +++ b/handler/http2/handler.go @@ -26,8 +26,11 @@ import ( ctxvalue "github.com/go-gost/x/internal/ctx" xio "github.com/go-gost/x/internal/io" netpkg "github.com/go-gost/x/internal/net" + stats_util "github.com/go-gost/x/internal/util/stats" "github.com/go-gost/x/limiter/traffic/wrapper" "github.com/go-gost/x/registry" + "github.com/go-gost/x/stats" + stats_wrapper "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -38,6 +41,8 @@ type http2Handler struct { router *chain.Router md metadata options handler.Options + stats *stats_util.HandlerStats + cancel context.CancelFunc } func NewHandler(opts ...handler.Option) handler.Handler { @@ -48,6 +53,7 @@ func NewHandler(opts ...handler.Option) handler.Handler { return &http2Handler{ options: options, + stats: stats_util.NewHandlerStats(options.Service), } } @@ -61,6 +67,12 @@ func (h *http2Handler) Init(md md.Metadata) error { h.router = chain.NewRouter(chain.LoggerRouterOption(h.options.Logger)) } + ctx, cancel := context.WithCancel(context.Background()) + h.cancel = cancel + + if h.options.Observer != nil { + go h.observeStats(ctx) + } return nil } @@ -98,6 +110,13 @@ func (h *http2Handler) Handle(ctx context.Context, conn net.Conn, opts ...handle ) } +func (h *http2Handler) Close() error { + if h.cancel != nil { + h.cancel() + } + return nil +} + // 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. @@ -204,12 +223,20 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req return nil } - rw := wrapper.WrapReadWriter(h.options.Limiter, xio.NewReadWriter(req.Body, flushWriter{w}), req.RemoteAddr, + rw := wrapper.WrapReadWriter(h.options.Limiter, xio.NewReadWriter(req.Body, flushWriter{w}), traffic.NetworkOption("tcp"), traffic.AddrOption(addr), traffic.ClientOption(clientID), traffic.SrcOption(req.RemoteAddr), ) + if h.options.Observer != nil { + pstats := h.stats.Stats(clientID) + pstats.Add(stats.KindTotalConns, 1) + pstats.Add(stats.KindCurrentConns, 1) + defer pstats.Add(stats.KindCurrentConns, -1) + rw = stats_wrapper.WrapReadWriter(rw, pstats) + } + start := time.Now() log.Infof("%s <-> %s", req.RemoteAddr, addr) netpkg.Transport(rw, cc) @@ -386,3 +413,21 @@ func (h *http2Handler) checkRateLimit(addr net.Addr) bool { return true } + +func (h *http2Handler) observeStats(ctx context.Context) { + if h.options.Observer == nil { + return + } + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + h.options.Observer.Observe(ctx, h.stats.Events()) + case <-ctx.Done(): + return + } + } +} diff --git a/handler/relay/connect.go b/handler/relay/connect.go index 6af1c6b..aa7cc4a 100644 --- a/handler/relay/connect.go +++ b/handler/relay/connect.go @@ -15,6 +15,8 @@ import ( xnet "github.com/go-gost/x/internal/net" serial "github.com/go-gost/x/internal/util/serial" "github.com/go-gost/x/limiter/traffic/wrapper" + "github.com/go-gost/x/stats" + stats_wrapper "github.com/go-gost/x/stats/wrapper" ) func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) (err error) { @@ -105,12 +107,20 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network } } - rw := wrapper.WrapReadWriter(h.options.Limiter, conn, conn.RemoteAddr().String(), + clientID := ctxvalue.ClientIDFromContext(ctx) + rw := wrapper.WrapReadWriter(h.options.Limiter, conn, traffic.NetworkOption(network), traffic.AddrOption(address), - traffic.ClientOption(string(ctxvalue.ClientIDFromContext(ctx))), + traffic.ClientOption(string(clientID)), traffic.SrcOption(conn.RemoteAddr().String()), ) + if h.options.Observer != nil { + pstats := h.stats.Stats(string(clientID)) + pstats.Add(stats.KindTotalConns, 1) + pstats.Add(stats.KindCurrentConns, 1) + defer pstats.Add(stats.KindCurrentConns, -1) + rw = stats_wrapper.WrapReadWriter(rw, pstats) + } t := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), address) diff --git a/handler/relay/forward.go b/handler/relay/forward.go index 04a9d33..8a86cf8 100644 --- a/handler/relay/forward.go +++ b/handler/relay/forward.go @@ -13,6 +13,8 @@ import ( ctxvalue "github.com/go-gost/x/internal/ctx" netpkg "github.com/go-gost/x/internal/net" "github.com/go-gost/x/limiter/traffic/wrapper" + "github.com/go-gost/x/stats" + stats_wrapper "github.com/go-gost/x/stats/wrapper" ) func (h *relayHandler) handleForward(ctx context.Context, conn net.Conn, network string, log logger.Logger) error { @@ -87,12 +89,20 @@ func (h *relayHandler) handleForward(ctx context.Context, conn net.Conn, network conn = rc } - rw := wrapper.WrapReadWriter(h.options.Limiter, conn, conn.RemoteAddr().String(), + clientID := ctxvalue.ClientIDFromContext(ctx) + rw := wrapper.WrapReadWriter(h.options.Limiter, conn, traffic.NetworkOption(network), traffic.AddrOption(target.Addr), - traffic.ClientOption(string(ctxvalue.ClientIDFromContext(ctx))), + traffic.ClientOption(string(clientID)), traffic.SrcOption(conn.RemoteAddr().String()), ) + if h.options.Observer != nil { + pstats := h.stats.Stats(string(clientID)) + pstats.Add(stats.KindTotalConns, 1) + pstats.Add(stats.KindCurrentConns, 1) + defer pstats.Add(stats.KindCurrentConns, -1) + rw = stats_wrapper.WrapReadWriter(rw, pstats) + } t := time.Now() log.Debugf("%s <-> %s", conn.RemoteAddr(), target.Addr) diff --git a/handler/relay/handler.go b/handler/relay/handler.go index 09ab0ab..17bebdc 100644 --- a/handler/relay/handler.go +++ b/handler/relay/handler.go @@ -11,10 +11,10 @@ import ( "github.com/go-gost/core/handler" "github.com/go-gost/core/hop" md "github.com/go-gost/core/metadata" - "github.com/go-gost/core/service" "github.com/go-gost/relay" ctxvalue "github.com/go-gost/x/internal/ctx" "github.com/go-gost/x/registry" + stats_util "github.com/go-gost/x/internal/util/stats" ) var ( @@ -33,7 +33,8 @@ type relayHandler struct { router *chain.Router md metadata options handler.Options - ep service.Service + stats *stats_util.HandlerStats + cancel context.CancelFunc } func NewHandler(opts ...handler.Option) handler.Handler { @@ -44,6 +45,7 @@ func NewHandler(opts ...handler.Option) handler.Handler { return &relayHandler{ options: options, + stats: stats_util.NewHandlerStats(options.Service), } } @@ -57,6 +59,13 @@ func (h *relayHandler) Init(md md.Metadata) (err error) { h.router = chain.NewRouter(chain.LoggerRouterOption(h.options.Logger)) } + ctx, cancel := context.WithCancel(context.Background()) + h.cancel = cancel + + if h.options.Observer != nil { + go h.observeStats(ctx) + } + return nil } @@ -172,8 +181,8 @@ func (h *relayHandler) Handle(ctx context.Context, conn net.Conn, opts ...handle // Close implements io.Closer interface. func (h *relayHandler) Close() error { - if h.ep != nil { - return h.ep.Close() + if h.cancel != nil { + h.cancel() } return nil } @@ -189,3 +198,21 @@ func (h *relayHandler) checkRateLimit(addr net.Addr) bool { return true } + +func (h *relayHandler) observeStats(ctx context.Context) { + if h.options.Observer == nil { + return + } + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + h.options.Observer.Observe(ctx, h.stats.Events()) + case <-ctx.Done(): + return + } + } +} \ No newline at end of file diff --git a/handler/socks/v4/handler.go b/handler/socks/v4/handler.go index db04955..f56d516 100644 --- a/handler/socks/v4/handler.go +++ b/handler/socks/v4/handler.go @@ -16,6 +16,9 @@ import ( netpkg "github.com/go-gost/x/internal/net" "github.com/go-gost/x/limiter/traffic/wrapper" "github.com/go-gost/x/registry" + stats_util "github.com/go-gost/x/internal/util/stats" + "github.com/go-gost/x/stats" + stats_wrapper "github.com/go-gost/x/stats/wrapper" ) var ( @@ -32,6 +35,8 @@ type socks4Handler struct { router *chain.Router md metadata options handler.Options + stats *stats_util.HandlerStats + cancel context.CancelFunc } func NewHandler(opts ...handler.Option) handler.Handler { @@ -42,6 +47,7 @@ func NewHandler(opts ...handler.Option) handler.Handler { return &socks4Handler{ options: options, + stats: stats_util.NewHandlerStats(options.Service), } } @@ -55,6 +61,13 @@ func (h *socks4Handler) Init(md md.Metadata) (err error) { h.router = chain.NewRouter(chain.LoggerRouterOption(h.options.Logger)) } + ctx, cancel := context.WithCancel(context.Background()) + h.cancel = cancel + + if h.options.Observer != nil { + go h.observeStats(ctx) + } + return nil } @@ -114,6 +127,13 @@ func (h *socks4Handler) Handle(ctx context.Context, conn net.Conn, opts ...handl } } +func (h *socks4Handler) Close() error { + if h.cancel != nil { + h.cancel() + } + return nil +} + func (h *socks4Handler) handleConnect(ctx context.Context, conn net.Conn, req *gosocks4.Request, log logger.Logger) error { addr := req.Addr.String() @@ -151,12 +171,20 @@ func (h *socks4Handler) handleConnect(ctx context.Context, conn net.Conn, req *g return err } - rw := wrapper.WrapReadWriter(h.options.Limiter, conn, conn.RemoteAddr().String(), + clientID := ctxvalue.ClientIDFromContext(ctx) + rw := wrapper.WrapReadWriter(h.options.Limiter, conn, traffic.NetworkOption("tcp"), traffic.AddrOption(addr), - traffic.ClientOption(string(ctxvalue.ClientIDFromContext(ctx))), + traffic.ClientOption(string(clientID)), traffic.SrcOption(conn.RemoteAddr().String()), ) + if h.options.Observer != nil { + pstats := h.stats.Stats(string(clientID)) + pstats.Add(stats.KindTotalConns, 1) + pstats.Add(stats.KindCurrentConns, 1) + defer pstats.Add(stats.KindCurrentConns, -1) + rw = stats_wrapper.WrapReadWriter(rw, pstats) + } t := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), addr) @@ -184,3 +212,21 @@ func (h *socks4Handler) checkRateLimit(addr net.Addr) bool { return true } + +func (h *socks4Handler) observeStats(ctx context.Context) { + if h.options.Observer == nil { + return + } + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + h.options.Observer.Observe(ctx, h.stats.Events()) + case <-ctx.Done(): + return + } + } +} diff --git a/handler/socks/v5/connect.go b/handler/socks/v5/connect.go index dcccbfd..faacbc0 100644 --- a/handler/socks/v5/connect.go +++ b/handler/socks/v5/connect.go @@ -12,6 +12,8 @@ import ( ctxvalue "github.com/go-gost/x/internal/ctx" netpkg "github.com/go-gost/x/internal/net" "github.com/go-gost/x/limiter/traffic/wrapper" + "github.com/go-gost/x/stats" + stats_wrapper "github.com/go-gost/x/stats/wrapper" ) func (h *socks5Handler) handleConnect(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) error { @@ -50,12 +52,20 @@ func (h *socks5Handler) handleConnect(ctx context.Context, conn net.Conn, networ return err } - rw := wrapper.WrapReadWriter(h.options.Limiter, conn, conn.RemoteAddr().String(), + clientID := ctxvalue.ClientIDFromContext(ctx) + rw := wrapper.WrapReadWriter(h.options.Limiter, conn, traffic.NetworkOption(network), traffic.AddrOption(address), - traffic.ClientOption(string(ctxvalue.ClientIDFromContext(ctx))), + traffic.ClientOption(string(clientID)), traffic.SrcOption(conn.RemoteAddr().String()), ) + if h.options.Observer != nil { + pstats := h.stats.Stats(string(clientID)) + pstats.Add(stats.KindTotalConns, 1) + pstats.Add(stats.KindCurrentConns, 1) + defer pstats.Add(stats.KindCurrentConns, -1) + rw = stats_wrapper.WrapReadWriter(rw, pstats) + } t := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), address) diff --git a/handler/socks/v5/handler.go b/handler/socks/v5/handler.go index 2432cce..b5a5d12 100644 --- a/handler/socks/v5/handler.go +++ b/handler/socks/v5/handler.go @@ -12,6 +12,7 @@ import ( "github.com/go-gost/gosocks5" ctxvalue "github.com/go-gost/x/internal/ctx" "github.com/go-gost/x/internal/util/socks" + stats_util "github.com/go-gost/x/internal/util/stats" "github.com/go-gost/x/registry" ) @@ -29,6 +30,8 @@ type socks5Handler struct { router *chain.Router md metadata options handler.Options + stats *stats_util.HandlerStats + cancel context.CancelFunc } func NewHandler(opts ...handler.Option) handler.Handler { @@ -39,6 +42,7 @@ func NewHandler(opts ...handler.Option) handler.Handler { return &socks5Handler{ options: options, + stats: stats_util.NewHandlerStats(options.Service), } } @@ -59,6 +63,13 @@ func (h *socks5Handler) Init(md md.Metadata) (err error) { noTLS: h.md.noTLS, } + ctx, cancel := context.WithCancel(context.Background()) + h.cancel = cancel + + if h.options.Observer != nil { + go h.observeStats(ctx) + } + return } @@ -125,6 +136,13 @@ func (h *socks5Handler) Handle(ctx context.Context, conn net.Conn, opts ...handl } } +func (h *socks5Handler) Close() error { + if h.cancel != nil { + h.cancel() + } + return nil +} + func (h *socks5Handler) checkRateLimit(addr net.Addr) bool { if h.options.RateLimiter == nil { return true @@ -136,3 +154,21 @@ func (h *socks5Handler) checkRateLimit(addr net.Addr) bool { return true } + +func (h *socks5Handler) observeStats(ctx context.Context) { + if h.options.Observer == nil { + return + } + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + h.options.Observer.Observe(ctx, h.stats.Events()) + case <-ctx.Done(): + return + } + } +} diff --git a/handler/socks/v5/udp.go b/handler/socks/v5/udp.go index d7a34cb..36e8ac1 100644 --- a/handler/socks/v5/udp.go +++ b/handler/socks/v5/udp.go @@ -10,8 +10,11 @@ import ( "github.com/go-gost/core/logger" "github.com/go-gost/gosocks5" + ctxvalue "github.com/go-gost/x/internal/ctx" "github.com/go-gost/x/internal/net/udp" "github.com/go-gost/x/internal/util/socks" + "github.com/go-gost/x/stats" + stats_wrapper "github.com/go-gost/x/stats/wrapper" ) func (h *socks5Handler) handleUDP(ctx context.Context, conn net.Conn, log logger.Logger) error { @@ -67,7 +70,17 @@ func (h *socks5Handler) handleUDP(ctx context.Context, conn net.Conn, log logger return err } - r := udp.NewRelay(socks.UDPConn(cc, h.md.udpBufferSize), pc). + var lc net.PacketConn = cc + clientID := ctxvalue.ClientIDFromContext(ctx) + if h.options.Observer != nil { + pstats := h.stats.Stats(string(clientID)) + pstats.Add(stats.KindTotalConns, 1) + pstats.Add(stats.KindCurrentConns, 1) + defer pstats.Add(stats.KindCurrentConns, -1) + lc = stats_wrapper.WrapPacketConn(lc, pstats) + } + + r := udp.NewRelay(socks.UDPConn(lc, h.md.udpBufferSize), pc). WithBypass(h.options.Bypass). WithLogger(log) r.SetBufferSize(h.md.udpBufferSize) diff --git a/handler/socks/v5/udp_tun.go b/handler/socks/v5/udp_tun.go index 6b65b83..de49b54 100644 --- a/handler/socks/v5/udp_tun.go +++ b/handler/socks/v5/udp_tun.go @@ -7,8 +7,11 @@ import ( "github.com/go-gost/core/logger" "github.com/go-gost/gosocks5" + ctxvalue "github.com/go-gost/x/internal/ctx" "github.com/go-gost/x/internal/net/udp" "github.com/go-gost/x/internal/util/socks" + "github.com/go-gost/x/stats" + stats_wrapper "github.com/go-gost/x/stats/wrapper" ) func (h *socks5Handler) handleUDPTun(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) error { @@ -56,6 +59,15 @@ func (h *socks5Handler) handleUDPTun(ctx context.Context, conn net.Conn, network } log.Debugf("bind on %s OK", pc.LocalAddr()) + clientID := ctxvalue.ClientIDFromContext(ctx) + if h.options.Observer != nil { + pstats := h.stats.Stats(string(clientID)) + pstats.Add(stats.KindTotalConns, 1) + pstats.Add(stats.KindCurrentConns, 1) + defer pstats.Add(stats.KindCurrentConns, -1) + conn = stats_wrapper.WrapConn(conn, pstats) + } + r := udp.NewRelay(socks.UDPTunServerConn(conn), pc). WithBypass(h.options.Bypass). WithLogger(log) diff --git a/handler/tunnel/connect.go b/handler/tunnel/connect.go index 51c329b..c8689a6 100644 --- a/handler/tunnel/connect.go +++ b/handler/tunnel/connect.go @@ -100,7 +100,7 @@ func (h *tunnelHandler) handleConnect(ctx context.Context, req *relay.Request, c req.WriteTo(cc) } - rw := wrapper.WrapReadWriter(h.options.Limiter, conn, tunnelID.String(), + rw := wrapper.WrapReadWriter(h.options.Limiter, conn, traffic.NetworkOption(network), traffic.AddrOption(dstAddr), traffic.ClientOption(string(ctxvalue.ClientIDFromContext(ctx))), diff --git a/hop/plugin/grpc.go b/hop/plugin/grpc.go new file mode 100644 index 0000000..c924800 --- /dev/null +++ b/hop/plugin/grpc.go @@ -0,0 +1,101 @@ +package hop + +import ( + "bytes" + "context" + "encoding/json" + "io" + + "github.com/go-gost/core/chain" + "github.com/go-gost/core/hop" + "github.com/go-gost/core/logger" + "github.com/go-gost/plugin/hop/proto" + "github.com/go-gost/x/config" + node_parser "github.com/go-gost/x/config/parsing/node" + ctxvalue "github.com/go-gost/x/internal/ctx" + "github.com/go-gost/x/internal/plugin" + "google.golang.org/grpc" +) + +type grpcPlugin struct { + name string + conn grpc.ClientConnInterface + client proto.HopClient + log logger.Logger +} + +// NewGRPCPlugin creates a Hop plugin based on gRPC. +func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) hop.Hop { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "hop", + "hop": name, + }) + conn, err := plugin.NewGRPCConn(addr, &options) + if err != nil { + log.Error(err) + } + + p := &grpcPlugin{ + name: name, + conn: conn, + log: log, + } + if conn != nil { + p.client = proto.NewHopClient(conn) + } + return p +} + +func (p *grpcPlugin) Select(ctx context.Context, opts ...hop.SelectOption) *chain.Node { + if p.client == nil { + return nil + } + + var options hop.SelectOptions + for _, opt := range opts { + opt(&options) + } + + r, err := p.client.Select(ctx, + &proto.SelectRequest{ + Network: options.Network, + Addr: options.Addr, + Host: options.Host, + Path: options.Path, + Client: string(ctxvalue.ClientIDFromContext(ctx)), + Src: string(ctxvalue.ClientAddrFromContext(ctx)), + }) + if err != nil { + p.log.Error(err) + return nil + } + + if r.Node == nil { + return nil + } + + var cfg config.NodeConfig + if err := json.NewDecoder(bytes.NewReader(r.Node)).Decode(&cfg); err != nil { + p.log.Error(err) + return nil + } + + node, err := node_parser.ParseNode(p.name, &cfg, logger.Default()) + if err != nil { + p.log.Error(err) + return nil + } + return node +} + +func (p *grpcPlugin) Close() error { + if closer, ok := p.conn.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/hop/plugin.go b/hop/plugin/http.go similarity index 60% rename from hop/plugin.go rename to hop/plugin/http.go index 9df2f48..0025e01 100644 --- a/hop/plugin.go +++ b/hop/plugin/http.go @@ -4,103 +4,17 @@ import ( "bytes" "context" "encoding/json" - "io" "net/http" "github.com/go-gost/core/chain" "github.com/go-gost/core/hop" "github.com/go-gost/core/logger" - "github.com/go-gost/plugin/hop/proto" "github.com/go-gost/x/config" node_parser "github.com/go-gost/x/config/parsing/node" ctxvalue "github.com/go-gost/x/internal/ctx" "github.com/go-gost/x/internal/plugin" - "google.golang.org/grpc" ) -type grpcPlugin struct { - name string - conn grpc.ClientConnInterface - client proto.HopClient - log logger.Logger -} - -// NewGRPCPlugin creates a Hop plugin based on gRPC. -func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) hop.Hop { - var options plugin.Options - for _, opt := range opts { - opt(&options) - } - - log := logger.Default().WithFields(map[string]any{ - "kind": "hop", - "hop": name, - }) - conn, err := plugin.NewGRPCConn(addr, &options) - if err != nil { - log.Error(err) - } - - p := &grpcPlugin{ - name: name, - conn: conn, - log: log, - } - if conn != nil { - p.client = proto.NewHopClient(conn) - } - return p -} - -func (p *grpcPlugin) Select(ctx context.Context, opts ...hop.SelectOption) *chain.Node { - if p.client == nil { - return nil - } - - var options hop.SelectOptions - for _, opt := range opts { - opt(&options) - } - - r, err := p.client.Select(ctx, - &proto.SelectRequest{ - Network: options.Network, - Addr: options.Addr, - Host: options.Host, - Path: options.Path, - Client: string(ctxvalue.ClientIDFromContext(ctx)), - Src: string(ctxvalue.ClientAddrFromContext(ctx)), - }) - if err != nil { - p.log.Error(err) - return nil - } - - if r.Node == nil { - return nil - } - - var cfg config.NodeConfig - if err := json.NewDecoder(bytes.NewReader(r.Node)).Decode(&cfg); err != nil { - p.log.Error(err) - return nil - } - - node, err := node_parser.ParseNode(p.name, &cfg, logger.Default()) - if err != nil { - p.log.Error(err) - return nil - } - return node -} - -func (p *grpcPlugin) Close() error { - if closer, ok := p.conn.(io.Closer); ok { - return closer.Close() - } - return nil -} - type httpPluginRequest struct { Network string `json:"network"` Addr string `json:"addr"` diff --git a/hosts/plugin/grpc.go b/hosts/plugin/grpc.go new file mode 100644 index 0000000..96c1c85 --- /dev/null +++ b/hosts/plugin/grpc.go @@ -0,0 +1,78 @@ +package hosts + +import ( + "context" + "io" + "net" + + "github.com/go-gost/core/hosts" + "github.com/go-gost/core/logger" + "github.com/go-gost/plugin/hosts/proto" + ctxvalue "github.com/go-gost/x/internal/ctx" + "github.com/go-gost/x/internal/plugin" + "google.golang.org/grpc" +) + +type grpcPlugin struct { + conn grpc.ClientConnInterface + client proto.HostMapperClient + log logger.Logger +} + +// NewGRPCPlugin creates a HostMapper plugin based on gRPC. +func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) hosts.HostMapper { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "hosts", + "hosts": name, + }) + conn, err := plugin.NewGRPCConn(addr, &options) + if err != nil { + log.Error(err) + } + p := &grpcPlugin{ + conn: conn, + log: log, + } + if conn != nil { + p.client = proto.NewHostMapperClient(conn) + } + return p +} + +func (p *grpcPlugin) Lookup(ctx context.Context, network, host string, opts ...hosts.Option) (ips []net.IP, ok bool) { + p.log.Debugf("lookup %s/%s", host, network) + + if p.client == nil { + return + } + + r, err := p.client.Lookup(ctx, + &proto.LookupRequest{ + Network: network, + Host: host, + Client: string(ctxvalue.ClientIDFromContext(ctx)), + }) + if err != nil { + p.log.Error(err) + return + } + for _, s := range r.Ips { + if ip := net.ParseIP(s); ip != nil { + ips = append(ips, ip) + } + } + ok = r.Ok + return +} + +func (p *grpcPlugin) Close() error { + if closer, ok := p.conn.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/hosts/plugin.go b/hosts/plugin/http.go similarity index 59% rename from hosts/plugin.go rename to hosts/plugin/http.go index 1ccd6b7..cacbb4a 100644 --- a/hosts/plugin.go +++ b/hosts/plugin/http.go @@ -4,82 +4,15 @@ import ( "bytes" "context" "encoding/json" - "io" "net" "net/http" "github.com/go-gost/core/hosts" "github.com/go-gost/core/logger" - "github.com/go-gost/plugin/hosts/proto" ctxvalue "github.com/go-gost/x/internal/ctx" "github.com/go-gost/x/internal/plugin" - "google.golang.org/grpc" ) -type grpcPlugin struct { - conn grpc.ClientConnInterface - client proto.HostMapperClient - log logger.Logger -} - -// NewGRPCPlugin creates a HostMapper plugin based on gRPC. -func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) hosts.HostMapper { - var options plugin.Options - for _, opt := range opts { - opt(&options) - } - - log := logger.Default().WithFields(map[string]any{ - "kind": "hosts", - "hosts": name, - }) - conn, err := plugin.NewGRPCConn(addr, &options) - if err != nil { - log.Error(err) - } - p := &grpcPlugin{ - conn: conn, - log: log, - } - if conn != nil { - p.client = proto.NewHostMapperClient(conn) - } - return p -} - -func (p *grpcPlugin) Lookup(ctx context.Context, network, host string, opts ...hosts.Option) (ips []net.IP, ok bool) { - p.log.Debugf("lookup %s/%s", host, network) - - if p.client == nil { - return - } - - r, err := p.client.Lookup(ctx, - &proto.LookupRequest{ - Network: network, - Host: host, - Client: string(ctxvalue.ClientIDFromContext(ctx)), - }) - if err != nil { - p.log.Error(err) - return - } - for _, s := range r.Ips { - if ip := net.ParseIP(s); ip != nil { - ips = append(ips, ip) - } - } - ok = r.Ok - return -} - -func (p *grpcPlugin) Close() error { - if closer, ok := p.conn.(io.Closer); ok { - return closer.Close() - } - return nil -} - type httpPluginRequest struct { Network string `json:"network"` Host string `json:"host"` diff --git a/ingress/plugin/grpc.go b/ingress/plugin/grpc.go new file mode 100644 index 0000000..ffb6535 --- /dev/null +++ b/ingress/plugin/grpc.go @@ -0,0 +1,89 @@ +package ingress + +import ( + "context" + "io" + + "github.com/go-gost/core/ingress" + "github.com/go-gost/core/logger" + "github.com/go-gost/plugin/ingress/proto" + "github.com/go-gost/x/internal/plugin" + "google.golang.org/grpc" +) + +type grpcPlugin struct { + conn grpc.ClientConnInterface + client proto.IngressClient + log logger.Logger +} + +// NewGRPCPlugin creates an Ingress plugin based on gRPC. +func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) ingress.Ingress { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "ingress", + "ingress": name, + }) + conn, err := plugin.NewGRPCConn(addr, &options) + if err != nil { + log.Error(err) + } + + p := &grpcPlugin{ + conn: conn, + log: log, + } + if conn != nil { + p.client = proto.NewIngressClient(conn) + } + return p +} + +func (p *grpcPlugin) GetRule(ctx context.Context, host string, opts ...ingress.Option) *ingress.Rule { + if p.client == nil { + return nil + } + + r, err := p.client.GetRule(ctx, + &proto.GetRuleRequest{ + Host: host, + }) + if err != nil { + p.log.Error(err) + return nil + } + if r.Endpoint == "" { + return nil + } + return &ingress.Rule{ + Hostname: host, + Endpoint: r.Endpoint, + } +} + +func (p *grpcPlugin) SetRule(ctx context.Context, rule *ingress.Rule, opts ...ingress.Option) bool { + if p.client == nil || rule == nil { + return false + } + + r, _ := p.client.SetRule(ctx, &proto.SetRuleRequest{ + Host: rule.Hostname, + Endpoint: rule.Endpoint, + }) + if r == nil { + return false + } + + return r.Ok +} + +func (p *grpcPlugin) Close() error { + if closer, ok := p.conn.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/ingress/plugin.go b/ingress/plugin/http.go similarity index 63% rename from ingress/plugin.go rename to ingress/plugin/http.go index c09a524..437552c 100644 --- a/ingress/plugin.go +++ b/ingress/plugin/http.go @@ -4,93 +4,13 @@ import ( "bytes" "context" "encoding/json" - "io" "net/http" "github.com/go-gost/core/ingress" "github.com/go-gost/core/logger" - "github.com/go-gost/plugin/ingress/proto" "github.com/go-gost/x/internal/plugin" - "google.golang.org/grpc" ) -type grpcPlugin struct { - conn grpc.ClientConnInterface - client proto.IngressClient - log logger.Logger -} - -// NewGRPCPlugin creates an Ingress plugin based on gRPC. -func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) ingress.Ingress { - var options plugin.Options - for _, opt := range opts { - opt(&options) - } - - log := logger.Default().WithFields(map[string]any{ - "kind": "ingress", - "ingress": name, - }) - conn, err := plugin.NewGRPCConn(addr, &options) - if err != nil { - log.Error(err) - } - - p := &grpcPlugin{ - conn: conn, - log: log, - } - if conn != nil { - p.client = proto.NewIngressClient(conn) - } - return p -} - -func (p *grpcPlugin) GetRule(ctx context.Context, host string, opts ...ingress.Option) *ingress.Rule { - if p.client == nil { - return nil - } - - r, err := p.client.GetRule(ctx, - &proto.GetRuleRequest{ - Host: host, - }) - if err != nil { - p.log.Error(err) - return nil - } - if r.Endpoint == "" { - return nil - } - return &ingress.Rule{ - Hostname: host, - Endpoint: r.Endpoint, - } -} - -func (p *grpcPlugin) SetRule(ctx context.Context, rule *ingress.Rule, opts ...ingress.Option) bool { - if p.client == nil || rule == nil { - return false - } - - r, _ := p.client.SetRule(ctx, &proto.SetRuleRequest{ - Host: rule.Hostname, - Endpoint: rule.Endpoint, - }) - if r == nil { - return false - } - - return r.Ok -} - -func (p *grpcPlugin) Close() error { - if closer, ok := p.conn.(io.Closer); ok { - return closer.Close() - } - return nil -} - type httpPluginGetRuleRequest struct { Host string `json:"host"` } diff --git a/internal/util/stats/stats.go b/internal/util/stats/stats.go new file mode 100644 index 0000000..81d33c1 --- /dev/null +++ b/internal/util/stats/stats.go @@ -0,0 +1,62 @@ +package stats + +import ( + "sync" + + "github.com/go-gost/core/observer" + "github.com/go-gost/x/stats" +) + +type HandlerStats struct { + service string + stats map[string]*stats.Stats + mu sync.RWMutex +} + +func NewHandlerStats(service string) *HandlerStats { + return &HandlerStats{ + service: service, + stats: make(map[string]*stats.Stats), + } +} + +func (p *HandlerStats) Stats(client string) *stats.Stats { + p.mu.RLock() + pstats := p.stats[client] + p.mu.RUnlock() + if pstats != nil { + return pstats + } + + p.mu.Lock() + defer p.mu.Unlock() + pstats = p.stats[client] + if pstats == nil { + pstats = &stats.Stats{} + } + p.stats[client] = pstats + + return pstats +} + +func (p *HandlerStats) Events() (events []observer.Event) { + p.mu.RLock() + defer p.mu.RUnlock() + + for k, v := range p.stats { + if !v.IsUpdated() { + continue + } + events = append(events, stats.StatsEvent{ + Kind: "handler", + Service: p.service, + Client: k, + TotalConns: v.Get(stats.KindTotalConns), + CurrentConns: v.Get(stats.KindCurrentConns), + InputBytes: v.Get(stats.KindInputBytes), + OutputBytes: v.Get(stats.KindOutputBytes), + TotalErrs: v.Get(stats.KindTotalErrs), + }) + } + return +} diff --git a/limiter/traffic/plugin/grpc.go b/limiter/traffic/plugin/grpc.go new file mode 100644 index 0000000..dbe456b --- /dev/null +++ b/limiter/traffic/plugin/grpc.go @@ -0,0 +1,102 @@ +package traffic + +import ( + "context" + "io" + + "github.com/go-gost/core/limiter/traffic" + "github.com/go-gost/core/logger" + "github.com/go-gost/plugin/limiter/traffic/proto" + "github.com/go-gost/x/internal/plugin" + xtraffic "github.com/go-gost/x/limiter/traffic" + "google.golang.org/grpc" +) + +type grpcPlugin struct { + conn grpc.ClientConnInterface + client proto.LimiterClient + log logger.Logger +} + +// NewGRPCPlugin creates a traffic limiter plugin based on gRPC. +func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) traffic.TrafficLimiter { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "limiter", + "limiter": name, + }) + conn, err := plugin.NewGRPCConn(addr, &options) + if err != nil { + log.Error(err) + } + + p := &grpcPlugin{ + conn: conn, + log: log, + } + if conn != nil { + p.client = proto.NewLimiterClient(conn) + } + return p +} + +func (p *grpcPlugin) In(ctx context.Context, key string, opts ...traffic.Option) traffic.Limiter { + if p.client == nil { + return nil + } + + var options traffic.Options + for _, opt := range opts { + opt(&options) + } + + r, err := p.client.Limit(ctx, + &proto.LimitRequest{ + Network: options.Network, + Addr: options.Addr, + Client: options.Client, + Src: options.Src, + }) + if err != nil { + p.log.Error(err) + return nil + } + + return xtraffic.NewLimiter(int(r.In)) +} + +func (p *grpcPlugin) Out(ctx context.Context, key string, opts ...traffic.Option) traffic.Limiter { + if p.client == nil { + return nil + } + + var options traffic.Options + for _, opt := range opts { + opt(&options) + } + + r, err := p.client.Limit(ctx, + &proto.LimitRequest{ + Network: options.Network, + Addr: options.Addr, + Client: options.Client, + Src: options.Src, + }) + if err != nil { + p.log.Error(err) + return nil + } + + return xtraffic.NewLimiter(int(r.Out)) +} + +func (p *grpcPlugin) Close() error { + if closer, ok := p.conn.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/limiter/traffic/plugin.go b/limiter/traffic/plugin/http.go similarity index 59% rename from limiter/traffic/plugin.go rename to limiter/traffic/plugin/http.go index a967566..7b85ffd 100644 --- a/limiter/traffic/plugin.go +++ b/limiter/traffic/plugin/http.go @@ -4,105 +4,14 @@ import ( "bytes" "context" "encoding/json" - "io" "net/http" "github.com/go-gost/core/limiter/traffic" "github.com/go-gost/core/logger" - "github.com/go-gost/plugin/limiter/traffic/proto" "github.com/go-gost/x/internal/plugin" - "google.golang.org/grpc" + xtraffic "github.com/go-gost/x/limiter/traffic" ) -type grpcPlugin struct { - conn grpc.ClientConnInterface - client proto.LimiterClient - log logger.Logger -} - -// NewGRPCPlugin creates a traffic limiter plugin based on gRPC. -func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) traffic.TrafficLimiter { - var options plugin.Options - for _, opt := range opts { - opt(&options) - } - - log := logger.Default().WithFields(map[string]any{ - "kind": "limiter", - "limiter": name, - }) - conn, err := plugin.NewGRPCConn(addr, &options) - if err != nil { - log.Error(err) - } - - p := &grpcPlugin{ - conn: conn, - log: log, - } - if conn != nil { - p.client = proto.NewLimiterClient(conn) - } - return p -} - -func (p *grpcPlugin) In(ctx context.Context, key string, opts ...traffic.Option) traffic.Limiter { - if p.client == nil { - return nil - } - - var options traffic.Options - for _, opt := range opts { - opt(&options) - } - - r, err := p.client.Limit(ctx, - &proto.LimitRequest{ - Network: options.Network, - Addr: options.Addr, - Client: options.Client, - Src: options.Src, - }) - if err != nil { - p.log.Error(err) - return nil - } - - return NewLimiter(int(r.In)) -} - -func (p *grpcPlugin) Out(ctx context.Context, key string, opts ...traffic.Option) traffic.Limiter { - if p.client == nil { - return nil - } - - var options traffic.Options - for _, opt := range opts { - opt(&options) - } - - r, err := p.client.Limit(ctx, - &proto.LimitRequest{ - Network: options.Network, - Addr: options.Addr, - Client: options.Client, - Src: options.Src, - }) - if err != nil { - p.log.Error(err) - return nil - } - - return NewLimiter(int(r.Out)) -} - -func (p *grpcPlugin) Close() error { - if closer, ok := p.conn.(io.Closer); ok { - return closer.Close() - } - return nil -} - type httpPluginRequest struct { Network string `json:"network"` Addr string `json:"addr"` @@ -184,7 +93,7 @@ func (p *httpPlugin) In(ctx context.Context, key string, opts ...traffic.Option) if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { return nil } - return NewLimiter(int(res.In)) + return xtraffic.NewLimiter(int(res.In)) } func (p *httpPlugin) Out(ctx context.Context, key string, opts ...traffic.Option) traffic.Limiter { @@ -231,5 +140,5 @@ func (p *httpPlugin) Out(ctx context.Context, key string, opts ...traffic.Option if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { return nil } - return NewLimiter(int(res.Out)) + return xtraffic.NewLimiter(int(res.Out)) } diff --git a/limiter/traffic/wrapper/io.go b/limiter/traffic/wrapper/io.go index 901a943..86923d2 100644 --- a/limiter/traffic/wrapper/io.go +++ b/limiter/traffic/wrapper/io.go @@ -22,7 +22,7 @@ type readWriter struct { key string } -func WrapReadWriter(limiter limiter.TrafficLimiter, rw io.ReadWriter, key string, opts ...limiter.Option) io.ReadWriter { +func WrapReadWriter(limiter limiter.TrafficLimiter, rw io.ReadWriter, opts ...limiter.Option) io.ReadWriter { if limiter == nil { return rw } diff --git a/listener/dns/listener.go b/listener/dns/listener.go index e5048e9..42717b2 100644 --- a/listener/dns/listener.go +++ b/listener/dns/listener.go @@ -17,6 +17,7 @@ import ( md "github.com/go-gost/core/metadata" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" "github.com/miekg/dns" ) @@ -117,6 +118,7 @@ func (l *dnsListener) Accept() (conn net.Conn, err error) { select { case conn = <-l.cqueue: conn = metrics.WrapConn(l.options.Service, conn) + conn = stats.WrapConn(conn, l.options.Stats) conn = admission.WrapConn(l.options.Admission, conn) conn = limiter.WrapConn(l.options.TrafficLimiter, conn) case err, ok = <-l.errChan: diff --git a/listener/dtls/listener.go b/listener/dtls/listener.go index 3796b3a..4675c21 100644 --- a/listener/dtls/listener.go +++ b/listener/dtls/listener.go @@ -17,6 +17,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" "github.com/pion/dtls/v2" ) @@ -79,6 +80,7 @@ func (l *dtlsListener) Init(md md.Metadata) (err error) { } ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/ftcp/listener.go b/listener/ftcp/listener.go index 4bfa88c..eb25d2f 100644 --- a/listener/ftcp/listener.go +++ b/listener/ftcp/listener.go @@ -12,6 +12,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" "github.com/xtaci/tcpraw" ) @@ -52,6 +53,7 @@ func (l *ftcpListener) Init(md md.Metadata) (err error) { return } conn = metrics.WrapPacketConn(l.options.Service, conn) + conn = stats.WrapPacketConn(conn, l.options.Stats) conn = admission.WrapPacketConn(l.options.Admission, conn) conn = limiter.WrapPacketConn(l.options.TrafficLimiter, conn) diff --git a/listener/grpc/listener.go b/listener/grpc/listener.go index 4c1b744..fd5f7ae 100644 --- a/listener/grpc/listener.go +++ b/listener/grpc/listener.go @@ -16,6 +16,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/keepalive" @@ -66,6 +67,7 @@ func (l *grpcListener) Init(md md.Metadata) (err error) { } ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/http2/h2/listener.go b/listener/http2/h2/listener.go index 5e4ab66..e01987f 100644 --- a/listener/http2/h2/listener.go +++ b/listener/http2/h2/listener.go @@ -19,6 +19,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) @@ -87,6 +88,7 @@ func (l *h2Listener) Init(md md.Metadata) (err error) { l.addr = ln.Addr() ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/http2/listener.go b/listener/http2/listener.go index a021660..ea115ab 100644 --- a/listener/http2/listener.go +++ b/listener/http2/listener.go @@ -18,6 +18,7 @@ import ( mdx "github.com/go-gost/x/metadata" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" "golang.org/x/net/http2" ) @@ -76,6 +77,7 @@ func (l *http2Listener) Init(md md.Metadata) (err error) { l.addr = ln.Addr() ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/http3/h3/listener.go b/listener/http3/h3/listener.go index fe812e9..fd0aaaa 100644 --- a/listener/http3/h3/listener.go +++ b/listener/http3/h3/listener.go @@ -12,6 +12,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" "github.com/quic-go/quic-go" ) @@ -85,6 +86,7 @@ func (l *http3Listener) Accept() (conn net.Conn, err error) { } conn = metrics.WrapConn(l.options.Service, conn) + conn = stats.WrapConn(conn, l.options.Stats) conn = admission.WrapConn(l.options.Admission, conn) conn = limiter.WrapConn(l.options.TrafficLimiter, conn) return conn, nil diff --git a/listener/http3/wt/listener.go b/listener/http3/wt/listener.go index 3e0d110..d2455ed 100644 --- a/listener/http3/wt/listener.go +++ b/listener/http3/wt/listener.go @@ -8,9 +8,13 @@ import ( "github.com/go-gost/core/listener" "github.com/go-gost/core/logger" md "github.com/go-gost/core/metadata" + admission "github.com/go-gost/x/admission/wrapper" xnet "github.com/go-gost/x/internal/net" wt_util "github.com/go-gost/x/internal/util/wt" + limiter "github.com/go-gost/x/limiter/traffic/wrapper" + metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/http3" wt "github.com/quic-go/webtransport-go" @@ -95,6 +99,10 @@ func (l *wtListener) Accept() (conn net.Conn, err error) { var ok bool select { case conn = <-l.cqueue: + conn = metrics.WrapConn(l.options.Service, conn) + conn = stats.WrapConn(conn, l.options.Stats) + conn = admission.WrapConn(l.options.Admission, conn) + conn = limiter.WrapConn(l.options.TrafficLimiter, conn) case err, ok = <-l.errChan: if !ok { err = listener.ErrClosed diff --git a/listener/icmp/listener.go b/listener/icmp/listener.go index d09cc0b..7347324 100644 --- a/listener/icmp/listener.go +++ b/listener/icmp/listener.go @@ -12,6 +12,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" "github.com/quic-go/quic-go" "golang.org/x/net/icmp" ) @@ -57,6 +58,7 @@ func (l *icmpListener) Init(md md.Metadata) (err error) { } conn = icmp_pkg.ServerConn(conn) conn = metrics.WrapPacketConn(l.options.Service, conn) + conn = stats.WrapPacketConn(conn, l.options.Stats) conn = admission.WrapPacketConn(l.options.Admission, conn) conn = limiter.WrapPacketConn(l.options.TrafficLimiter, conn) diff --git a/listener/kcp/listener.go b/listener/kcp/listener.go index abfdabd..93ea34d 100644 --- a/listener/kcp/listener.go +++ b/listener/kcp/listener.go @@ -13,6 +13,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" "github.com/xtaci/kcp-go/v5" "github.com/xtaci/smux" "github.com/xtaci/tcpraw" @@ -75,6 +76,7 @@ func (l *kcpListener) Init(md md.Metadata) (err error) { } conn = metrics.WrapUDPConn(l.options.Service, conn) + conn = stats.WrapUDPConn(conn, l.options.Stats) conn = admission.WrapUDPConn(l.options.Admission, conn) conn = limiter.WrapUDPConn(l.options.TrafficLimiter, conn) diff --git a/listener/mtcp/listener.go b/listener/mtcp/listener.go index b483910..22654e4 100644 --- a/listener/mtcp/listener.go +++ b/listener/mtcp/listener.go @@ -16,6 +16,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -66,6 +67,7 @@ func (l *mtcpListener) Init(md md.Metadata) (err error) { ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/mtls/listener.go b/listener/mtls/listener.go index 7e7061a..924fff9 100644 --- a/listener/mtls/listener.go +++ b/listener/mtls/listener.go @@ -17,6 +17,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -65,6 +66,7 @@ func (l *mtlsListener) Init(md md.Metadata) (err error) { ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/mws/listener.go b/listener/mws/listener.go index 4a12789..6888030 100644 --- a/listener/mws/listener.go +++ b/listener/mws/listener.go @@ -20,6 +20,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" "github.com/gorilla/websocket" ) @@ -107,6 +108,7 @@ func (l *mwsListener) Init(md md.Metadata) (err error) { } ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/obfs/http/listener.go b/listener/obfs/http/listener.go index 3111545..a1e0671 100644 --- a/listener/obfs/http/listener.go +++ b/listener/obfs/http/listener.go @@ -9,13 +9,13 @@ import ( "github.com/go-gost/core/logger" md "github.com/go-gost/core/metadata" admission "github.com/go-gost/x/admission/wrapper" + xnet "github.com/go-gost/x/internal/net" + "github.com/go-gost/x/internal/net/proxyproto" climiter "github.com/go-gost/x/limiter/conn/wrapper" limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" - - xnet "github.com/go-gost/x/internal/net" - "github.com/go-gost/x/internal/net/proxyproto" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -61,6 +61,7 @@ func (l *obfsListener) Init(md md.Metadata) (err error) { } ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/obfs/tls/listener.go b/listener/obfs/tls/listener.go index ff27bb6..3aa29f6 100644 --- a/listener/obfs/tls/listener.go +++ b/listener/obfs/tls/listener.go @@ -15,6 +15,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -60,6 +61,7 @@ func (l *obfsListener) Init(md md.Metadata) (err error) { } ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/pht/listener.go b/listener/pht/listener.go index df30bd2..50a7999 100644 --- a/listener/pht/listener.go +++ b/listener/pht/listener.go @@ -14,6 +14,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -92,6 +93,7 @@ func (l *phtListener) Accept() (conn net.Conn, err error) { return } conn = metrics.WrapConn(l.options.Service, conn) + conn = stats.WrapConn(conn, l.options.Stats) conn = admission.WrapConn(l.options.Admission, conn) conn = limiter.WrapConn(l.options.TrafficLimiter, conn) return diff --git a/listener/quic/listener.go b/listener/quic/listener.go index 0d80d4d..6b3c9ff 100644 --- a/listener/quic/listener.go +++ b/listener/quic/listener.go @@ -13,6 +13,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" "github.com/quic-go/quic-go" ) @@ -102,6 +103,7 @@ func (l *quicListener) Accept() (conn net.Conn, err error) { select { case conn = <-l.cqueue: conn = metrics.WrapConn(l.options.Service, conn) + conn = stats.WrapConn(conn, l.options.Stats) conn = admission.WrapConn(l.options.Admission, conn) conn = limiter.WrapConn(l.options.TrafficLimiter, conn) case err, ok = <-l.errChan: diff --git a/listener/redirect/tcp/listener.go b/listener/redirect/tcp/listener.go index 00de640..bcbfb17 100644 --- a/listener/redirect/tcp/listener.go +++ b/listener/redirect/tcp/listener.go @@ -15,6 +15,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -65,6 +66,7 @@ func (l *redirectListener) Init(md md.Metadata) (err error) { ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/redirect/udp/listener.go b/listener/redirect/udp/listener.go index 98f29bc..d2b22e3 100644 --- a/listener/redirect/udp/listener.go +++ b/listener/redirect/udp/listener.go @@ -10,6 +10,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -54,6 +55,7 @@ func (l *redirectListener) Accept() (conn net.Conn, err error) { return } conn = metrics.WrapConn(l.options.Service, conn) + conn = stats.WrapConn(conn, l.options.Stats) conn = admission.WrapConn(l.options.Admission, conn) conn = limiter.WrapConn(l.options.TrafficLimiter, conn) return diff --git a/listener/rtcp/listener.go b/listener/rtcp/listener.go index ff51da6..263ae15 100644 --- a/listener/rtcp/listener.go +++ b/listener/rtcp/listener.go @@ -15,6 +15,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -84,6 +85,7 @@ func (l *rtcpListener) Accept() (conn net.Conn, err error) { return nil, listener.NewAcceptError(err) } ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/rudp/listener.go b/listener/rudp/listener.go index bbd7848..5b3ca23 100644 --- a/listener/rudp/listener.go +++ b/listener/rudp/listener.go @@ -14,6 +14,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -105,6 +106,7 @@ func (l *rudpListener) Accept() (conn net.Conn, err error) { if pc, ok := conn.(net.PacketConn); ok { uc := metrics.WrapUDPConn(l.options.Service, pc) + uc = stats.WrapUDPConn(uc, l.options.Stats) uc = admission.WrapUDPConn(l.options.Admission, uc) conn = limiter.WrapUDPConn(l.options.TrafficLimiter, uc) } diff --git a/listener/serial/listener.go b/listener/serial/listener.go index b4b5540..7db6f2e 100644 --- a/listener/serial/listener.go +++ b/listener/serial/listener.go @@ -12,6 +12,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -95,6 +96,7 @@ func (l *serialListener) listenLoop() { c := serial.NewConn(port, l.addr, cancel) c = metrics.WrapConn(l.options.Service, c) + c = stats.WrapConn(c, l.options.Stats) c = limiter.WrapConn(l.options.TrafficLimiter, c) l.cqueue <- c diff --git a/listener/ssh/listener.go b/listener/ssh/listener.go index b5b7879..18383a2 100644 --- a/listener/ssh/listener.go +++ b/listener/ssh/listener.go @@ -17,6 +17,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" "golang.org/x/crypto/ssh" ) @@ -67,6 +68,7 @@ func (l *sshListener) Init(md md.Metadata) (err error) { ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/sshd/listener.go b/listener/sshd/listener.go index ef19a22..5f891ec 100644 --- a/listener/sshd/listener.go +++ b/listener/sshd/listener.go @@ -20,6 +20,7 @@ import ( metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" "golang.org/x/crypto/ssh" + stats "github.com/go-gost/x/stats/wrapper" ) // Applicable SSH Request types for Port Forwarding - RFC 4254 7.X @@ -75,6 +76,7 @@ func (l *sshdListener) Init(md md.Metadata) (err error) { ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/tap/listener.go b/listener/tap/listener.go index 46f453b..02b5803 100644 --- a/listener/tap/listener.go +++ b/listener/tap/listener.go @@ -13,6 +13,7 @@ import ( mdx "github.com/go-gost/x/metadata" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -90,6 +91,7 @@ func (l *tapListener) listenLoop() { cancel: cancel, } c = metrics.WrapConn(l.options.Service, c) + c = stats.WrapConn(c, l.options.Stats) c = limiter.WrapConn(l.options.TrafficLimiter, c) c = withMetadata(mdx.NewMetadata(map[string]any{ "config": l.md.config, diff --git a/listener/tcp/listener.go b/listener/tcp/listener.go index 1e34e00..2ca37b1 100644 --- a/listener/tcp/listener.go +++ b/listener/tcp/listener.go @@ -15,6 +15,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -63,6 +64,7 @@ func (l *tcpListener) Init(md md.Metadata) (err error) { ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/tls/listener.go b/listener/tls/listener.go index 5460e6f..0f46624 100644 --- a/listener/tls/listener.go +++ b/listener/tls/listener.go @@ -16,6 +16,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -61,6 +62,7 @@ func (l *tlsListener) Init(md md.Metadata) (err error) { } ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/tun/listener.go b/listener/tun/listener.go index 2b2cce8..a6a5058 100644 --- a/listener/tun/listener.go +++ b/listener/tun/listener.go @@ -14,6 +14,7 @@ import ( mdx "github.com/go-gost/x/metadata" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -27,7 +28,7 @@ type tunListener struct { logger logger.Logger md metadata options listener.Options - routes []*router.Route + routes []*router.Route } func NewListener(opts ...listener.Option) listener.Listener { @@ -91,6 +92,7 @@ func (l *tunListener) listenLoop() { cancel: cancel, } c = metrics.WrapConn(l.options.Service, c) + c = stats.WrapConn(c, l.options.Stats) c = limiter.WrapConn(l.options.TrafficLimiter, c) c = withMetadata(mdx.NewMetadata(map[string]any{ "config": l.md.config, diff --git a/listener/udp/listener.go b/listener/udp/listener.go index f428f3c..a33073b 100644 --- a/listener/udp/listener.go +++ b/listener/udp/listener.go @@ -12,6 +12,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -56,6 +57,7 @@ func (l *udpListener) Init(md md.Metadata) (err error) { return } conn = metrics.WrapPacketConn(l.options.Service, conn) + conn = stats.WrapPacketConn(conn, l.options.Stats) conn = admission.WrapPacketConn(l.options.Admission, conn) conn = limiter.WrapPacketConn(l.options.TrafficLimiter, conn) diff --git a/listener/unix/listener.go b/listener/unix/listener.go index 98fe4c8..2c0d94b 100644 --- a/listener/unix/listener.go +++ b/listener/unix/listener.go @@ -11,6 +11,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" ) func init() { @@ -46,6 +47,7 @@ func (l *unixListener) Init(md md.Metadata) (err error) { } ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/listener/ws/listener.go b/listener/ws/listener.go index 16ce03d..b32146a 100644 --- a/listener/ws/listener.go +++ b/listener/ws/listener.go @@ -19,6 +19,7 @@ import ( limiter "github.com/go-gost/x/limiter/traffic/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" "github.com/go-gost/x/registry" + stats "github.com/go-gost/x/stats/wrapper" "github.com/gorilla/websocket" ) @@ -102,6 +103,7 @@ func (l *wsListener) Init(md md.Metadata) (err error) { } ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) + ln = stats.WrapListener(ln, l.options.Stats) ln = admission.WrapListener(l.options.Admission, ln) ln = limiter.WrapListener(l.options.TrafficLimiter, ln) ln = climiter.WrapListener(l.options.ConnLimiter, ln) diff --git a/observer/plugin/grpc.go b/observer/plugin/grpc.go new file mode 100644 index 0000000..ecbad4b --- /dev/null +++ b/observer/plugin/grpc.go @@ -0,0 +1,98 @@ +package observer + +import ( + "context" + "io" + + "github.com/go-gost/core/logger" + "github.com/go-gost/core/observer" + "github.com/go-gost/plugin/observer/proto" + "github.com/go-gost/x/internal/plugin" + "github.com/go-gost/x/service" + "github.com/go-gost/x/stats" + "google.golang.org/grpc" +) + +type grpcPlugin struct { + conn grpc.ClientConnInterface + client proto.ObserverClient + log logger.Logger +} + +// NewGRPCPlugin creates an Observer plugin based on gRPC. +func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) observer.Observer { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "observer", + "observer": name, + }) + conn, err := plugin.NewGRPCConn(addr, &options) + if err != nil { + log.Error(err) + } + + p := &grpcPlugin{ + conn: conn, + log: log, + } + if conn != nil { + p.client = proto.NewObserverClient(conn) + } + return p +} + +func (p *grpcPlugin) Observe(ctx context.Context, events []observer.Event, opts ...observer.Option) error { + if p.client == nil || len(events) == 0 { + return nil + } + + var req proto.ObserveRequest + + for _, event := range events { + switch event.Type() { + case observer.EventStatus: + ev := event.(service.ServiceEvent) + req.Events = append(req.Events, &proto.Event{ + Kind: ev.Kind, + Service: ev.Service, + Type: string(event.Type()), + Status: &proto.ServiceStatus{ + State: string(ev.State), + Msg: ev.Msg, + }, + }) + case observer.EventStats: + ev := event.(stats.StatsEvent) + req.Events = append(req.Events, &proto.Event{ + Kind: ev.Kind, + Service: ev.Service, + Client: ev.Client, + Type: string(event.Type()), + Stats: &proto.Stats{ + TotalConns: ev.TotalConns, + CurrentConns: ev.CurrentConns, + InputBytes: ev.InputBytes, + OutputBytes: ev.OutputBytes, + TotalErrs: ev.TotalErrs, + }, + }) + } + } + _, err := p.client.Observe(ctx, &req) + if err != nil { + p.log.Error(err) + return err + } + return nil +} + +func (p *grpcPlugin) Close() error { + if closer, ok := p.conn.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/observer/plugin/http.go b/observer/plugin/http.go new file mode 100644 index 0000000..321e35b --- /dev/null +++ b/observer/plugin/http.go @@ -0,0 +1,134 @@ +package observer + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/go-gost/core/logger" + "github.com/go-gost/core/observer" + "github.com/go-gost/x/internal/plugin" + "github.com/go-gost/x/service" + "github.com/go-gost/x/stats" +) + +type observeRequest struct { + Events []event `json:"events"` +} + +type event struct { + Kind string `json:"kind"` + Service string `json:"service"` + Client string `json:"client,omitempty"` + Type observer.EventType `json:"type"` + Stats *statsEvent `json:"stats,omitempty"` + Status *statusEvent `json:"status,omitempty"` +} + +type statsEvent struct { + TotalConns uint64 `json:"totalConns"` + CurrentConns uint64 `json:"currentConns"` + InputBytes uint64 `json:"inputBytes"` + OutputBytes uint64 `json:"outputBytes"` + TotalErrs uint64 `json:"totalErrs"` +} + +type statusEvent struct { + State string `json:"state"` + Msg string `json:"msg"` +} + +type observeResponse struct { + OK bool `json:"ok"` +} + +type httpPlugin struct { + url string + client *http.Client + header http.Header + log logger.Logger +} + +// NewHTTPPlugin creates an Observer plugin based on HTTP. +func NewHTTPPlugin(name string, url string, opts ...plugin.Option) observer.Observer { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + return &httpPlugin{ + url: url, + client: plugin.NewHTTPClient(&options), + header: options.Header, + log: logger.Default().WithFields(map[string]any{ + "kind": "observer", + "observer": name, + }), + } +} + +func (p *httpPlugin) Observe(ctx context.Context, events []observer.Event, opts ...observer.Option) error { + if p.client == nil || len(events) == 0 { + return nil + } + + var r observeRequest + + for _, e := range events { + switch e.Type() { + case observer.EventStatus: + ev := e.(service.ServiceEvent) + r.Events = append(r.Events, event{ + Kind: ev.Kind, + Service: ev.Service, + Type: ev.Type(), + Status: &statusEvent{ + State: string(ev.State), + Msg: ev.Msg, + }, + }) + case observer.EventStats: + ev := e.(stats.StatsEvent) + r.Events = append(r.Events, event{ + Kind: ev.Kind, + Service: ev.Service, + Client: ev.Client, + Type: ev.Type(), + Stats: &statsEvent{ + TotalConns: ev.TotalConns, + CurrentConns: ev.CurrentConns, + InputBytes: ev.InputBytes, + OutputBytes: ev.OutputBytes, + TotalErrs: ev.TotalErrs, + }, + }) + } + } + v, err := json.Marshal(r) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.url, bytes.NewReader(v)) + if err != nil { + return err + } + + if p.header != nil { + req.Header = p.header.Clone() + } + req.Header.Set("Content-Type", "application/json") + resp, err := p.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf(resp.Status) + } + + return nil +} diff --git a/recorder/plugin/grpc.go b/recorder/plugin/grpc.go new file mode 100644 index 0000000..11fa028 --- /dev/null +++ b/recorder/plugin/grpc.go @@ -0,0 +1,76 @@ +package recorder + +import ( + "context" + "encoding/json" + "io" + + "github.com/go-gost/core/logger" + "github.com/go-gost/core/recorder" + "github.com/go-gost/plugin/recorder/proto" + "github.com/go-gost/x/internal/plugin" + "google.golang.org/grpc" +) + +type grpcPlugin struct { + conn grpc.ClientConnInterface + client proto.RecorderClient + log logger.Logger +} + +// NewGRPCPlugin creates a Recorder plugin based on gRPC. +func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) recorder.Recorder { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "recorder", + "recorder": name, + }) + conn, err := plugin.NewGRPCConn(addr, &options) + if err != nil { + log.Error(err) + } + + p := &grpcPlugin{ + conn: conn, + log: log, + } + if conn != nil { + p.client = proto.NewRecorderClient(conn) + } + return p +} + +func (p *grpcPlugin) Record(ctx context.Context, b []byte, opts ...recorder.RecordOption) error { + if p.client == nil { + return nil + } + + var options recorder.RecordOptions + for _, opt := range opts { + opt(&options) + } + + md, _ := json.Marshal(options.Metadata) + + _, err := p.client.Record(ctx, + &proto.RecordRequest{ + Data: b, + Metadata: md, + }) + if err != nil { + p.log.Error(err) + return err + } + return nil +} + +func (p *grpcPlugin) Close() error { + if closer, ok := p.conn.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/recorder/plugin.go b/recorder/plugin/http.go similarity index 59% rename from recorder/plugin.go rename to recorder/plugin/http.go index a0b6a5d..a0442b5 100644 --- a/recorder/plugin.go +++ b/recorder/plugin/http.go @@ -6,79 +6,13 @@ import ( "encoding/json" "errors" "fmt" - "io" "net/http" "github.com/go-gost/core/logger" "github.com/go-gost/core/recorder" - "github.com/go-gost/plugin/recorder/proto" "github.com/go-gost/x/internal/plugin" - "google.golang.org/grpc" ) -type grpcPlugin struct { - conn grpc.ClientConnInterface - client proto.RecorderClient - log logger.Logger -} - -// NewGRPCPlugin creates a Recorder plugin based on gRPC. -func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) recorder.Recorder { - var options plugin.Options - for _, opt := range opts { - opt(&options) - } - - log := logger.Default().WithFields(map[string]any{ - "kind": "recorder", - "recorder": name, - }) - conn, err := plugin.NewGRPCConn(addr, &options) - if err != nil { - log.Error(err) - } - - p := &grpcPlugin{ - conn: conn, - log: log, - } - if conn != nil { - p.client = proto.NewRecorderClient(conn) - } - return p -} - -func (p *grpcPlugin) Record(ctx context.Context, b []byte, opts ...recorder.RecordOption) error { - if p.client == nil { - return nil - } - - var options recorder.RecordOptions - for _, opt := range opts { - opt(&options) - } - - md, _ := json.Marshal(options.Metadata) - - _, err := p.client.Record(ctx, - &proto.RecordRequest{ - Data: b, - Metadata: md, - }) - if err != nil { - p.log.Error(err) - return err - } - return nil -} - -func (p *grpcPlugin) Close() error { - if closer, ok := p.conn.(io.Closer); ok { - return closer.Close() - } - return nil -} - type httpPluginRequest struct { Data []byte `json:"data"` Metadata []byte `json:"metadata"` diff --git a/registry/observer.go b/registry/observer.go new file mode 100644 index 0000000..62e9df2 --- /dev/null +++ b/registry/observer.go @@ -0,0 +1,39 @@ +package registry + +import ( + "context" + + "github.com/go-gost/core/observer" +) + +type observerRegistry struct { + registry[observer.Observer] +} + +func (r *observerRegistry) Register(name string, v observer.Observer) error { + return r.registry.Register(name, v) +} + +func (r *observerRegistry) Get(name string) observer.Observer { + if name != "" { + return &observerWrapper{name: name, r: r} + } + return nil +} + +func (r *observerRegistry) get(name string) observer.Observer { + return r.registry.Get(name) +} + +type observerWrapper struct { + name string + r *observerRegistry +} + +func (w *observerWrapper) Observe(ctx context.Context, events []observer.Event, opts ...observer.Option) error { + v := w.r.get(w.name) + if v == nil { + return nil + } + return v.Observe(ctx, events, opts...) +} diff --git a/registry/registry.go b/registry/registry.go index 933386c..489f65d 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -16,6 +16,7 @@ import ( "github.com/go-gost/core/limiter/rate" "github.com/go-gost/core/limiter/traffic" "github.com/go-gost/core/logger" + "github.com/go-gost/core/observer" "github.com/go-gost/core/recorder" reg "github.com/go-gost/core/registry" "github.com/go-gost/core/resolver" @@ -47,9 +48,10 @@ var ( connLimiterReg reg.Registry[conn.ConnLimiter] = new(connLimiterRegistry) rateLimiterReg reg.Registry[rate.RateLimiter] = new(rateLimiterRegistry) - ingressReg reg.Registry[ingress.Ingress] = new(ingressRegistry) - routerReg reg.Registry[router.Router] = new(routerRegistry) - sdReg reg.Registry[sd.SD] = new(sdRegistry) + ingressReg reg.Registry[ingress.Ingress] = new(ingressRegistry) + routerReg reg.Registry[router.Router] = new(routerRegistry) + sdReg reg.Registry[sd.SD] = new(sdRegistry) + observerReg reg.Registry[observer.Observer] = new(observerRegistry) loggerReg reg.Registry[logger.Logger] = new(loggerRegistry) ) @@ -179,6 +181,10 @@ func SDRegistry() reg.Registry[sd.SD] { return sdReg } +func ObserverRegistry() reg.Registry[observer.Observer] { + return observerReg +} + func LoggerRegistry() reg.Registry[logger.Logger] { return loggerReg } diff --git a/resolver/plugin/grpc.go b/resolver/plugin/grpc.go new file mode 100644 index 0000000..a221143 --- /dev/null +++ b/resolver/plugin/grpc.go @@ -0,0 +1,77 @@ +package resolver + +import ( + "context" + "io" + "net" + + "github.com/go-gost/core/logger" + "github.com/go-gost/core/resolver" + "github.com/go-gost/plugin/resolver/proto" + ctxvalue "github.com/go-gost/x/internal/ctx" + "github.com/go-gost/x/internal/plugin" + "google.golang.org/grpc" +) + +type grpcPlugin struct { + conn grpc.ClientConnInterface + client proto.ResolverClient + log logger.Logger +} + +// NewGRPCPlugin creates a Resolver plugin based on gRPC. +func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) (resolver.Resolver, error) { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "resolver", + "resolover": name, + }) + conn, err := plugin.NewGRPCConn(addr, &options) + if err != nil { + log.Error(err) + } + p := &grpcPlugin{ + conn: conn, + log: log, + } + if conn != nil { + p.client = proto.NewResolverClient(conn) + } + return p, nil +} + +func (p *grpcPlugin) Resolve(ctx context.Context, network, host string, opts ...resolver.Option) (ips []net.IP, err error) { + p.log.Debugf("resolve %s/%s", host, network) + + if p.client == nil { + return + } + + r, err := p.client.Resolve(ctx, + &proto.ResolveRequest{ + Network: network, + Host: host, + Client: string(ctxvalue.ClientIDFromContext(ctx)), + }) + if err != nil { + p.log.Error(err) + return + } + for _, s := range r.Ips { + if ip := net.ParseIP(s); ip != nil { + ips = append(ips, ip) + } + } + return +} + +func (p *grpcPlugin) Close() error { + if closer, ok := p.conn.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/resolver/plugin.go b/resolver/plugin/http.go similarity index 60% rename from resolver/plugin.go rename to resolver/plugin/http.go index d523a45..4a4cb2a 100644 --- a/resolver/plugin.go +++ b/resolver/plugin/http.go @@ -6,81 +6,15 @@ import ( "encoding/json" "errors" "fmt" - "io" "net" "net/http" "github.com/go-gost/core/logger" "github.com/go-gost/core/resolver" - "github.com/go-gost/plugin/resolver/proto" ctxvalue "github.com/go-gost/x/internal/ctx" "github.com/go-gost/x/internal/plugin" - "google.golang.org/grpc" ) -type grpcPlugin struct { - conn grpc.ClientConnInterface - client proto.ResolverClient - log logger.Logger -} - -// NewGRPCPlugin creates a Resolver plugin based on gRPC. -func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) (resolver.Resolver, error) { - var options plugin.Options - for _, opt := range opts { - opt(&options) - } - - log := logger.Default().WithFields(map[string]any{ - "kind": "resolver", - "resolover": name, - }) - conn, err := plugin.NewGRPCConn(addr, &options) - if err != nil { - log.Error(err) - } - p := &grpcPlugin{ - conn: conn, - log: log, - } - if conn != nil { - p.client = proto.NewResolverClient(conn) - } - return p, nil -} - -func (p *grpcPlugin) Resolve(ctx context.Context, network, host string, opts ...resolver.Option) (ips []net.IP, err error) { - p.log.Debugf("resolve %s/%s", host, network) - - if p.client == nil { - return - } - - r, err := p.client.Resolve(ctx, - &proto.ResolveRequest{ - Network: network, - Host: host, - Client: string(ctxvalue.ClientIDFromContext(ctx)), - }) - if err != nil { - p.log.Error(err) - return - } - for _, s := range r.Ips { - if ip := net.ParseIP(s); ip != nil { - ips = append(ips, ip) - } - } - return -} - -func (p *grpcPlugin) Close() error { - if closer, ok := p.conn.(io.Closer); ok { - return closer.Close() - } - return nil -} - type httpPluginRequest struct { Network string `json:"network"` Host string `json:"host"` diff --git a/router/plugin/grpc.go b/router/plugin/grpc.go new file mode 100644 index 0000000..d966f34 --- /dev/null +++ b/router/plugin/grpc.go @@ -0,0 +1,70 @@ +package router + +import ( + "context" + "io" + "net" + + "github.com/go-gost/core/logger" + "github.com/go-gost/core/router" + "github.com/go-gost/plugin/router/proto" + "github.com/go-gost/x/internal/plugin" + xrouter "github.com/go-gost/x/router" + "google.golang.org/grpc" +) + +type grpcPlugin struct { + conn grpc.ClientConnInterface + client proto.RouterClient + log logger.Logger +} + +// NewGRPCPlugin creates an Router plugin based on gRPC. +func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) router.Router { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "router", + "router": name, + }) + conn, err := plugin.NewGRPCConn(addr, &options) + if err != nil { + log.Error(err) + } + + p := &grpcPlugin{ + conn: conn, + log: log, + } + if conn != nil { + p.client = proto.NewRouterClient(conn) + } + return p +} + +func (p *grpcPlugin) GetRoute(ctx context.Context, dst net.IP, opts ...router.Option) *router.Route { + if p.client == nil { + return nil + } + + r, err := p.client.GetRoute(ctx, + &proto.GetRouteRequest{ + Dst: dst.String(), + }) + if err != nil { + p.log.Error(err) + return nil + } + + return xrouter.ParseRoute(r.Net, r.Gateway) +} + +func (p *grpcPlugin) Close() error { + if closer, ok := p.conn.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/router/plugin.go b/router/plugin/http.go similarity index 57% rename from router/plugin.go rename to router/plugin/http.go index 23c1268..03cc069 100644 --- a/router/plugin.go +++ b/router/plugin/http.go @@ -3,73 +3,15 @@ package router import ( "context" "encoding/json" - "io" "net" "net/http" "github.com/go-gost/core/logger" "github.com/go-gost/core/router" - "github.com/go-gost/plugin/router/proto" "github.com/go-gost/x/internal/plugin" - "google.golang.org/grpc" + xrouter "github.com/go-gost/x/router" ) -type grpcPlugin struct { - conn grpc.ClientConnInterface - client proto.RouterClient - log logger.Logger -} - -// NewGRPCPlugin creates an Router plugin based on gRPC. -func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) router.Router { - var options plugin.Options - for _, opt := range opts { - opt(&options) - } - - log := logger.Default().WithFields(map[string]any{ - "kind": "router", - "router": name, - }) - conn, err := plugin.NewGRPCConn(addr, &options) - if err != nil { - log.Error(err) - } - - p := &grpcPlugin{ - conn: conn, - log: log, - } - if conn != nil { - p.client = proto.NewRouterClient(conn) - } - return p -} - -func (p *grpcPlugin) GetRoute(ctx context.Context, dst net.IP, opts ...router.Option) *router.Route { - if p.client == nil { - return nil - } - - r, err := p.client.GetRoute(ctx, - &proto.GetRouteRequest{ - Dst: dst.String(), - }) - if err != nil { - p.log.Error(err) - return nil - } - - return ParseRoute(r.Net, r.Gateway) -} - -func (p *grpcPlugin) Close() error { - if closer, ok := p.conn.(io.Closer); ok { - return closer.Close() - } - return nil -} - type httpPluginGetRouteRequest struct { Dst string `json:"dst"` } @@ -137,5 +79,5 @@ func (p *httpPlugin) GetRoute(ctx context.Context, dst net.IP, opts ...router.Op return nil } - return ParseRoute(res.Net, res.Gateway) + return xrouter.ParseRoute(res.Net, res.Gateway) } diff --git a/sd/plugin/grpc.go b/sd/plugin/grpc.go new file mode 100644 index 0000000..0b08f70 --- /dev/null +++ b/sd/plugin/grpc.go @@ -0,0 +1,134 @@ +package sd + +import ( + "context" + "io" + + "github.com/go-gost/core/logger" + "github.com/go-gost/core/sd" + "github.com/go-gost/plugin/sd/proto" + "github.com/go-gost/x/internal/plugin" + "google.golang.org/grpc" +) + +type grpcPlugin struct { + conn grpc.ClientConnInterface + client proto.SDClient + log logger.Logger +} + +// NewGRPCPlugin creates an SD plugin based on gRPC. +func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) sd.SD { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "sd", + "sd": name, + }) + conn, err := plugin.NewGRPCConn(addr, &options) + if err != nil { + log.Error(err) + } + + p := &grpcPlugin{ + conn: conn, + log: log, + } + if conn != nil { + p.client = proto.NewSDClient(conn) + } + return p +} + +func (p *grpcPlugin) Register(ctx context.Context, service *sd.Service, opts ...sd.Option) error { + if p.client == nil { + return nil + } + + _, err := p.client.Register(ctx, + &proto.RegisterRequest{ + Service: &proto.Service{ + Id: service.ID, + Name: service.Name, + Node: service.Node, + Network: service.Network, + Address: service.Address, + }, + }) + if err != nil { + p.log.Error(err) + return err + } + return nil +} + +func (p *grpcPlugin) Deregister(ctx context.Context, service *sd.Service) error { + if p.client == nil { + return nil + } + + _, err := p.client.Deregister(ctx, &proto.DeregisterRequest{ + Service: &proto.Service{ + Id: service.ID, + Name: service.Name, + Node: service.Node, + Network: service.Network, + Address: service.Address, + }, + }) + return err +} + +func (p *grpcPlugin) Renew(ctx context.Context, service *sd.Service) error { + if p.client == nil { + return nil + } + + _, err := p.client.Renew(ctx, &proto.RenewRequest{ + Service: &proto.Service{ + Id: service.ID, + Name: service.Name, + Node: service.Node, + Network: service.Network, + Address: service.Address, + }, + }) + return err +} + +func (p *grpcPlugin) Get(ctx context.Context, name string) ([]*sd.Service, error) { + if p.client == nil { + return nil, nil + } + + r, err := p.client.Get(ctx, &proto.GetServiceRequest{ + Name: name, + }) + if err != nil { + return nil, err + } + + var services []*sd.Service + for _, v := range r.Services { + if v == nil { + continue + } + services = append(services, &sd.Service{ + Node: v.Node, + Name: v.Name, + Network: v.Network, + Address: v.Address, + }) + } + return services, nil +} + +func (p *grpcPlugin) Close() error { + if closer, ok := p.conn.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/sd/plugin.go b/sd/plugin/http.go similarity index 62% rename from sd/plugin.go rename to sd/plugin/http.go index e41b16b..87c0766 100644 --- a/sd/plugin.go +++ b/sd/plugin/http.go @@ -1,142 +1,17 @@ -package ingress +package sd import ( "bytes" "context" "encoding/json" "fmt" - "io" "net/http" "github.com/go-gost/core/logger" "github.com/go-gost/core/sd" - "github.com/go-gost/plugin/sd/proto" "github.com/go-gost/x/internal/plugin" - "google.golang.org/grpc" ) -type grpcPlugin struct { - conn grpc.ClientConnInterface - client proto.SDClient - log logger.Logger -} - -// NewGRPCPlugin creates an SD plugin based on gRPC. -func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) sd.SD { - var options plugin.Options - for _, opt := range opts { - opt(&options) - } - - log := logger.Default().WithFields(map[string]any{ - "kind": "sd", - "sd": name, - }) - conn, err := plugin.NewGRPCConn(addr, &options) - if err != nil { - log.Error(err) - } - - p := &grpcPlugin{ - conn: conn, - log: log, - } - if conn != nil { - p.client = proto.NewSDClient(conn) - } - return p -} - -func (p *grpcPlugin) Register(ctx context.Context, service *sd.Service, opts ...sd.Option) error { - if p.client == nil { - return nil - } - - _, err := p.client.Register(ctx, - &proto.RegisterRequest{ - Service: &proto.Service{ - Id: service.ID, - Name: service.Name, - Node: service.Node, - Network: service.Network, - Address: service.Address, - }, - }) - if err != nil { - p.log.Error(err) - return err - } - return nil -} - -func (p *grpcPlugin) Deregister(ctx context.Context, service *sd.Service) error { - if p.client == nil { - return nil - } - - _, err := p.client.Deregister(ctx, &proto.DeregisterRequest{ - Service: &proto.Service{ - Id: service.ID, - Name: service.Name, - Node: service.Node, - Network: service.Network, - Address: service.Address, - }, - }) - return err -} - -func (p *grpcPlugin) Renew(ctx context.Context, service *sd.Service) error { - if p.client == nil { - return nil - } - - _, err := p.client.Renew(ctx, &proto.RenewRequest{ - Service: &proto.Service{ - Id: service.ID, - Name: service.Name, - Node: service.Node, - Network: service.Network, - Address: service.Address, - }, - }) - return err -} - -func (p *grpcPlugin) Get(ctx context.Context, name string) ([]*sd.Service, error) { - if p.client == nil { - return nil, nil - } - - r, err := p.client.Get(ctx, &proto.GetServiceRequest{ - Name: name, - }) - if err != nil { - return nil, err - } - - var services []*sd.Service - for _, v := range r.Services { - if v == nil { - continue - } - services = append(services, &sd.Service{ - Node: v.Node, - Name: v.Name, - Network: v.Network, - Address: v.Address, - }) - } - return services, nil -} - -func (p *grpcPlugin) Close() error { - if closer, ok := p.conn.(io.Closer); ok { - return closer.Close() - } - return nil -} - type sdService struct { ID string `json:"id"` Name string `json:"name"` diff --git a/service/service.go b/service/service.go index 86bcfc5..e25b9e9 100644 --- a/service/service.go +++ b/service/service.go @@ -2,6 +2,7 @@ package service import ( "context" + "fmt" "io" "net" "os/exec" @@ -13,10 +14,12 @@ import ( "github.com/go-gost/core/listener" "github.com/go-gost/core/logger" "github.com/go-gost/core/metrics" + "github.com/go-gost/core/observer" "github.com/go-gost/core/recorder" "github.com/go-gost/core/service" ctxvalue "github.com/go-gost/x/internal/ctx" xmetrics "github.com/go-gost/x/metrics" + "github.com/go-gost/x/stats" "github.com/rs/xid" ) @@ -27,6 +30,8 @@ type options struct { postUp []string preDown []string postDown []string + stats *stats.Stats + observer observer.Observer logger logger.Logger } @@ -68,6 +73,18 @@ func PostDownOption(cmds []string) Option { } } +func StatsOption(stats *stats.Stats) Option { + return func(opts *options) { + opts.stats = stats + } +} + +func ObserverOption(observer observer.Observer) Option { + return func(opts *options) { + opts.observer = observer + } +} + func LoggerOption(logger logger.Logger) Option { return func(opts *options) { opts.logger = logger @@ -78,6 +95,7 @@ type defaultService struct { name string listener listener.Listener handler handler.Handler + status *Status options options } @@ -91,7 +109,13 @@ func NewService(name string, ln listener.Listener, h handler.Handler, opts ...Op listener: ln, handler: h, options: options, + status: &Status{ + createTime: time.Now(), + events: make([]Event, 0, MaxEventSize), + stats: options.stats, + }, } + s.setState(StateRunning) s.execCmds("pre-up", s.options.preUp) @@ -104,6 +128,14 @@ func (s *defaultService) Addr() net.Addr { func (s *defaultService) Serve() error { s.execCmds("post-up", s.options.postUp) + s.setState(StateReady) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if s.status.Stats() != nil { + go s.observeStats(ctx) + } if v := xmetrics.GetGauge( xmetrics.MetricServicesGauge, @@ -126,14 +158,25 @@ func (s *defaultService) Serve() error { if max := 5 * time.Second; tempDelay > max { tempDelay = max } + + s.setState(StateFailed) + s.options.logger.Warnf("accept: %v, retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } + s.setState(StateClosed) s.options.logger.Errorf("accept: %v", e) + return e } - tempDelay = 0 + + if tempDelay > 0 { + tempDelay = 0 + s.setState(StateReady) + } + + s.status.stats.Add(stats.KindTotalConns, 1) clientAddr := conn.RemoteAddr().String() clientIP := clientAddr @@ -141,7 +184,7 @@ func (s *defaultService) Serve() error { clientIP = h } - ctx := ctxvalue.ContextWithSid(context.Background(), ctxvalue.Sid(xid.New().String())) + ctx := ctxvalue.ContextWithSid(ctx, ctxvalue.Sid(xid.New().String())) ctx = ctxvalue.ContextWithClientAddr(ctx, ctxvalue.ClientAddr(clientAddr)) ctx = ctxvalue.ContextWithHash(ctx, &ctxvalue.Hash{Source: clientIP}) @@ -161,6 +204,9 @@ func (s *defaultService) Serve() error { } go func() { + s.status.stats.Add(stats.KindCurrentConns, 1) + defer s.status.stats.Add(stats.KindCurrentConns, -1) + if v := xmetrics.GetCounter(xmetrics.MetricServiceRequestsCounter, metrics.Labels{"service": s.name, "client": clientIP}); v != nil { v.Inc() @@ -186,11 +232,16 @@ func (s *defaultService) Serve() error { metrics.Labels{"service": s.name, "client": clientIP}); v != nil { v.Inc() } + s.status.stats.Add(stats.KindTotalErrs, 1) } }() } } +func (s *defaultService) Status() *Status { + return s.status +} + func (s *defaultService) Close() error { s.execCmds("pre-down", s.options.preDown) defer s.execCmds("post-down", s.options.postDown) @@ -214,3 +265,64 @@ func (s *defaultService) execCmds(phase string, cmds []string) { } } } + +func (s *defaultService) setState(state State) { + s.status.setState(state) + + msg := fmt.Sprintf("service %s is %s", s.name, state) + s.status.addEvent(Event{ + Time: time.Now(), + Message: msg, + }) + + if obs := s.options.observer; obs != nil { + obs.Observe(context.Background(), []observer.Event{ServiceEvent{ + Kind: "service", + Service: s.name, + State: state, + Msg: msg, + }}) + } +} + +func (s *defaultService) observeStats(ctx context.Context) { + if s.options.observer == nil { + return + } + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + st := s.status.Stats() + if !st.IsUpdated() { + break + } + s.options.observer.Observe(ctx, []observer.Event{ + stats.StatsEvent{ + Kind: "service", + Service: s.name, + TotalConns: st.Get(stats.KindTotalConns), + CurrentConns: st.Get(stats.KindCurrentConns), + InputBytes: st.Get(stats.KindInputBytes), + OutputBytes: st.Get(stats.KindOutputBytes), + }, + }) + case <-ctx.Done(): + return + } + } +} + +type ServiceEvent struct { + Kind string + Service string + State State + Msg string +} + +func (ServiceEvent) Type() observer.EventType { + return observer.EventStatus +} diff --git a/service/status.go b/service/status.go new file mode 100644 index 0000000..0d3a913 --- /dev/null +++ b/service/status.go @@ -0,0 +1,76 @@ +package service + +import ( + "sync" + "time" + + "github.com/go-gost/x/stats" +) + +const ( + MaxEventSize = 20 +) + +type State string + +const ( + StateRunning State = "running" + StateReady State = "ready" + StateFailed State = "failed" + StateClosed State = "closed" +) + +type Event struct { + Time time.Time + Message string +} + +type Status struct { + createTime time.Time + state State + events []Event + stats *stats.Stats + mu sync.RWMutex +} + +func (p *Status) CreateTime() time.Time { + return p.createTime +} + +func (p *Status) State() State { + p.mu.RLock() + defer p.mu.RUnlock() + return p.state +} + +func (p *Status) setState(state State) { + p.mu.Lock() + defer p.mu.Unlock() + p.state = state +} + +func (p *Status) Events() []Event { + events := make([]Event, MaxEventSize) + + p.mu.RLock() + defer p.mu.RUnlock() + + copy(events, p.events) + return events +} + +func (p *Status) addEvent(event Event) { + p.mu.Lock() + defer p.mu.Unlock() + + if len(p.events) == MaxEventSize { + events := make([]Event, MaxEventSize-1, MaxEventSize) + copy(events, p.events[1:]) + p.events = events + } + p.events = append(p.events, event) +} + +func (p *Status) Stats() *stats.Stats { + return p.stats +} diff --git a/stats/stats.go b/stats/stats.go new file mode 100644 index 0000000..d00bf75 --- /dev/null +++ b/stats/stats.go @@ -0,0 +1,93 @@ +package stats + +import ( + "sync/atomic" + + "github.com/go-gost/core/observer" +) + +type Kind int + +const ( + KindTotalConns Kind = 1 + KindCurrentConns Kind = 2 + KindInputBytes Kind = 3 + KindOutputBytes Kind = 4 + KindTotalErrs Kind = 5 +) + +type Stats struct { + updated atomic.Bool + totalConns atomic.Uint64 + currentConns atomic.Int64 + inputBytes atomic.Uint64 + outputBytes atomic.Uint64 + totalErrs atomic.Uint64 +} + +func (s *Stats) Add(kind Kind, n int64) { + if s == nil { + return + } + switch kind { + case KindTotalConns: + if n > 0 { + s.totalConns.Add(uint64(n)) + } + case KindCurrentConns: + s.currentConns.Add(n) + case KindInputBytes: + if n > 0 { + s.inputBytes.Add(uint64(n)) + } + case KindOutputBytes: + if n > 0 { + s.outputBytes.Add(uint64(n)) + } + case KindTotalErrs: + if n > 0 { + s.totalErrs.Add(uint64(n)) + } + } + s.updated.Store(true) +} + +func (s *Stats) Get(kind Kind) uint64 { + if s == nil { + return 0 + } + + switch kind { + case KindTotalConns: + return s.totalConns.Load() + case KindCurrentConns: + return uint64(s.currentConns.Load()) + case KindInputBytes: + return s.inputBytes.Load() + case KindOutputBytes: + return s.outputBytes.Load() + case KindTotalErrs: + return s.totalErrs.Load() + } + return 0 +} + +func (s *Stats) IsUpdated() bool { + return s.updated.Swap(false) +} + +type StatsEvent struct { + Kind string + Service string + Client string + + TotalConns uint64 + CurrentConns uint64 + InputBytes uint64 + OutputBytes uint64 + TotalErrs uint64 +} + +func (StatsEvent) Type() observer.EventType { + return observer.EventStats +} diff --git a/stats/wrapper/conn.go b/stats/wrapper/conn.go new file mode 100644 index 0000000..5e00f05 --- /dev/null +++ b/stats/wrapper/conn.go @@ -0,0 +1,222 @@ +package wrapper + +import ( + "errors" + "io" + "net" + "syscall" + + "github.com/go-gost/core/metadata" + xnet "github.com/go-gost/x/internal/net" + "github.com/go-gost/x/internal/net/udp" + "github.com/go-gost/x/stats" +) + +var ( + errUnsupport = errors.New("unsupported operation") +) + +type conn struct { + net.Conn + stats *stats.Stats +} + +func WrapConn(c net.Conn, stats *stats.Stats) net.Conn { + if stats == nil { + return c + } + + return &conn{ + Conn: c, + stats: stats, + } +} + +func (c *conn) Read(b []byte) (n int, err error) { + n, err = c.Conn.Read(b) + c.stats.Add(stats.KindInputBytes, int64(n)) + return +} + +func (c *conn) Write(b []byte) (n int, err error) { + n, err = c.Conn.Write(b) + c.stats.Add(stats.KindOutputBytes, int64(n)) + return +} + +func (c *conn) SyscallConn() (rc syscall.RawConn, err error) { + if sc, ok := c.Conn.(syscall.Conn); ok { + rc, err = sc.SyscallConn() + return + } + err = errUnsupport + return +} + +func (c *conn) Metadata() metadata.Metadata { + if md, ok := c.Conn.(metadata.Metadatable); ok { + return md.Metadata() + } + return nil +} + +type packetConn struct { + net.PacketConn + stats *stats.Stats +} + +func WrapPacketConn(pc net.PacketConn, stats *stats.Stats) net.PacketConn { + if stats == nil { + return pc + } + return &packetConn{ + PacketConn: pc, + stats: stats, + } +} + +func (c *packetConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + n, addr, err = c.PacketConn.ReadFrom(p) + c.stats.Add(stats.KindInputBytes, int64(n)) + return +} + +func (c *packetConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + n, err = c.PacketConn.WriteTo(p, addr) + c.stats.Add(stats.KindOutputBytes, int64(n)) + return +} + +func (c *packetConn) Metadata() metadata.Metadata { + if md, ok := c.PacketConn.(metadata.Metadatable); ok { + return md.Metadata() + } + return nil +} + +type udpConn struct { + net.PacketConn + stats *stats.Stats +} + +func WrapUDPConn(pc net.PacketConn, stats *stats.Stats) udp.Conn { + return &udpConn{ + PacketConn: pc, + stats: stats, + } +} + +func (c *udpConn) RemoteAddr() net.Addr { + if nc, ok := c.PacketConn.(xnet.RemoteAddr); ok { + return nc.RemoteAddr() + } + return nil +} + +func (c *udpConn) SetReadBuffer(n int) error { + if nc, ok := c.PacketConn.(xnet.SetBuffer); ok { + return nc.SetReadBuffer(n) + } + return errUnsupport +} + +func (c *udpConn) SetWriteBuffer(n int) error { + if nc, ok := c.PacketConn.(xnet.SetBuffer); ok { + return nc.SetWriteBuffer(n) + } + return errUnsupport +} + +func (c *udpConn) Read(b []byte) (n int, err error) { + if nc, ok := c.PacketConn.(io.Reader); ok { + n, err = nc.Read(b) + c.stats.Add(stats.KindInputBytes, int64(n)) + return + } + err = errUnsupport + return +} + +func (c *udpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + n, addr, err = c.PacketConn.ReadFrom(p) + c.stats.Add(stats.KindInputBytes, int64(n)) + return +} + +func (c *udpConn) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) { + if nc, ok := c.PacketConn.(udp.ReadUDP); ok { + n, addr, err = nc.ReadFromUDP(b) + c.stats.Add(stats.KindInputBytes, int64(n)) + return + } + err = errUnsupport + return +} + +func (c *udpConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) { + if nc, ok := c.PacketConn.(udp.ReadUDP); ok { + n, oobn, flags, addr, err = nc.ReadMsgUDP(b, oob) + c.stats.Add(stats.KindInputBytes, int64(n)) + return + } + err = errUnsupport + return +} + +func (c *udpConn) Write(b []byte) (n int, err error) { + if nc, ok := c.PacketConn.(io.Writer); ok { + n, err = nc.Write(b) + c.stats.Add(stats.KindOutputBytes, int64(n)) + return + } + err = errUnsupport + return +} + +func (c *udpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + n, err = c.PacketConn.WriteTo(p, addr) + c.stats.Add(stats.KindOutputBytes, int64(n)) + return +} + +func (c *udpConn) WriteToUDP(b []byte, addr *net.UDPAddr) (n int, err error) { + if nc, ok := c.PacketConn.(udp.WriteUDP); ok { + n, err = nc.WriteToUDP(b, addr) + c.stats.Add(stats.KindOutputBytes, int64(n)) + return + } + err = errUnsupport + return +} + +func (c *udpConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) { + if nc, ok := c.PacketConn.(udp.WriteUDP); ok { + n, oobn, err = nc.WriteMsgUDP(b, oob, addr) + c.stats.Add(stats.KindOutputBytes, int64(n)) + return + } + err = errUnsupport + return +} + +func (c *udpConn) SyscallConn() (rc syscall.RawConn, err error) { + if nc, ok := c.PacketConn.(syscall.Conn); ok { + return nc.SyscallConn() + } + err = errUnsupport + return +} + +func (c *udpConn) SetDSCP(n int) error { + if nc, ok := c.PacketConn.(xnet.SetDSCP); ok { + return nc.SetDSCP(n) + } + return nil +} + +func (c *udpConn) Metadata() metadata.Metadata { + if md, ok := c.PacketConn.(metadata.Metadatable); ok { + return md.Metadata() + } + return nil +} diff --git a/stats/wrapper/io.go b/stats/wrapper/io.go new file mode 100644 index 0000000..365606f --- /dev/null +++ b/stats/wrapper/io.go @@ -0,0 +1,38 @@ +package wrapper + +import ( + "io" + + "github.com/go-gost/x/stats" +) + +// readWriter is an io.ReadWriter with Stats. +type readWriter struct { + io.ReadWriter + stats *stats.Stats +} + +func WrapReadWriter(rw io.ReadWriter, stats *stats.Stats) io.ReadWriter { + if stats == nil { + return rw + } + + return &readWriter{ + ReadWriter: rw, + stats: stats, + } +} + +func (p *readWriter) Read(b []byte) (n int, err error) { + n, err = p.ReadWriter.Read(b) + p.stats.Add(stats.KindInputBytes, int64(n)) + + return +} + +func (p *readWriter) Write(b []byte) (n int, err error) { + n, err = p.ReadWriter.Write(b) + p.stats.Add(stats.KindOutputBytes, int64(n)) + + return +} diff --git a/stats/wrapper/listener.go b/stats/wrapper/listener.go new file mode 100644 index 0000000..88c272e --- /dev/null +++ b/stats/wrapper/listener.go @@ -0,0 +1,32 @@ +package wrapper + +import ( + "net" + + "github.com/go-gost/x/stats" +) + +type listener struct { + stats *stats.Stats + net.Listener +} + +func WrapListener(ln net.Listener, stats *stats.Stats) net.Listener { + if stats == nil { + return ln + } + + return &listener{ + stats: stats, + Listener: ln, + } +} + +func (ln *listener) Accept() (net.Conn, error) { + c, err := ln.Listener.Accept() + if err != nil { + return nil, err + } + + return WrapConn(c, ln.stats), nil +} From 262ac0e9a57cd728a9e4035a54879e8459b5eabd Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sun, 7 Jan 2024 19:40:42 +0800 Subject: [PATCH 2/5] add header for basic auth --- api/middleware.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/middleware.go b/api/middleware.go index c350b1c..27202c3 100644 --- a/api/middleware.go +++ b/api/middleware.go @@ -36,6 +36,7 @@ func mwBasicAuth(auther auth.Authenticator) gin.HandlerFunc { } u, p, _ := c.Request.BasicAuth() if _, ok := auther.Authenticate(c, u, p); !ok { + c.Writer.Header().Set("WWW-Authenticate", "Basic") c.AbortWithStatus(http.StatusUnauthorized) } } From 936954ecf29d3f172d51473bedcc322d9e85ead3 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Mon, 8 Jan 2024 21:20:56 +0800 Subject: [PATCH 3/5] fix dns handler panic --- handler/dns/handler.go | 5 ++++- handler/dns/metadata.go | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/handler/dns/handler.go b/handler/dns/handler.go index 18ec098..e7c71fb 100644 --- a/handler/dns/handler.go +++ b/handler/dns/handler.go @@ -71,7 +71,10 @@ func (h *dnsHandler) Init(md md.Metadata) (err error) { for i, addr := range h.md.dns { nodes = append(nodes, chain.NewNode(fmt.Sprintf("target-%d", i), addr)) } - h.hop = xhop.NewHop(xhop.NodeOption(nodes...)) + h.hop = xhop.NewHop( + xhop.NodeOption(nodes...), + xhop.LoggerOption(log), + ) } var nodes []*chain.Node diff --git a/handler/dns/metadata.go b/handler/dns/metadata.go index 90ba3b2..4b45e83 100644 --- a/handler/dns/metadata.go +++ b/handler/dns/metadata.go @@ -2,6 +2,7 @@ package dns import ( "net" + "strings" "time" mdata "github.com/go-gost/core/metadata" @@ -45,7 +46,14 @@ func (h *dnsHandler) parseMetadata(md mdata.Metadata) (err error) { if sip != "" { h.md.clientIP = net.ParseIP(sip) } - h.md.dns = mdutil.GetStrings(md, dns) + for _, v := range strings.Split(mdutil.GetString(md, dns), ",") { + v = strings.TrimSpace(v) + if v == "" { + continue + } + h.md.dns = append(h.md.dns, v) + } + h.md.bufferSize = mdutil.GetInt(md, bufferSize) if h.md.bufferSize <= 0 { h.md.bufferSize = defaultBufferSize From c04c28e1fd78d797e6eed8b79d0c0539c34ee1cd Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Mon, 8 Jan 2024 21:22:20 +0800 Subject: [PATCH 4/5] add protocol based node selection for hop --- hop/hop.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/hop/hop.go b/hop/hop.go index dec6392..b528f2e 100644 --- a/hop/hop.go +++ b/hop/hop.go @@ -168,16 +168,24 @@ func (p *chainHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain. if len(filters) == 0 { filters = nodes } - } else if protocol := options.Protocol; protocol != "" { - filters = nil - for _, node := range ns { + } + + if protocol := options.Protocol; protocol != "" { + p.options.logger.Debugf("filter by protocol: %s", protocol) + var nodes []*chain.Node + for _, node := range filters { if node == nil { continue } + if node.Options().Protocol == "" { + nodes = append(nodes, node) + continue + } if node.Options().Protocol == protocol { - filters = append(filters, node) + nodes = append(nodes, node) } } + filters = nodes } // filter by path From 01168e98467cfc5138bdf6b5e1cc9c5128b2a349 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Fri, 12 Jan 2024 23:46:22 +0800 Subject: [PATCH 5/5] fix deadlock in websocket client conn --- api/config.go | 4 +- config/config.go | 3 +- config/parsing/service/parse.go | 7 +- handler/auto/handler.go | 2 + handler/http/handler.go | 1 + hop/hop.go | 173 +++++++++++++++++++------------- internal/util/ws/ws.go | 9 +- 7 files changed, 122 insertions(+), 77 deletions(-) diff --git a/api/config.go b/api/config.go index 3cb09ea..e5261a7 100644 --- a/api/config.go +++ b/api/config.go @@ -54,7 +54,7 @@ func getConfig(ctx *gin.Context) { if ok && ss != nil { status := ss.Status() svc.Status = &config.ServiceStatus{ - CreateTime: status.CreateTime().Unix(), + CreateTime: status.CreateTime().UnixNano(), State: string(status.State()), } if st := status.Stats(); st != nil { @@ -69,7 +69,7 @@ func getConfig(ctx *gin.Context) { for _, ev := range status.Events() { if !ev.Time.IsZero() { svc.Status.Events = append(svc.Status.Events, config.ServiceEvent{ - Time: ev.Time.Unix(), + Time: ev.Time.UnixNano(), Msg: ev.Message, }) } diff --git a/config/config.go b/config/config.go index 7c6333f..c511738 100644 --- a/config/config.go +++ b/config/config.go @@ -360,6 +360,7 @@ type ForwardNodeConfig struct { HTTP *HTTPNodeConfig `yaml:",omitempty" json:"http,omitempty"` TLS *TLSNodeConfig `yaml:",omitempty" json:"tls,omitempty"` Auth *AuthConfig `yaml:",omitempty" json:"auth,omitempty"` + Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"` } type HTTPNodeConfig struct { @@ -482,10 +483,10 @@ type NodeConfig struct { Hosts string `yaml:",omitempty" json:"hosts,omitempty"` Connector *ConnectorConfig `yaml:",omitempty" json:"connector,omitempty"` Dialer *DialerConfig `yaml:",omitempty" json:"dialer,omitempty"` - Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"` HTTP *HTTPNodeConfig `yaml:",omitempty" json:"http,omitempty"` TLS *TLSNodeConfig `yaml:",omitempty" json:"tls,omitempty"` Auth *AuthConfig `yaml:",omitempty" json:"auth,omitempty"` + Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"` } type Config struct { diff --git a/config/parsing/service/parse.go b/config/parsing/service/parse.go index 09cf5eb..6dfa36e 100644 --- a/config/parsing/service/parse.go +++ b/config/parsing/service/parse.go @@ -234,7 +234,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) { } if forwarder, ok := h.(handler.Forwarder); ok { - hop, err := parseForwarder(cfg.Forwarder) + hop, err := parseForwarder(cfg.Forwarder, log) if err != nil { return nil, err } @@ -266,7 +266,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) { return s, nil } -func parseForwarder(cfg *config.ForwarderConfig) (hop.Hop, error) { +func parseForwarder(cfg *config.ForwarderConfig, log logger.Logger) (hop.Hop, error) { if cfg == nil { return nil, nil } @@ -298,12 +298,13 @@ func parseForwarder(cfg *config.ForwarderConfig) (hop.Hop, error) { HTTP: node.HTTP, TLS: node.TLS, Auth: node.Auth, + Metadata: node.Metadata, }) } } } if len(hc.Nodes) > 0 { - return hop_parser.ParseHop(&hc, logger.Default()) + return hop_parser.ParseHop(&hc, log) } return registry.HopRegistry().Get(hc.Name), nil } diff --git a/handler/auto/handler.go b/handler/auto/handler.go index 80c9063..3b251ce 100644 --- a/handler/auto/handler.go +++ b/handler/auto/handler.go @@ -11,6 +11,7 @@ import ( md "github.com/go-gost/core/metadata" "github.com/go-gost/gosocks4" "github.com/go-gost/gosocks5" + ctxvalue "github.com/go-gost/x/internal/ctx" netpkg "github.com/go-gost/x/internal/net" "github.com/go-gost/x/registry" ) @@ -79,6 +80,7 @@ func (h *autoHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler log := h.options.Logger.WithFields(map[string]any{ "remote": conn.RemoteAddr().String(), "local": conn.LocalAddr().String(), + "sid": ctxvalue.SidFromContext(ctx), }) if log.IsLevelEnabled(logger.DebugLevel) { diff --git a/handler/http/handler.go b/handler/http/handler.go index 4ddb4f9..89a190e 100644 --- a/handler/http/handler.go +++ b/handler/http/handler.go @@ -84,6 +84,7 @@ func (h *httpHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler log := h.options.Logger.WithFields(map[string]any{ "remote": conn.RemoteAddr().String(), "local": conn.LocalAddr().String(), + "sid": ctxvalue.SidFromContext(ctx), }) log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) defer func() { diff --git a/hop/hop.go b/hop/hop.go index b528f2e..689bb30 100644 --- a/hop/hop.go +++ b/hop/hop.go @@ -133,80 +133,15 @@ func (p *chainHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain. opt(&options) } - ns := p.Nodes() - if len(ns) == 0 { - return nil - } - // hop level bypass if p.options.bypass != nil && p.options.bypass.Contains(ctx, options.Network, options.Addr, bypass.WithHostOpton(options.Host)) { return nil } - filters := ns - if host := options.Host; host != "" { - filters = nil - if v, _, _ := net.SplitHostPort(host); v != "" { - host = v - } - var nodes []*chain.Node - for _, node := range ns { - if node == nil { - continue - } - vhost := node.Options().Host - if vhost == "" { - nodes = append(nodes, node) - continue - } - if vhost == host || - vhost[0] == '.' && strings.HasSuffix(host, vhost[1:]) { - filters = append(filters, node) - } - } - if len(filters) == 0 { - filters = nodes - } - } - - if protocol := options.Protocol; protocol != "" { - p.options.logger.Debugf("filter by protocol: %s", protocol) - var nodes []*chain.Node - for _, node := range filters { - if node == nil { - continue - } - if node.Options().Protocol == "" { - nodes = append(nodes, node) - continue - } - if node.Options().Protocol == protocol { - nodes = append(nodes, node) - } - } - filters = nodes - } - - // filter by path - if path := options.Path; path != "" { - p.options.logger.Debugf("filter by path: %s", path) - sort.SliceStable(filters, func(i, j int) bool { - return len(filters[i].Options().Path) > len(filters[j].Options().Path) - }) - var nodes []*chain.Node - for _, node := range filters { - if node.Options().Path == "" { - nodes = append(nodes, node) - continue - } - if strings.HasPrefix(path, node.Options().Path) { - nodes = append(nodes, node) - break - } - } - filters = nodes - } + filters := p.filterByHost(options.Host, p.Nodes()...) + filters = p.filterByProtocol(options.Protocol, filters...) + filters = p.filterByPath(options.Path, filters...) var nodes []*chain.Node for _, node := range filters { @@ -231,6 +166,108 @@ func (p *chainHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain. return nodes[0] } +func (p *chainHop) filterByHost(host string, nodes ...*chain.Node) (filters []*chain.Node) { + if host == "" || len(nodes) == 0 { + return nodes + } + + if v, _, _ := net.SplitHostPort(host); v != "" { + host = v + } + p.options.logger.Debugf("filter by host: %s", host) + + found := false + for _, node := range nodes { + if node == nil { + continue + } + vhost := node.Options().Host + if vhost == "" { // backup node + if !found { + filters = append(filters, node) + } + continue + } + + if vhost == host || + vhost[0] == '.' && strings.HasSuffix(host, vhost[1:]) { + if !found { // clear all backup nodes when matched node found + filters = nil + } + filters = append(filters, node) + found = true + continue + } + + } + + return +} + +func (p *chainHop) filterByProtocol(protocol string, nodes ...*chain.Node) (filters []*chain.Node) { + if protocol == "" || len(nodes) == 0 { + return nodes + } + + p.options.logger.Debugf("filter by protocol: %s", protocol) + found := false + for _, node := range nodes { + if node == nil { + continue + } + + if node.Options().Protocol == "" { + if !found { + filters = append(filters, node) + } + continue + } + + if node.Options().Protocol == protocol { + if !found { + filters = nil + } + filters = append(filters, node) + found = true + continue + } + } + + return +} + +func (p *chainHop) filterByPath(path string, nodes ...*chain.Node) (filters []*chain.Node) { + if path == "" || len(nodes) == 0 { + return nodes + } + + p.options.logger.Debugf("filter by path: %s", path) + + sort.SliceStable(nodes, func(i, j int) bool { + return len(nodes[i].Options().Path) > len(nodes[j].Options().Path) + }) + + found := false + for _, node := range nodes { + if node.Options().Path == "" { + if !found { + filters = append(filters, node) + } + continue + } + + if strings.HasPrefix(path, node.Options().Path) { + if !found { + filters = nil + } + filters = append(filters, node) + break + } + } + + return +} + func (p *chainHop) periodReload(ctx context.Context) error { period := p.options.period if period < time.Second { diff --git a/internal/util/ws/ws.go b/internal/util/ws/ws.go index 0dd6612..a6c1512 100644 --- a/internal/util/ws/ws.go +++ b/internal/util/ws/ws.go @@ -49,15 +49,18 @@ func (c *websocketConn) WriteMessage(messageType int, data []byte) error { } func (c *websocketConn) SetDeadline(t time.Time) error { - c.mux.Lock() - defer c.mux.Unlock() - if err := c.SetReadDeadline(t); err != nil { return err } return c.SetWriteDeadline(t) } +func (c *websocketConn) SetReadDeadline(t time.Time) error { + c.mux.Lock() + defer c.mux.Unlock() + return c.Conn.SetReadDeadline(t) +} + func (c *websocketConn) SetWriteDeadline(t time.Time) error { c.mux.Lock() defer c.mux.Unlock()