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:
+9
-2
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user