From 3b25e0317f9e6413bb4bd770cc7c531f0dcf4ba3 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sat, 27 Jun 2026 21:44:13 +0800 Subject: [PATCH] feat(rewriter): implement plugin-based Rewriter module with gRPC and HTTP backends Add the first implementation of core/rewriter.Rewriter as a plugin-only component following the bypass/admission pattern (single-value, no RewriterObject). Includes: - plugin/rewriter/proto/: protobuf definition with Rewrite RPC returning transformed data in the reply - x/rewriter/plugin/: gRPC and HTTP plugin clients - x/registry/rewriter.go: hot-reload-safe RewriterRegistry with wrapper - x/config/config.go: RewriterConfig, Config.Rewriters, ServiceConfig.Rewriter - x/config/parsing/rewriter/parse.go: config parser (plugin backends only) - core/handler/option.go: Rewriter field + RewriterOption on handler.Options - x/config/loader/loader.go: rewriter registration during startup - x/config/parsing/service/parse.go: inject rewriter into handler options --- config/config.go | 11 +++- config/loader/loader.go | 12 ++++ config/parsing/rewriter/parse.go | 47 ++++++++++++++ config/parsing/service/parse.go | 7 +++ go.mod | 4 +- go.sum | 8 +-- registry/registry.go | 7 +++ registry/rewriter.go | 43 +++++++++++++ rewriter/plugin/grpc.go | 89 ++++++++++++++++++++++++++ rewriter/plugin/http.go | 104 +++++++++++++++++++++++++++++++ 10 files changed, 324 insertions(+), 8 deletions(-) create mode 100644 config/parsing/rewriter/parse.go create mode 100644 registry/rewriter.go create mode 100644 rewriter/plugin/grpc.go create mode 100644 rewriter/plugin/http.go diff --git a/config/config.go b/config/config.go index 7d462c0d..3b0e892b 100644 --- a/config/config.go +++ b/config/config.go @@ -316,6 +316,11 @@ type RecorderObject struct { Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"` } +type RewriterConfig struct { + Name string `json:"name"` + Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"` +} + type LimiterConfig struct { Name string `json:"name"` Limits []string `yaml:",omitempty" json:"limits,omitempty"` @@ -487,8 +492,9 @@ type ServiceConfig struct { 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"` + Rewriter string `yaml:",omitempty" json:"rewriter,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"` @@ -637,6 +643,7 @@ type Config struct { Routers []*RouterConfig `yaml:",omitempty" json:"routers,omitempty"` SDs []*SDConfig `yaml:"sds,omitempty" json:"sds,omitempty"` Recorders []*RecorderConfig `yaml:",omitempty" json:"recorders,omitempty"` + Rewriters []*RewriterConfig `yaml:",omitempty" json:"rewriters,omitempty"` Limiters []*LimiterConfig `yaml:",omitempty" json:"limiters,omitempty"` Quotas []*QuotaConfig `yaml:",omitempty" json:"quotas,omitempty"` CLimiters []*LimiterConfig `yaml:"climiters,omitempty" json:"climiters,omitempty"` diff --git a/config/loader/loader.go b/config/loader/loader.go index 2e9c5f58..86310c20 100644 --- a/config/loader/loader.go +++ b/config/loader/loader.go @@ -22,6 +22,7 @@ import ( reg "github.com/go-gost/core/registry" "github.com/go-gost/core/resolver" "github.com/go-gost/core/router" + "github.com/go-gost/core/rewriter" "github.com/go-gost/core/sd" "github.com/go-gost/x/config" "github.com/go-gost/x/config/parsing" @@ -38,6 +39,7 @@ import ( quota_parser "github.com/go-gost/x/config/parsing/quota" recorder_parser "github.com/go-gost/x/config/parsing/recorder" resolver_parser "github.com/go-gost/x/config/parsing/resolver" + rewriter_parser "github.com/go-gost/x/config/parsing/rewriter" router_parser "github.com/go-gost/x/config/parsing/router" sd_parser "github.com/go-gost/x/config/parsing/sd" service_parser "github.com/go-gost/x/config/parsing/service" @@ -244,6 +246,16 @@ func register(cfg *config.Config) error { } } + { + var entries []named[rewriter.Rewriter] + for _, c := range cfg.Rewriters { + entries = append(entries, named[rewriter.Rewriter]{c.Name, rewriter_parser.ParseRewriter(c)}) + } + if err := registerGroup(entries, registry.RewriterRegistry()); err != nil { + return err + } + } + { var entries []named[traffic.TrafficLimiter] for _, c := range cfg.Limiters { diff --git a/config/parsing/rewriter/parse.go b/config/parsing/rewriter/parse.go new file mode 100644 index 00000000..3ba5535a --- /dev/null +++ b/config/parsing/rewriter/parse.go @@ -0,0 +1,47 @@ +package rewriter + +import ( + "crypto/tls" + "strings" + + "github.com/go-gost/core/rewriter" + "github.com/go-gost/x/config" + "github.com/go-gost/x/internal/plugin" + rewriter_plugin "github.com/go-gost/x/rewriter/plugin" +) + +// ParseRewriter converts a RewriterConfig into a rewriter.Rewriter. +// It currently supports plugin backends only (HTTP or gRPC). +// Returns nil when cfg is nil or no backend is configured. +func ParseRewriter(cfg *config.RewriterConfig) rewriter.Rewriter { + if cfg == nil { + return nil + } + + if cfg.Plugin != 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 rewriter_plugin.NewHTTPPlugin( + cfg.Name, cfg.Plugin.Addr, + plugin.TokenOption(cfg.Plugin.Token), + plugin.TLSConfigOption(tlsCfg), + plugin.TimeoutOption(cfg.Plugin.Timeout), + ) + default: + return rewriter_plugin.NewGRPCPlugin( + cfg.Name, cfg.Plugin.Addr, + plugin.TokenOption(cfg.Plugin.Token), + plugin.TLSConfigOption(tlsCfg), + ) + } + } + + return nil +} diff --git a/config/parsing/service/parse.go b/config/parsing/service/parse.go index 315691d1..2bf7052f 100644 --- a/config/parsing/service/parse.go +++ b/config/parsing/service/parse.go @@ -10,6 +10,7 @@ import ( "github.com/go-gost/core/chain" "github.com/go-gost/core/handler" "github.com/go-gost/core/hop" + "github.com/go-gost/core/rewriter" "github.com/go-gost/core/listener" "github.com/go-gost/core/logger" "github.com/go-gost/core/observer/stats" @@ -353,6 +354,11 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) { }) } + var rew rewriter.Rewriter + if cfg.Rewriter != "" { + rew = registry.RewriterRegistry().Get(cfg.Rewriter) + } + routerOpts = []chain.RouterOption{ chain.RetriesRouterOption(cfg.Handler.Retries), chain.TimeoutRouterOption(dialTimeout), @@ -382,6 +388,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) { handler.TrafficLimiterOption(registry.TrafficLimiterRegistry().Get(cfg.Handler.Limiter)), handler.ObserverOption(registry.ObserverRegistry().Get(cfg.Handler.Observer)), handler.RecordersOption(recorders...), + handler.RewriterOption(rew), handler.LoggerOption(handlerLogger), handler.ServiceOption(cfg.Name), handler.NetnsOption(netnsIn), diff --git a/go.mod b/go.mod index ad5dc7b4..7f0eb84a 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,11 @@ require ( github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d github.com/gin-contrib/cors v1.7.2 github.com/gin-gonic/gin v1.10.1 - github.com/go-gost/core v0.4.3 + github.com/go-gost/core v0.5.0 github.com/go-gost/go-shadowsocks2 v0.1.3 github.com/go-gost/gosocks4 v0.1.0 github.com/go-gost/gosocks5 v0.5.0 - github.com/go-gost/plugin v0.3.0 + github.com/go-gost/plugin v0.4.0 github.com/go-gost/relay v0.6.1 github.com/go-gost/tls-dissector v0.2.0 github.com/go-redis/redis/v8 v8.11.5 diff --git a/go.sum b/go.sum index b658f56c..b8d48e92 100644 --- a/go.sum +++ b/go.sum @@ -51,16 +51,16 @@ 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.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-gost/core v0.4.3 h1:9VEHRzuSq/MVdDZYrp/TtVG0+0r6NVGUQDXsJozrvJs= -github.com/go-gost/core v0.4.3/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A= +github.com/go-gost/core v0.5.0 h1:3SkPojjqw2Av9l5OSyRVlBpRvWhaXgU5BWMXB9aR4SM= +github.com/go-gost/core v0.5.0/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A= github.com/go-gost/go-shadowsocks2 v0.1.3 h1:6CUZLp+mTWXnKP2aK8/Z9ZP+ERMX9gSbywmPu4kGX/A= github.com/go-gost/go-shadowsocks2 v0.1.3/go.mod h1:866zFNNI3He6Wef1M/IvAjTal74WhcfKfBgRpTlkKys= github.com/go-gost/gosocks4 v0.1.0 h1:eAzev6qw4fzkFQKC9uCHLVNnnPdHyqCggbnfNN80Pmk= github.com/go-gost/gosocks4 v0.1.0/go.mod h1:hzVjwijJuZR1pp3GqpTj+AKcSGrx68RlWTrQMFMYBP0= github.com/go-gost/gosocks5 v0.5.0 h1:YE37l1MJwde8diIQdynStqogMotG5enoTdborhA5yic= github.com/go-gost/gosocks5 v0.5.0/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4= -github.com/go-gost/plugin v0.3.0 h1:pmll8nNd9PX92BWMB5+b2y2SEkBAWJLxD2ANfX+WHuw= -github.com/go-gost/plugin v0.3.0/go.mod h1:oN23l+yGDCIP9G3KnDl/I/0zVGOobZUDCB2Z5yYYXts= +github.com/go-gost/plugin v0.4.0 h1:M7MR5PL7QAFCrdWfcXVX+5iLNTVIZlhl8FfdV/K8dZY= +github.com/go-gost/plugin v0.4.0/go.mod h1:oN23l+yGDCIP9G3KnDl/I/0zVGOobZUDCB2Z5yYYXts= github.com/go-gost/relay v0.6.1 h1:7SqnHFbY8x/DzvjpK03a5zcVH9+TbJAcnW/RT6s1ecc= github.com/go-gost/relay v0.6.1/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI= github.com/go-gost/tls-dissector v0.2.0 h1:9tE6WOzzpurATTBWn60DU4R8gibpGNY8/qVcc1SicVg= diff --git a/registry/registry.go b/registry/registry.go index f63c54c6..7a694f7a 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -21,6 +21,7 @@ import ( reg "github.com/go-gost/core/registry" "github.com/go-gost/core/resolver" "github.com/go-gost/core/router" + "github.com/go-gost/core/rewriter" "github.com/go-gost/core/sd" "github.com/go-gost/core/service" "github.com/go-gost/x/limiter/quota" @@ -46,6 +47,7 @@ var ( resolverReg reg.Registry[resolver.Resolver] = new(resolverRegistry) hostsReg reg.Registry[hosts.HostMapper] = new(hostsRegistry) recorderReg reg.Registry[recorder.Recorder] = new(recorderRegistry) + rewriterReg reg.Registry[rewriter.Rewriter] = new(rewriterRegistry) trafficLimiterReg reg.Registry[traffic.TrafficLimiter] = new(trafficLimiterRegistry) connLimiterReg reg.Registry[conn.ConnLimiter] = new(connLimiterRegistry) @@ -187,6 +189,11 @@ func RecorderRegistry() reg.Registry[recorder.Recorder] { return recorderReg } +// RewriterRegistry returns the global registry of rewriter instances. +func RewriterRegistry() reg.Registry[rewriter.Rewriter] { + return rewriterReg +} + // TrafficLimiterRegistry returns the global registry of traffic limiter instances. func TrafficLimiterRegistry() reg.Registry[traffic.TrafficLimiter] { return trafficLimiterReg diff --git a/registry/rewriter.go b/registry/rewriter.go new file mode 100644 index 00000000..b7939ee8 --- /dev/null +++ b/registry/rewriter.go @@ -0,0 +1,43 @@ +package registry + +import ( + "context" + + "github.com/go-gost/core/rewriter" +) + +// rewriterRegistry implements a hot-reload-safe registry for rewriter.Rewriter. +type rewriterRegistry struct { + registry[rewriter.Rewriter] +} + +// Register stores a Rewriter under the given name. +func (r *rewriterRegistry) Register(name string, v rewriter.Rewriter) error { + return r.registry.Register(name, v) +} + +// Get returns a wrapper that delegates to the currently registered Rewriter. +// Returns nil if name is empty. +func (r *rewriterRegistry) Get(name string) rewriter.Rewriter { + if name != "" { + return &rewriterWrapper{name: name, r: r} + } + return nil +} + +func (r *rewriterRegistry) get(name string) rewriter.Rewriter { + return r.registry.Get(name) +} + +type rewriterWrapper struct { + name string + r *rewriterRegistry +} + +func (w *rewriterWrapper) Rewrite(ctx context.Context, b []byte, opts ...rewriter.RewriteOption) ([]byte, error) { + v := w.r.get(w.name) + if v == nil { + return b, nil + } + return v.Rewrite(ctx, b, opts...) +} diff --git a/rewriter/plugin/grpc.go b/rewriter/plugin/grpc.go new file mode 100644 index 00000000..dfb18464 --- /dev/null +++ b/rewriter/plugin/grpc.go @@ -0,0 +1,89 @@ +package rewriter + +import ( + "context" + "encoding/json" + "io" + + "github.com/go-gost/core/logger" + "github.com/go-gost/core/rewriter" + "github.com/go-gost/plugin/rewriter/proto" + "github.com/go-gost/x/internal/plugin" + "google.golang.org/grpc" +) + +type grpcPlugin struct { + conn grpc.ClientConnInterface + client proto.RewriterClient + log logger.Logger +} + +// NewGRPCPlugin creates a Rewriter plugin based on gRPC. +func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) rewriter.Rewriter { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "rewriter", + "rewriter": 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.NewRewriterClient(conn) + } + return p +} + +func (p *grpcPlugin) Rewrite(ctx context.Context, b []byte, opts ...rewriter.RewriteOption) ([]byte, error) { + if p.client == nil { + return b, nil + } + + var options rewriter.RewriteOptions + for _, opt := range opts { + opt(&options) + } + + md, err := json.Marshal(options.Metadata) + if err != nil { + return b, err + } + + reply, err := p.client.Rewrite(ctx, + &proto.RewriteRequest{ + Data: b, + Metadata: md, + }) + if err != nil { + p.log.Error(err) + return b, err + } + if reply == nil || !reply.Ok { + return b, nil + } + if reply.Data != nil { + return reply.Data, nil + } + return b, nil +} + +func (p *grpcPlugin) Close() error { + if p.conn == nil { + return nil + } + + if closer, ok := p.conn.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/rewriter/plugin/http.go b/rewriter/plugin/http.go new file mode 100644 index 00000000..ee49da29 --- /dev/null +++ b/rewriter/plugin/http.go @@ -0,0 +1,104 @@ +package rewriter + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + + "github.com/go-gost/core/logger" + "github.com/go-gost/core/rewriter" + "github.com/go-gost/x/internal/plugin" +) + +type httpPluginRequest struct { + Data []byte `json:"data"` + Metadata []byte `json:"metadata"` +} + +type httpPluginResponse struct { + OK bool `json:"ok"` + Data []byte `json:"data"` +} + +type httpPlugin struct { + url string + client *http.Client + header http.Header + log logger.Logger +} + +// NewHTTPPlugin creates a Rewriter plugin based on HTTP. +func NewHTTPPlugin(name string, url string, opts ...plugin.Option) rewriter.Rewriter { + 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": "rewriter", + "rewriter": name, + }), + } +} + +func (p *httpPlugin) Rewrite(ctx context.Context, b []byte, opts ...rewriter.RewriteOption) ([]byte, error) { + if p.client == nil { + return b, nil + } + + var options rewriter.RewriteOptions + for _, opt := range opts { + opt(&options) + } + + md, err := json.Marshal(options.Metadata) + if err != nil { + return b, err + } + + rb := httpPluginRequest{ + Data: b, + Metadata: md, + } + v, err := json.Marshal(&rb) + if err != nil { + return b, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.url, bytes.NewReader(v)) + if err != nil { + return b, 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 b, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return b, nil + } + + res := httpPluginResponse{} + if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { + return b, err + } + + if !res.OK { + return b, nil + } + if res.Data != nil { + return res.Data, nil + } + return b, nil +}