diff --git a/infra/conf/plugin.go b/infra/conf/plugin.go new file mode 100644 index 000000000000..1c533892d7ec --- /dev/null +++ b/infra/conf/plugin.go @@ -0,0 +1,24 @@ +package conf + +import ( + "encoding/json" + + "github.com/xtls/xray-core/proxy/plugin" + "google.golang.org/protobuf/proto" +) + +type PluginOutboundConfig struct { + Name string `json:"name"` + Params *json.RawMessage `json:"params"` +} + +func (c *PluginOutboundConfig) Build() (proto.Message, error) { + var paramsBytes []byte + if c.Params != nil { + paramsBytes = []byte(*c.Params) + } + return &plugin.ClientConfig{ + Name: c.Name, + Params: paramsBytes, + }, nil +} diff --git a/infra/conf/xray.go b/infra/conf/xray.go index 3e6e4371036e..63f1406607ff 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -48,6 +48,7 @@ var ( "hysteria": func() interface{} { return new(HysteriaClientConfig) }, "dns": func() interface{} { return new(DNSOutboundConfig) }, "wireguard": func() interface{} { return &WireGuardConfig{IsClient: true} }, + "plugin": func() interface{} { return new(PluginOutboundConfig) }, }, "protocol", "settings") ) diff --git a/proxy/plugin/client.go b/proxy/plugin/client.go new file mode 100644 index 000000000000..e1dab6105a3b --- /dev/null +++ b/proxy/plugin/client.go @@ -0,0 +1,123 @@ +package plugin + +import ( + "context" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/stats" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" +) + +type Client struct { + name string + params []byte +} + +func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + var tag string + if handler := session.FullHandlerFromContext(ctx); handler != nil { + tag = handler.Tag() + } + TriggerOnPluginRegistered(tag, config.Name, config.Params) + + return &Client{ + name: config.Name, + params: config.Params, + }, nil +} + +type sizeStatReader struct { + buf.Reader + counter stats.Counter +} + +func (r *sizeStatReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + mb, err := r.Reader.ReadMultiBuffer() + if r.counter != nil { + r.counter.Add(int64(mb.Len())) + } + return mb, err +} + +type sizeStatWriter struct { + buf.Writer + counter stats.Counter +} + +func (w *sizeStatWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + if w.counter != nil { + w.counter.Add(int64(mb.Len())) + } + return w.Writer.WriteMultiBuffer(mb) +} + +func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if !ob.Target.IsValid() { + return errors.New("target not specified.") + } + destination := ob.Target + + handlerFunc := GetHandler(c.name) + if handlerFunc == nil { + return errors.New("plugin outbound handler not registered: ", c.name) + } + + var tag string + if len(outbounds) > 0 { + tag = outbounds[len(outbounds)-1].Tag + } + if len(tag) > 0 { + if v := core.FromContext(ctx); v != nil { + if pmFeature := v.GetFeature(policy.ManagerType()); pmFeature != nil { + if pm, ok := pmFeature.(policy.Manager); ok { + if smFeature := v.GetFeature(stats.ManagerType()); smFeature != nil { + if sm, ok := smFeature.(stats.Manager); ok { + var uplinkCounter stats.Counter + var downlinkCounter stats.Counter + if pm.ForSystem().Stats.OutboundUplink { + name := "outbound>>>" + tag + ">>>traffic>>>uplink" + if c, err := stats.GetOrRegisterCounter(sm, name); err == nil && c != nil { + uplinkCounter = c + } + } + if pm.ForSystem().Stats.OutboundDownlink { + name := "outbound>>>" + tag + ">>>traffic>>>downlink" + if c, err := stats.GetOrRegisterCounter(sm, name); err == nil && c != nil { + downlinkCounter = c + } + } + if uplinkCounter != nil { + link.Reader = &sizeStatReader{ + Reader: link.Reader, + counter: uplinkCounter, + } + } + if downlinkCounter != nil { + link.Writer = &sizeStatWriter{ + Writer: link.Writer, + counter: downlinkCounter, + } + } + } + } + } + } + } + } + + return handlerFunc(ctx, destination, link) +} + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} diff --git a/proxy/plugin/config.pb.go b/proxy/plugin/config.pb.go new file mode 100644 index 000000000000..109e31f1eafa --- /dev/null +++ b/proxy/plugin/config.pb.go @@ -0,0 +1,131 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/plugin/config.proto + +package plugin + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ClientConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Params []byte `protobuf:"bytes,2,opt,name=params,proto3" json:"params,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + mi := &file_proxy_plugin_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_plugin_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_plugin_config_proto_rawDescGZIP(), []int{0} +} + +func (x *ClientConfig) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ClientConfig) GetParams() []byte { + if x != nil { + return x.Params + } + return nil +} + +var File_proxy_plugin_config_proto protoreflect.FileDescriptor + +const file_proxy_plugin_config_proto_rawDesc = "" + + "\n" + + "\x19proxy/plugin/config.proto\x12\x11xray.proxy.plugin\":\n" + + "\fClientConfig\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" + + "\x06params\x18\x02 \x01(\fR\x06paramsB(Z&github.com/xtls/xray-core/proxy/pluginb\x06proto3" + +var ( + file_proxy_plugin_config_proto_rawDescOnce sync.Once + file_proxy_plugin_config_proto_rawDescData []byte +) + +func file_proxy_plugin_config_proto_rawDescGZIP() []byte { + file_proxy_plugin_config_proto_rawDescOnce.Do(func() { + file_proxy_plugin_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_plugin_config_proto_rawDesc), len(file_proxy_plugin_config_proto_rawDesc))) + }) + return file_proxy_plugin_config_proto_rawDescData +} + +var file_proxy_plugin_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_plugin_config_proto_goTypes = []any{ + (*ClientConfig)(nil), // 0: xray.proxy.plugin.ClientConfig +} +var file_proxy_plugin_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_proxy_plugin_config_proto_init() } +func file_proxy_plugin_config_proto_init() { + if File_proxy_plugin_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_plugin_config_proto_rawDesc), len(file_proxy_plugin_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_plugin_config_proto_goTypes, + DependencyIndexes: file_proxy_plugin_config_proto_depIdxs, + MessageInfos: file_proxy_plugin_config_proto_msgTypes, + }.Build() + File_proxy_plugin_config_proto = out.File + file_proxy_plugin_config_proto_goTypes = nil + file_proxy_plugin_config_proto_depIdxs = nil +} diff --git a/proxy/plugin/config.proto b/proxy/plugin/config.proto new file mode 100644 index 000000000000..ce9f3ef0d78f --- /dev/null +++ b/proxy/plugin/config.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package xray.proxy.plugin; +option go_package = "github.com/xtls/xray-core/proxy/plugin"; + +message ClientConfig { + string name = 1; + bytes params = 2; +} diff --git a/proxy/plugin/plugin.go b/proxy/plugin/plugin.go new file mode 100644 index 000000000000..42f59021e8de --- /dev/null +++ b/proxy/plugin/plugin.go @@ -0,0 +1,48 @@ +package plugin + +import ( + "context" + "sync" + + v2net "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport" +) + +type OutboundHandlerFunc func(ctx context.Context, dest v2net.Destination, link *transport.Link) error + +type OnPluginRegisteredFunc func(tag string, name string, params []byte) + +var ( + handlersMu sync.RWMutex + handlers = make(map[string]OutboundHandlerFunc) + + onPluginRegisteredMu sync.Mutex + onPluginRegistered OnPluginRegisteredFunc +) + +func RegisterHandler(name string, handler OutboundHandlerFunc) { + handlersMu.Lock() + defer handlersMu.Unlock() + handlers[name] = handler +} + +func GetHandler(name string) OutboundHandlerFunc { + handlersMu.RLock() + defer handlersMu.RUnlock() + return handlers[name] +} + +func SetOnPluginRegistered(cb OnPluginRegisteredFunc) { + onPluginRegisteredMu.Lock() + defer onPluginRegisteredMu.Unlock() + onPluginRegistered = cb +} + +func TriggerOnPluginRegistered(tag string, name string, params []byte) { + onPluginRegisteredMu.Lock() + cb := onPluginRegistered + onPluginRegisteredMu.Unlock() + if cb != nil { + cb(tag, name, params) + } +}