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
This commit is contained in:
ginuerzh
2026-06-27 21:44:13 +08:00
parent fb0cf72446
commit 3b25e0317f
10 changed files with 324 additions and 8 deletions
+7
View File
@@ -316,6 +316,11 @@ type RecorderObject struct {
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"` 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 { type LimiterConfig struct {
Name string `json:"name"` Name string `json:"name"`
Limits []string `yaml:",omitempty" json:"limits,omitempty"` Limits []string `yaml:",omitempty" json:"limits,omitempty"`
@@ -487,6 +492,7 @@ type ServiceConfig struct {
Logger string `yaml:",omitempty" json:"logger,omitempty"` Logger string `yaml:",omitempty" json:"logger,omitempty"`
Loggers []string `yaml:",omitempty" json:"loggers,omitempty"` Loggers []string `yaml:",omitempty" json:"loggers,omitempty"`
Observer string `yaml:",omitempty" json:"observer,omitempty"` Observer string `yaml:",omitempty" json:"observer,omitempty"`
Rewriter string `yaml:",omitempty" json:"rewriter,omitempty"`
Recorders []*RecorderObject `yaml:",omitempty" json:"recorders,omitempty"` Recorders []*RecorderObject `yaml:",omitempty" json:"recorders,omitempty"`
Handler *HandlerConfig `yaml:",omitempty" json:"handler,omitempty"` Handler *HandlerConfig `yaml:",omitempty" json:"handler,omitempty"`
Listener *ListenerConfig `yaml:",omitempty" json:"listener,omitempty"` Listener *ListenerConfig `yaml:",omitempty" json:"listener,omitempty"`
@@ -637,6 +643,7 @@ type Config struct {
Routers []*RouterConfig `yaml:",omitempty" json:"routers,omitempty"` Routers []*RouterConfig `yaml:",omitempty" json:"routers,omitempty"`
SDs []*SDConfig `yaml:"sds,omitempty" json:"sds,omitempty"` SDs []*SDConfig `yaml:"sds,omitempty" json:"sds,omitempty"`
Recorders []*RecorderConfig `yaml:",omitempty" json:"recorders,omitempty"` Recorders []*RecorderConfig `yaml:",omitempty" json:"recorders,omitempty"`
Rewriters []*RewriterConfig `yaml:",omitempty" json:"rewriters,omitempty"`
Limiters []*LimiterConfig `yaml:",omitempty" json:"limiters,omitempty"` Limiters []*LimiterConfig `yaml:",omitempty" json:"limiters,omitempty"`
Quotas []*QuotaConfig `yaml:",omitempty" json:"quotas,omitempty"` Quotas []*QuotaConfig `yaml:",omitempty" json:"quotas,omitempty"`
CLimiters []*LimiterConfig `yaml:"climiters,omitempty" json:"climiters,omitempty"` CLimiters []*LimiterConfig `yaml:"climiters,omitempty" json:"climiters,omitempty"`
+12
View File
@@ -22,6 +22,7 @@ import (
reg "github.com/go-gost/core/registry" reg "github.com/go-gost/core/registry"
"github.com/go-gost/core/resolver" "github.com/go-gost/core/resolver"
"github.com/go-gost/core/router" "github.com/go-gost/core/router"
"github.com/go-gost/core/rewriter"
"github.com/go-gost/core/sd" "github.com/go-gost/core/sd"
"github.com/go-gost/x/config" "github.com/go-gost/x/config"
"github.com/go-gost/x/config/parsing" "github.com/go-gost/x/config/parsing"
@@ -38,6 +39,7 @@ import (
quota_parser "github.com/go-gost/x/config/parsing/quota" quota_parser "github.com/go-gost/x/config/parsing/quota"
recorder_parser "github.com/go-gost/x/config/parsing/recorder" recorder_parser "github.com/go-gost/x/config/parsing/recorder"
resolver_parser "github.com/go-gost/x/config/parsing/resolver" 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" router_parser "github.com/go-gost/x/config/parsing/router"
sd_parser "github.com/go-gost/x/config/parsing/sd" sd_parser "github.com/go-gost/x/config/parsing/sd"
service_parser "github.com/go-gost/x/config/parsing/service" 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] var entries []named[traffic.TrafficLimiter]
for _, c := range cfg.Limiters { for _, c := range cfg.Limiters {
+47
View File
@@ -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
}
+7
View File
@@ -10,6 +10,7 @@ import (
"github.com/go-gost/core/chain" "github.com/go-gost/core/chain"
"github.com/go-gost/core/handler" "github.com/go-gost/core/handler"
"github.com/go-gost/core/hop" "github.com/go-gost/core/hop"
"github.com/go-gost/core/rewriter"
"github.com/go-gost/core/listener" "github.com/go-gost/core/listener"
"github.com/go-gost/core/logger" "github.com/go-gost/core/logger"
"github.com/go-gost/core/observer/stats" "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{ routerOpts = []chain.RouterOption{
chain.RetriesRouterOption(cfg.Handler.Retries), chain.RetriesRouterOption(cfg.Handler.Retries),
chain.TimeoutRouterOption(dialTimeout), chain.TimeoutRouterOption(dialTimeout),
@@ -382,6 +388,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
handler.TrafficLimiterOption(registry.TrafficLimiterRegistry().Get(cfg.Handler.Limiter)), handler.TrafficLimiterOption(registry.TrafficLimiterRegistry().Get(cfg.Handler.Limiter)),
handler.ObserverOption(registry.ObserverRegistry().Get(cfg.Handler.Observer)), handler.ObserverOption(registry.ObserverRegistry().Get(cfg.Handler.Observer)),
handler.RecordersOption(recorders...), handler.RecordersOption(recorders...),
handler.RewriterOption(rew),
handler.LoggerOption(handlerLogger), handler.LoggerOption(handlerLogger),
handler.ServiceOption(cfg.Name), handler.ServiceOption(cfg.Name),
handler.NetnsOption(netnsIn), handler.NetnsOption(netnsIn),
+2 -2
View File
@@ -7,11 +7,11 @@ require (
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
github.com/gin-contrib/cors v1.7.2 github.com/gin-contrib/cors v1.7.2
github.com/gin-gonic/gin v1.10.1 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/go-shadowsocks2 v0.1.3
github.com/go-gost/gosocks4 v0.1.0 github.com/go-gost/gosocks4 v0.1.0
github.com/go-gost/gosocks5 v0.5.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/relay v0.6.1
github.com/go-gost/tls-dissector v0.2.0 github.com/go-gost/tls-dissector v0.2.0
github.com/go-redis/redis/v8 v8.11.5 github.com/go-redis/redis/v8 v8.11.5
+4 -4
View File
@@ -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-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 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 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.5.0 h1:3SkPojjqw2Av9l5OSyRVlBpRvWhaXgU5BWMXB9aR4SM=
github.com/go-gost/core v0.4.3/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A= 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 h1:6CUZLp+mTWXnKP2aK8/Z9ZP+ERMX9gSbywmPu4kGX/A=
github.com/go-gost/go-shadowsocks2 v0.1.3/go.mod h1:866zFNNI3He6Wef1M/IvAjTal74WhcfKfBgRpTlkKys= 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 h1:eAzev6qw4fzkFQKC9uCHLVNnnPdHyqCggbnfNN80Pmk=
github.com/go-gost/gosocks4 v0.1.0/go.mod h1:hzVjwijJuZR1pp3GqpTj+AKcSGrx68RlWTrQMFMYBP0= 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 h1:YE37l1MJwde8diIQdynStqogMotG5enoTdborhA5yic=
github.com/go-gost/gosocks5 v0.5.0/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4= 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.4.0 h1:M7MR5PL7QAFCrdWfcXVX+5iLNTVIZlhl8FfdV/K8dZY=
github.com/go-gost/plugin v0.3.0/go.mod h1:oN23l+yGDCIP9G3KnDl/I/0zVGOobZUDCB2Z5yYYXts= 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 h1:7SqnHFbY8x/DzvjpK03a5zcVH9+TbJAcnW/RT6s1ecc=
github.com/go-gost/relay v0.6.1/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI= github.com/go-gost/relay v0.6.1/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI=
github.com/go-gost/tls-dissector v0.2.0 h1:9tE6WOzzpurATTBWn60DU4R8gibpGNY8/qVcc1SicVg= github.com/go-gost/tls-dissector v0.2.0 h1:9tE6WOzzpurATTBWn60DU4R8gibpGNY8/qVcc1SicVg=
+7
View File
@@ -21,6 +21,7 @@ import (
reg "github.com/go-gost/core/registry" reg "github.com/go-gost/core/registry"
"github.com/go-gost/core/resolver" "github.com/go-gost/core/resolver"
"github.com/go-gost/core/router" "github.com/go-gost/core/router"
"github.com/go-gost/core/rewriter"
"github.com/go-gost/core/sd" "github.com/go-gost/core/sd"
"github.com/go-gost/core/service" "github.com/go-gost/core/service"
"github.com/go-gost/x/limiter/quota" "github.com/go-gost/x/limiter/quota"
@@ -46,6 +47,7 @@ var (
resolverReg reg.Registry[resolver.Resolver] = new(resolverRegistry) resolverReg reg.Registry[resolver.Resolver] = new(resolverRegistry)
hostsReg reg.Registry[hosts.HostMapper] = new(hostsRegistry) hostsReg reg.Registry[hosts.HostMapper] = new(hostsRegistry)
recorderReg reg.Registry[recorder.Recorder] = new(recorderRegistry) recorderReg reg.Registry[recorder.Recorder] = new(recorderRegistry)
rewriterReg reg.Registry[rewriter.Rewriter] = new(rewriterRegistry)
trafficLimiterReg reg.Registry[traffic.TrafficLimiter] = new(trafficLimiterRegistry) trafficLimiterReg reg.Registry[traffic.TrafficLimiter] = new(trafficLimiterRegistry)
connLimiterReg reg.Registry[conn.ConnLimiter] = new(connLimiterRegistry) connLimiterReg reg.Registry[conn.ConnLimiter] = new(connLimiterRegistry)
@@ -187,6 +189,11 @@ func RecorderRegistry() reg.Registry[recorder.Recorder] {
return recorderReg 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. // TrafficLimiterRegistry returns the global registry of traffic limiter instances.
func TrafficLimiterRegistry() reg.Registry[traffic.TrafficLimiter] { func TrafficLimiterRegistry() reg.Registry[traffic.TrafficLimiter] {
return trafficLimiterReg return trafficLimiterReg
+43
View File
@@ -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...)
}
+89
View File
@@ -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
}
+104
View File
@@ -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
}