diff --git a/app/observatory/command/command.pb.go b/app/observatory/command/command.pb.go index bdbe6d2b7..28852b94b 100644 --- a/app/observatory/command/command.pb.go +++ b/app/observatory/command/command.pb.go @@ -2,6 +2,7 @@ package command import ( observatory "github.com/v2fly/v2ray-core/v5/app/observatory" + _ "github.com/v2fly/v2ray-core/v5/common/protoext" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -154,39 +155,43 @@ var file_app_observatory_command_command_proto_rawDesc = []byte{ 0x79, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x22, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, - 0x6f, 0x72, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x1a, 0x1c, 0x61, 0x70, 0x70, - 0x2f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x2f, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2c, 0x0a, 0x18, 0x47, 0x65, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x1a, 0x20, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x65, 0x78, 0x74, 0x2f, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x61, + 0x70, 0x70, 0x2f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x2f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2c, 0x0a, 0x18, 0x47, + 0x65, 0x74, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x61, 0x67, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x54, 0x61, 0x67, 0x22, 0x62, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x54, 0x61, 0x67, 0x22, 0x62, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x4f, 0x75, - 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, + 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x28, 0x0a, + 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x3a, 0x1e, 0x82, 0xb5, 0x18, 0x1a, 0x0a, 0x0b, 0x67, + 0x72, 0x70, 0x63, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x0b, 0x6f, 0x62, 0x73, 0x65, + 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x32, 0xa9, 0x01, 0x0a, 0x12, 0x4f, 0x62, 0x73, 0x65, + 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x92, + 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x3c, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, - 0x79, 0x2e, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x08, 0x0a, 0x06, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x32, 0xa9, 0x01, 0x0a, 0x12, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, - 0x61, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x92, 0x01, 0x0a, - 0x11, 0x47, 0x65, 0x74, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x3c, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, + 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x75, 0x74, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x3d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x75, 0x74, 0x62, 0x6f, - 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x3d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, - 0x70, 0x2e, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, - 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x42, 0x87, 0x01, 0x0a, 0x26, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, - 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, - 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x36, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79, - 0x2f, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x61, - 0x70, 0x70, 0x2f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x2f, 0x63, - 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x22, 0x56, 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43, - 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, - 0x6f, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x42, 0x87, 0x01, 0x0a, 0x26, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, + 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6f, 0x62, 0x73, 0x65, 0x72, + 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x01, + 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66, + 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x35, + 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, + 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x22, 0x56, 0x32, 0x52, 0x61, 0x79, + 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, + 0x61, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/app/observatory/command/command.proto b/app/observatory/command/command.proto index e5ff38bb6..b9291f632 100644 --- a/app/observatory/command/command.proto +++ b/app/observatory/command/command.proto @@ -6,6 +6,7 @@ option go_package = "github.com/v2fly/v2ray-core/v5/app/observatory/command"; option java_package = "com.v2ray.core.app.observatory.command"; option java_multiple_files = true; +import "common/protoext/extensions.proto"; import "app/observatory/config.proto"; message GetOutboundStatusRequest { @@ -22,4 +23,7 @@ service ObservatoryService { } -message Config {} \ No newline at end of file +message Config { + option (v2ray.core.common.protoext.message_opt).type = "grpcservice"; + option (v2ray.core.common.protoext.message_opt).short_name = "observatory"; +} \ No newline at end of file diff --git a/app/observatory/observer.go b/app/observatory/observer.go index 690d3c525..e0912074e 100644 --- a/app/observatory/observer.go +++ b/app/observatory/observer.go @@ -73,6 +73,7 @@ func (o *Observer) background() { o.updateStatus(outbounds) + slept := false for _, v := range outbounds { result := o.probe(v) o.updateStatusForResult(v, &result) @@ -84,6 +85,14 @@ func (o *Observer) background() { sleepTime = time.Duration(o.config.ProbeInterval) } time.Sleep(sleepTime) + slept = true + } + if !slept { + sleepTime := time.Second * 10 + if o.config.ProbeInterval != 0 { + sleepTime = time.Duration(o.config.ProbeInterval) + } + time.Sleep(sleepTime) } } } diff --git a/app/proxyman/command/command.go b/app/proxyman/command/command.go index fdc013839..61758be3d 100644 --- a/app/proxyman/command/command.go +++ b/app/proxyman/command/command.go @@ -107,7 +107,7 @@ func (s *handlerServer) AddOutbound(ctx context.Context, request *AddOutboundReq } func (s *handlerServer) RemoveOutbound(ctx context.Context, request *RemoveOutboundRequest) (*RemoveOutboundResponse, error) { - return &RemoveOutboundResponse{}, s.ohm.RemoveHandler(ctx, request.Tag) + return &RemoveOutboundResponse{}, core.RemoveOutboundHandler(s.s, request.Tag) } func (s *handlerServer) AlterOutbound(ctx context.Context, request *AlterOutboundRequest) (*AlterOutboundResponse, error) { diff --git a/app/proxyman/outbound/handler.go b/app/proxyman/outbound/handler.go index 71e66c2dd..7febd5c20 100644 --- a/app/proxyman/outbound/handler.go +++ b/app/proxyman/outbound/handler.go @@ -317,5 +317,11 @@ func (h *Handler) Start() error { // Close implements common.Closable. func (h *Handler) Close() error { common.Close(h.mux) + + if closableProxy, ok := h.proxy.(common.Closable); ok { + if err := closableProxy.Close(); err != nil { + return newError("unable to close proxy").Base(err) + } + } return nil } diff --git a/app/proxyman/outbound/outbound.go b/app/proxyman/outbound/outbound.go index 300a535fe..a612bf936 100644 --- a/app/proxyman/outbound/outbound.go +++ b/app/proxyman/outbound/outbound.go @@ -11,6 +11,7 @@ import ( "github.com/v2fly/v2ray-core/v5/app/proxyman" "github.com/v2fly/v2ray-core/v5/common" "github.com/v2fly/v2ray-core/v5/common/errors" + "github.com/v2fly/v2ray-core/v5/common/session" "github.com/v2fly/v2ray-core/v5/features/outbound" ) @@ -131,12 +132,18 @@ func (m *Manager) RemoveHandler(ctx context.Context, tag string) error { m.access.Lock() defer m.access.Unlock() - delete(m.taggedHandler, tag) - if m.defaultHandler != nil && m.defaultHandler.Tag() == tag { - m.defaultHandler = nil + if handler, found := m.taggedHandler[tag]; found { + if err := handler.Close(); err != nil { + newError("failed to close handler ", tag).Base(err).AtWarning().WriteToLog(session.ExportIDToError(ctx)) + } + delete(m.taggedHandler, tag) + if m.defaultHandler != nil && m.defaultHandler.Tag() == tag { + m.defaultHandler = nil + } + return nil } - return nil + return common.ErrNoClue } // Select implements outbound.HandlerSelector. diff --git a/app/subscription/config.proto b/app/subscription/config.proto new file mode 100644 index 000000000..71dd51a1e --- /dev/null +++ b/app/subscription/config.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package v2ray.core.app.subscription; + +option csharp_namespace = "V2Ray.Core.App.Subscription"; +option go_package = "github.com/v2fly/v2ray-core/v5/app/subscription"; +option java_package = "com.v2ray.core.app.subscription"; +option java_multiple_files = true; + +import "common/protoext/extensions.proto"; + +message ImportSource { + string name = 1; + string url = 2; + string tag_prefix = 3; + + string import_network_tag = 4; + + uint64 default_expire_seconds = 5; +} + +// Config is the settings for Subscription Manager. +message Config { + option (v2ray.core.common.protoext.message_opt).type = "service"; + option (v2ray.core.common.protoext.message_opt).short_name = "subscription"; + + repeated ImportSource imports = 1; + + bytes nonnative_converter_overlay = 2; + string nonnative_converter_overlay_file = 96002 [(v2ray.core.common.protoext.field_opt).convert_time_read_file_into = "nonnative_converter_overlay"]; +} \ No newline at end of file diff --git a/app/subscription/containers/base64urlline/base64urlline.go b/app/subscription/containers/base64urlline/base64urlline.go new file mode 100644 index 000000000..97b2fbf52 --- /dev/null +++ b/app/subscription/containers/base64urlline/base64urlline.go @@ -0,0 +1,3 @@ +package base64urlline + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen diff --git a/app/subscription/containers/base64urlline/parser.go b/app/subscription/containers/base64urlline/parser.go new file mode 100644 index 000000000..44b7186e4 --- /dev/null +++ b/app/subscription/containers/base64urlline/parser.go @@ -0,0 +1,46 @@ +package base64urlline + +import ( + "bufio" + "bytes" + "encoding/base64" + "io" + + "github.com/v2fly/v2ray-core/v5/app/subscription/containers" + "github.com/v2fly/v2ray-core/v5/common" +) + +func newBase64URLLineParser() containers.SubscriptionContainerDocumentParser { + return &parser{} +} + +type parser struct{} + +func (p parser) ParseSubscriptionContainerDocument(rawConfig []byte) (*containers.Container, error) { + result := &containers.Container{} + result.Kind = "Base64URLLine" + result.Metadata = make(map[string]string) + + bodyDecoder := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(rawConfig)) + decoded, err := io.ReadAll(bodyDecoder) + if err != nil { + return nil, newError("failed to decode base64url body base64").Base(err) + } + scanner := bufio.NewScanner(bytes.NewReader(decoded)) + + const maxCapacity int = 1024 * 256 + buf := make([]byte, maxCapacity) + scanner.Buffer(buf, maxCapacity) + + for scanner.Scan() { + result.ServerSpecs = append(result.ServerSpecs, containers.UnparsedServerConf{ + KindHint: "URL", + Content: scanner.Bytes(), + }) + } + return result, nil +} + +func init() { + common.Must(containers.RegisterParser("Base64URLLine", newBase64URLLineParser())) +} diff --git a/app/subscription/containers/containers.go b/app/subscription/containers/containers.go new file mode 100644 index 000000000..5a1b4097c --- /dev/null +++ b/app/subscription/containers/containers.go @@ -0,0 +1,28 @@ +package containers + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen + +type UnparsedServerConf struct { + KindHint string + Content []byte +} + +type Container struct { + Kind string + Metadata map[string]string + ServerSpecs []UnparsedServerConf +} + +type SubscriptionContainerDocumentParser interface { + ParseSubscriptionContainerDocument(rawConfig []byte) (*Container, error) +} + +var knownParsers = make(map[string]SubscriptionContainerDocumentParser) + +func RegisterParser(kind string, parser SubscriptionContainerDocumentParser) error { + if _, found := knownParsers[kind]; found { + return newError("parser already registered for kind ", kind) + } + knownParsers[kind] = parser + return nil +} diff --git a/app/subscription/containers/jsonfieldarray/jsonfieldarray.go b/app/subscription/containers/jsonfieldarray/jsonfieldarray.go new file mode 100644 index 000000000..52270eeae --- /dev/null +++ b/app/subscription/containers/jsonfieldarray/jsonfieldarray.go @@ -0,0 +1,3 @@ +package jsonfieldarray + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen diff --git a/app/subscription/containers/jsonfieldarray/jsonified/jsonified.go b/app/subscription/containers/jsonfieldarray/jsonified/jsonified.go new file mode 100644 index 000000000..313ffc9d7 --- /dev/null +++ b/app/subscription/containers/jsonfieldarray/jsonified/jsonified.go @@ -0,0 +1,3 @@ +package jsonified + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen diff --git a/app/subscription/containers/jsonfieldarray/jsonified/parser.go b/app/subscription/containers/jsonfieldarray/jsonified/parser.go new file mode 100644 index 000000000..b713899e9 --- /dev/null +++ b/app/subscription/containers/jsonfieldarray/jsonified/parser.go @@ -0,0 +1,36 @@ +package jsonified + +import ( + "github.com/v2fly/v2ray-core/v5/app/subscription/containers" + "github.com/v2fly/v2ray-core/v5/app/subscription/containers/jsonfieldarray" + "github.com/v2fly/v2ray-core/v5/common" + jsonConf "github.com/v2fly/v2ray-core/v5/infra/conf/json" +) + +func newJsonifiedYamlParser() containers.SubscriptionContainerDocumentParser { + return &jsonifiedYAMLParser{} +} + +type jsonifiedYAMLParser struct{} + +func (j jsonifiedYAMLParser) ParseSubscriptionContainerDocument(rawConfig []byte) (*containers.Container, error) { + parser := jsonfieldarray.NewJSONFieldArrayParser() + jsonified, err := jsonConf.FromYAML(rawConfig) + if err != nil { + return nil, newError("failed to parse as yaml").Base(err) + } + container, err := parser.ParseSubscriptionContainerDocument(jsonified) + if err != nil { + return nil, newError("failed to parse as jsonfieldarray").Base(err) + } + container.Kind = "Yaml2Json+" + container.Kind + + for _, value := range container.ServerSpecs { + value.KindHint = "Yaml2Json+" + value.KindHint + } + return container, nil +} + +func init() { + common.Must(containers.RegisterParser("Yaml2Json", newJsonifiedYamlParser())) +} diff --git a/app/subscription/containers/jsonfieldarray/parser.go b/app/subscription/containers/jsonfieldarray/parser.go new file mode 100644 index 000000000..1fadf001d --- /dev/null +++ b/app/subscription/containers/jsonfieldarray/parser.go @@ -0,0 +1,68 @@ +package jsonfieldarray + +import ( + "encoding/json" + + "github.com/v2fly/v2ray-core/v5/app/subscription/containers" + "github.com/v2fly/v2ray-core/v5/common" +) + +// NewJSONFieldArrayParser internal api +func NewJSONFieldArrayParser() containers.SubscriptionContainerDocumentParser { + return newJSONFieldArrayParser() +} + +func newJSONFieldArrayParser() containers.SubscriptionContainerDocumentParser { + return &parser{} +} + +type parser struct{} + +type jsonDocument map[string]json.RawMessage + +func (p parser) ParseSubscriptionContainerDocument(rawConfig []byte) (*containers.Container, error) { + result := &containers.Container{} + result.Kind = "JsonFieldArray" + result.Metadata = make(map[string]string) + + var doc jsonDocument + if err := json.Unmarshal(rawConfig, &doc); err != nil { + return nil, newError("failed to parse as json").Base(err) + } + + for key, value := range doc { + switch value[0] { + case '[': + parsedArray, err := p.parseArray(value, "JsonFieldArray+"+key) + if err != nil { + return nil, newError("failed to parse as json array").Base(err) + } + result.ServerSpecs = append(result.ServerSpecs, parsedArray...) + case '{': + fallthrough + default: + result.Metadata[key] = string(value) + } + } + + return result, nil +} + +func (p parser) parseArray(rawConfig []byte, kindHint string) ([]containers.UnparsedServerConf, error) { + var result []json.RawMessage + if err := json.Unmarshal(rawConfig, &result); err != nil { + return nil, newError("failed to parse as json array").Base(err) + } + var ret []containers.UnparsedServerConf + for _, value := range result { + ret = append(ret, containers.UnparsedServerConf{ + KindHint: kindHint, + Content: []byte(value), + }) + } + return ret, nil +} + +func init() { + common.Must(containers.RegisterParser("JsonFieldArray", newJSONFieldArrayParser())) +} diff --git a/app/subscription/containers/tryall.go b/app/subscription/containers/tryall.go new file mode 100644 index 000000000..22ded5f41 --- /dev/null +++ b/app/subscription/containers/tryall.go @@ -0,0 +1,20 @@ +package containers + +func TryAllParsers(rawConfig []byte, prioritizedParser string) (*Container, error) { + if prioritizedParser != "" { + if parser, found := knownParsers[prioritizedParser]; found { + container, err := parser.ParseSubscriptionContainerDocument(rawConfig) + if err == nil { + return container, nil + } + } + } + + for _, parser := range knownParsers { + container, err := parser.ParseSubscriptionContainerDocument(rawConfig) + if err == nil { + return container, nil + } + } + return nil, newError("no parser found for config") +} diff --git a/app/subscription/documentfetcher/fetcher.go b/app/subscription/documentfetcher/fetcher.go new file mode 100644 index 000000000..8f17b5eaf --- /dev/null +++ b/app/subscription/documentfetcher/fetcher.go @@ -0,0 +1,32 @@ +package documentfetcher + +import ( + "context" + + "github.com/v2fly/v2ray-core/v5/app/subscription" +) + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen + +type FetcherOptions interface{} + +type Fetcher interface { + DownloadDocument(ctx context.Context, source *subscription.ImportSource, opts ...FetcherOptions) ([]byte, error) +} + +var knownFetcher = make(map[string]Fetcher) + +func RegisterFetcher(name string, fetcher Fetcher) error { + if _, found := knownFetcher[name]; found { + return newError("fetcher ", name, " already registered") + } + knownFetcher[name] = fetcher + return nil +} + +func GetFetcher(name string) (Fetcher, error) { + if fetcher, found := knownFetcher[name]; found { + return fetcher, nil + } + return nil, newError("fetcher ", name, " not found") +} diff --git a/app/subscription/documentfetcher/httpfetcher/http.go b/app/subscription/documentfetcher/httpfetcher/http.go new file mode 100644 index 000000000..6da0b65c9 --- /dev/null +++ b/app/subscription/documentfetcher/httpfetcher/http.go @@ -0,0 +1,60 @@ +package httpfetcher + +import ( + "context" + "io" + gonet "net" + "net/http" + + "github.com/v2fly/v2ray-core/v5/common" + "github.com/v2fly/v2ray-core/v5/common/net" + + "github.com/v2fly/v2ray-core/v5/app/subscription" + "github.com/v2fly/v2ray-core/v5/app/subscription/documentfetcher" + "github.com/v2fly/v2ray-core/v5/common/environment" + "github.com/v2fly/v2ray-core/v5/common/environment/envctx" +) + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen + +func newHTTPFetcher() *httpFetcher { + return &httpFetcher{} +} + +func init() { + common.Must(documentfetcher.RegisterFetcher("http", newHTTPFetcher())) +} + +type httpFetcher struct{} + +func (h *httpFetcher) DownloadDocument(ctx context.Context, source *subscription.ImportSource, opts ...documentfetcher.FetcherOptions) ([]byte, error) { + instanceNetwork := envctx.EnvironmentFromContext(ctx).(environment.InstanceNetworkCapabilitySet) + outboundDialer := instanceNetwork.OutboundDialer() + var httpRoundTripper http.RoundTripper //nolint: gosimple + httpRoundTripper = &http.Transport{ + DialContext: func(ctx_ context.Context, network string, addr string) (gonet.Conn, error) { + dest, err := net.ParseDestination(network + ":" + addr) + if err != nil { + return nil, newError("unable to parse destination") + } + return outboundDialer(ctx, dest, source.ImportNetworkTag) + }, + } + request, err := http.NewRequest("GET", source.Url, nil) + if err != nil { + return nil, newError("unable to generate request").Base(err) + } + resp, err := httpRoundTripper.RoundTrip(request) + if err != nil { + return nil, newError("unable to send request").Base(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, newError("unexpected http status ", resp.StatusCode, "=", resp.Status) + } + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, newError("unable to read response").Base(err) + } + return data, nil +} diff --git a/app/subscription/entries/entries.go b/app/subscription/entries/entries.go new file mode 100644 index 000000000..5d5b5bc5f --- /dev/null +++ b/app/subscription/entries/entries.go @@ -0,0 +1,9 @@ +package entries + +import "github.com/v2fly/v2ray-core/v5/app/subscription/specs" + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen + +type Converter interface { + ConvertToAbstractServerConfig(rawConfig []byte, kindHint string) (*specs.SubscriptionServerConfig, error) +} diff --git a/app/subscription/entries/nonnative/converter.go b/app/subscription/entries/nonnative/converter.go new file mode 100644 index 000000000..ab2b2b9c3 --- /dev/null +++ b/app/subscription/entries/nonnative/converter.go @@ -0,0 +1,51 @@ +package nonnative + +import ( + "io/fs" + + "github.com/v2fly/v2ray-core/v5/app/subscription/entries" + "github.com/v2fly/v2ray-core/v5/app/subscription/entries/nonnative/nonnativeifce" + "github.com/v2fly/v2ray-core/v5/app/subscription/entries/outbound" + "github.com/v2fly/v2ray-core/v5/app/subscription/specs" + "github.com/v2fly/v2ray-core/v5/common" +) + +type nonNativeConverter struct { + matcher *DefMatcher +} + +func (n *nonNativeConverter) ConvertToAbstractServerConfig(rawConfig []byte, kindHint string) (*specs.SubscriptionServerConfig, error) { + nonNativeLink := ExtractAllValuesFromBytes(rawConfig) + nonNativeLink.Values["_kind"] = kindHint + result, err := n.matcher.ExecuteAll(nonNativeLink) + if err != nil { + return nil, newError("failed to find working converting template").Base(err) + } + outboundParser := outbound.NewOutboundEntriesParser() + outboundEntries, err := outboundParser.ConvertToAbstractServerConfig(result, "") + if err != nil { + return nil, newError("failed to parse template output as outbound entries").Base(err) + } + return outboundEntries, nil +} + +func NewNonNativeConverter(fs fs.FS) (entries.Converter, error) { + matcher := NewDefMatcher() + if fs == nil { + err := matcher.LoadEmbeddedDefinitions() + if err != nil { + return nil, newError("failed to load embedded definitions").Base(err) + } + } else { + err := matcher.LoadDefinitions(fs) + if err != nil { + return nil, newError("failed to load provided definitions").Base(err) + } + } + return &nonNativeConverter{matcher: matcher}, nil +} + +func init() { + common.Must(entries.RegisterConverter("nonnative", common.Must2(NewNonNativeConverter(nil)).(entries.Converter))) + nonnativeifce.NewNonNativeConverterConstructor = NewNonNativeConverter +} diff --git a/app/subscription/entries/nonnative/definitions/shadowsocks.jsont b/app/subscription/entries/nonnative/definitions/shadowsocks.jsont new file mode 100644 index 000000000..0dd3a4c7b --- /dev/null +++ b/app/subscription/entries/nonnative/definitions/shadowsocks.jsont @@ -0,0 +1,35 @@ +{{if assertExists . "root_!kind" | not}} Unknown environment {{end}} +{{if assertIsOneOf . "root_!kind" "json" | not}} This template only works for json input. {{end}} + +{{ $methodName := tryGet . "root_!json_method_!unquoted" "root_!json_protocol_!unquoted" "root_!json_cipher_!unquoted"}} +{{if assertValueIsOneOf $methodName "chacha20-ietf-poly1305" "chacha20-poly1305" "aes-128-gcm" "aes-256-gcm" | not}} + This template only works for ss. {{end}} + +{{ $server_address := tryGet . "root_!json_server" "root_!json_address" "root_!json_endpoint"}} +{{ $server_port := tryGet . "root_!json_port" "root_!json_server_port" "root_!json_endpoint"}} +{{if $server_address | splitAndGetAfterNth ":" 0 | len | gt 1}} + {{ $server_addressport_unquoted := tryGet . "root_!json_endpoint_!unquoted"}} + {{ $server_port = $server_addressport_unquoted | splitAndGetAfterNth ":" -1}} + + {{ $server_portWithSep := printf ":%v" $server_port}} + {{ $server_address = $server_addressport_unquoted | stringCutSuffix $server_portWithSep | jsonEncode}} +{{end}} + +{{ $name_annotation := tryGet . "root_!json_name_!unquoted" "root_!json_id_!unquoted" "root_!json_tag_!unquoted" "root_!json_remarks_!unquoted" ""}} + +{{$password := tryGet . "root_!json_password" "root_!json_psk"}} + +{ + "protocol": "shadowsocks", + "settings": { + "address": {{$server_address}}, + "port": {{$server_port}}, + "method": {{$methodName | jsonEncode}}, + "password": {{$password}} + }, + "metadata":{ + + "TagName": {{print $name_annotation "_" $server_address | jsonEncode}} + + } +} \ No newline at end of file diff --git a/app/subscription/entries/nonnative/definitions/shadowsocks2022.jsont b/app/subscription/entries/nonnative/definitions/shadowsocks2022.jsont new file mode 100644 index 000000000..585008867 --- /dev/null +++ b/app/subscription/entries/nonnative/definitions/shadowsocks2022.jsont @@ -0,0 +1,51 @@ +{{if assertExists . "root_!kind" | not}} Unknown environment {{end}} +{{if assertIsOneOf . "root_!kind" "json" | not}} This template only works for json input. {{end}} + +{{ $methodName := tryGet . "root_!json_method_!unquoted" "root_!json_protocol_!unquoted"}} +{{if assertValueIsOneOf $methodName "2022-blake3-aes-128-gcm" "2022-blake3-aes-256-gcm" | not}} + This template only works for ss2022. {{end}} + +{{ $server_address := tryGet . "root_!json_server" "root_!json_address" "root_!json_endpoint"}} +{{ $server_port := tryGet . "root_!json_port" "root_!json_server_port" "root_!json_endpoint"}} +{{if $server_address | splitAndGetAfterNth ":" 0 | len | gt 1}} + {{ $server_addressport_unquoted := tryGet . "root_!json_endpoint_!unquoted"}} + {{ $server_port = $server_addressport_unquoted | splitAndGetAfterNth ":" -1}} + + {{ $server_portWithSep := printf ":%v" $server_port}} + {{ $server_address = $server_addressport_unquoted | stringCutSuffix $server_portWithSep | jsonEncode}} +{{end}} + +{{ $name_annotation := tryGet . "root_!json_name_!unquoted" "root_!json_id_!unquoted" "root_!json_tag_!unquoted" "root_!json_remarks_!unquoted" ""}} + +{{ $psk := tryGet . "root_!json_password_!unquoted" "root_!json_psk_!unquoted"}} +{{ $ipsk_encoded := "" }} +{{if $psk | splitAndGetAfterNth ":" 0 | len | ne 1}} + {{ $origpsk := $psk }} + {{ $psk = $psk | splitAndGetNth ":" -1 }} + {{ $pskWithSep := printf ":%v" $psk}} + {{ $ipsk_encoded = $origpsk | stringCutSuffix $pskWithSep | splitAndGetAfterNth ":" 0 | jsonEncode}} +{{else}} + {{$ipsk_encoded = tryGet . "root_!json_iPSKs" ""}} +{{end}} + + + { + "protocol": "shadowsocks2022", + "settings": { + "address": {{$server_address}}, + "port": {{$server_port}}, + "method": {{$methodName | jsonEncode}}, + "psk": {{$psk | jsonEncode}} + {{if $ipsk_encoded|len|ne 0}} + , + "ipsk": {{$ipsk_encoded}} + {{end}} + }, + + "metadata":{ + + "TagName": {{print $name_annotation "_" $server_address | jsonEncode}} + + } + + } \ No newline at end of file diff --git a/app/subscription/entries/nonnative/definitions/vmess.jsont b/app/subscription/entries/nonnative/definitions/vmess.jsont new file mode 100644 index 000000000..7c42af3e1 --- /dev/null +++ b/app/subscription/entries/nonnative/definitions/vmess.jsont @@ -0,0 +1,66 @@ +{{if assertExists . "root_!kind" | not}} Unknown environment {{end}} +{{ $protocol_name := tryGet . "root_!link_protocol" "root_!json_type_!unquoted"}} +{{if assertValueIsOneOf $protocol_name "vmess" | not}} This template will only handle vmess link {{end}} + +{{ $server_address := tryGet . "root_!link_host_!base64_!json_add" "root_!json_server"}} +{{ $server_uuid := tryGet . "root_!link_host_!base64_!json_id" "root_!json_uuid"}} +{{ $server_port := tryGet . "root_!link_host_!base64_!json_port_!unquoted" "root_!link_host_!base64_!json_port" "root_!json_port_!unquoted" "root_!json_port"}} + +{{ $transport_type := tryGet . "root_!link_host_!base64_!json_net_!unquoted" "root_!json_network_!unquoted" ""}} +{{ $transport_type = $transport_type | unalias "tcp" ""}} + +{{ $name_annotation := tryGet . "root_!link_host_!base64_!json_ps_!unquoted" "root_!json_name_!unquoted" ""}} + +{{if assertValueIsOneOf $transport_type "tcp" "kcp" "ws" "h2" "quic" "grpc"| not }} + unknown transport type {{end}} + + {{$transport_grpc_service_name := ""}} + {{ if $transport_type | eq "grpc"}} + {{ $transport_grpc_service_name = tryGet . "root_!link_host_!base64_!json_path" ""}} + {{end}} + + {{$transport_ws_path := ""}} + {{ if $transport_type | eq "ws"}} + {{ $transport_ws_path = tryGet . "root_!link_host_!base64_!json_path" "root_!json_ws-opts_!json_path" ""}} + {{end}} + +{{ $security_type := tryGet . "root_!link_host_!base64_!json_tls_!unquoted" "root_!json_tls" ""}} +{{ $security_type = $security_type | unalias "none" "" "false"}} + +{{if assertValueIsOneOf $security_type "tls" "utls" "none"| not }} + unknown security type {{end}} + +{{ $security_tlsmmon_sni := tryGet . "root_!link_host_!base64_!json_sni" ""}} +{{ $security_tlsmmon_sni = $security_tlsmmon_sni | unalias $server_address ""}} + +{ + "protocol": "vmess", + "settings":{ + "address":{{$server_address}}, + "port":{{$server_port}}, + "uuid":{{$server_uuid}} + }, + "streamSettings":{ + "transport":{{$transport_type|jsonEncode}}, + "security":{{$security_type|jsonEncode}}, + "transportSettings":{ + {{ if $transport_type | eq "grpc"}} + "serviceName":{{$transport_grpc_service_name}} + {{end}} + {{ if $transport_type | eq "ws"}} + "path":{{$transport_ws_path}} + {{end}} + }, + + "securitySettings":{ + {{ if $security_type | eq "tls"}} + "serverName":{{$security_tlsmmon_sni}} + {{end}} + } + }, + "metadata":{ + + "TagName": {{print $name_annotation "_" $server_address | jsonEncode}} + + } +} \ No newline at end of file diff --git a/app/subscription/entries/nonnative/matchdef.go b/app/subscription/entries/nonnative/matchdef.go new file mode 100644 index 000000000..8a95084e4 --- /dev/null +++ b/app/subscription/entries/nonnative/matchdef.go @@ -0,0 +1,191 @@ +package nonnative + +import ( + "bytes" + "embed" + "encoding/json" + "io/fs" + "strings" + "text/template" +) + +//go:embed definitions/* +var embeddedDefinitions embed.FS + +func NewDefMatcher() *DefMatcher { + d := &DefMatcher{} + d.init() + return d +} + +type DefMatcher struct { + templates *template.Template +} + +type ExecutionEnvironment struct { + link AbstractNonNativeLink +} + +func (d *DefMatcher) createFuncMap() template.FuncMap { + return map[string]any{ + "assertExists": func(env *ExecutionEnvironment, names ...string) (bool, error) { + link := env.link + for _, v := range names { + _, ok := link.Values[v] + if !ok { + return false, newError("failed assertExists of ", v) + } + } + return true, nil + }, + "assertIsOneOf": func(env *ExecutionEnvironment, name string, values ...string) (bool, error) { + link := env.link + actualValue, ok := link.Values[name] + if !ok { + return false, newError("failed assertIs of non-exist ", name) + } + found := false + for _, currentValue := range values { + if currentValue == actualValue { + found = true + break + } + } + if !found { + return false, newError("failed assertIsOneOf name = ", actualValue, "is not one of ", values) + } + return true, nil + }, + "assertValueIsOneOf": func(value string, values ...string) (bool, error) { + actualValue := value + found := false + for _, currentValue := range values { + if currentValue == actualValue { + found = true + break + } + } + if !found { + return false, newError("failed assertIsOneOf name = ", actualValue, "is not one of ", values) + } + return true, nil + }, + "tryGet": func(env *ExecutionEnvironment, names ...string) (string, error) { + link := env.link + for _, currentName := range names { + value, ok := link.Values[currentName] + if ok { + return value, nil + } else if currentName == "" { + return "", nil + } + } + return "", newError("failed tryGet exists none of ", names) + }, + "splitAndGetNth": func(sep string, n int, content string) (string, error) { + result := strings.Split(content, sep) + if n > len(result)-1 { + return "", newError("failed splitAndGetNth exists too short content:", content, "n = ", n, "sep =", sep) + } + if n < 0 { + n = len(result) + n + if n < 0 { + return "", newError("failed splitAndGetNth exists too short content:", content, "n = ", n, "sep =", sep) + } + } + return result[n], nil + }, + "splitAndGetAfterNth": func(sep string, n int, content string) ([]string, error) { + result := strings.Split(content, sep) + if n < 0 { + n = len(result) + n + } + if n > len(result)-1 { + return []string{}, newError("failed splitAndGetNth exists too short content:", content) + } + return result[n:], nil + }, + "splitAndGetBeforeNth": func(sep string, n int, content string) ([]string, error) { + result := strings.Split(content, sep) + if n < 0 { + n = len(result) + n + } + if n > len(result)-1 { + return []string{}, newError("failed splitAndGetNth exists too short content:", content) + } + return result[:n], nil + }, + "jsonEncode": func(content any) (string, error) { + buf := bytes.NewBuffer(nil) + err := json.NewEncoder(buf).Encode(content) + if err != nil { + return "", newError("unable to jsonQuote ", content).Base(err) + } + return buf.String(), nil + }, + "stringCutSuffix": func(suffix, content string) (string, error) { + remaining, found := strings.CutSuffix(content, suffix) + if !found { + return "", newError("suffix not found in content =", suffix, " suffix =", suffix) + } + return remaining, nil + }, + "unalias": func(standardName string, names ...string) (string, error) { + if len(names) == 0 { + return "", newError("no input value specified") + } + actualInput := names[len(names)-1] + alias := names[:len(names)-1] + for _, v := range alias { + if v == actualInput { + return standardName, nil + } + } + return actualInput, nil + }, + } +} + +func (d *DefMatcher) init() { + d.templates = template.New("root").Funcs(d.createFuncMap()) +} + +func (d *DefMatcher) LoadEmbeddedDefinitions() error { + return d.LoadDefinitions(embeddedDefinitions) +} + +func (d *DefMatcher) LoadDefinitions(fs fs.FS) error { + var err error + d.templates, err = d.templates.ParseFS(fs, "definitions/*.jsont") + if err != nil { + return err + } + return nil +} + +func (d *DefMatcher) ExecuteNamed(link AbstractNonNativeLink, name string) ([]byte, error) { + outputBuffer := bytes.NewBuffer(nil) + env := &ExecutionEnvironment{link: link} + err := d.templates.ExecuteTemplate(outputBuffer, name, env) + if err != nil { + return nil, newError("failed to execute template").Base(err) + } + return outputBuffer.Bytes(), nil +} + +func (d *DefMatcher) ExecuteAll(link AbstractNonNativeLink) ([]byte, error) { + outputBuffer := bytes.NewBuffer(nil) + for _, loadedTemplates := range d.templates.Templates() { + env := &ExecutionEnvironment{link: link} + err := loadedTemplates.Execute(outputBuffer, env) + if err != nil { + outputBuffer.Reset() + } else { + break + } + } + if outputBuffer.Len() == 0 { + return nil, newError("failed to find a working template") + } + return outputBuffer.Bytes(), nil +} diff --git a/app/subscription/entries/nonnative/nonnative.go b/app/subscription/entries/nonnative/nonnative.go new file mode 100644 index 000000000..fe6583976 --- /dev/null +++ b/app/subscription/entries/nonnative/nonnative.go @@ -0,0 +1,108 @@ +package nonnative + +import ( + "encoding/base64" + "encoding/json" + "net/url" + "regexp" + "strings" +) + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen + +func ExtractAllValuesFromBytes(bytes []byte) AbstractNonNativeLink { + link := AbstractNonNativeLink{} + link.fromBytes(bytes) + return link +} + +type jsonDocument map[string]json.RawMessage + +type AbstractNonNativeLink struct { + Values map[string]string +} + +func (a *AbstractNonNativeLink) fromBytes(bytes []byte) { + a.Values = make(map[string]string) + content := string(bytes) + content = strings.Trim(content, " \n\t\r") + a.extractValue(content, "root") +} + +func (a *AbstractNonNativeLink) extractValue(content, prefix string) { + { + // check if the content is a link + match, err := regexp.Match("[a-zA-Z0-9]+:((\\/\\/)|\\?)", []byte(content)) + if err != nil { + panic(err) + } + if match { + // if so, parse as link + parsedURL, err := url.Parse(content) + // if process is successful, then continue to parse every element of the link + if err == nil { + a.Values[prefix+"_!kind"] = "link" + a.extractLink(parsedURL, prefix) + return + } + } + } + { + // check if it is base64 + content = strings.Trim(content, "=") + decoded, err := base64.RawStdEncoding.DecodeString(content) + if err == nil { + a.Values[prefix+"_!kind"] = "base64" + a.extractValue(string(decoded), prefix+"_!base64") + return + } + } + { + // check if it is base64url + content = strings.Trim(content, "=") + decoded, err := base64.RawURLEncoding.DecodeString(content) + if err == nil { + a.Values[prefix+"_!kind"] = "base64url" + a.extractValue(string(decoded), prefix+"_!base64") + return + } + } + { + // check if it is json + var doc jsonDocument + if err := json.Unmarshal([]byte(content), &doc); err == nil { + a.Values[prefix+"_!kind"] = "json" + a.extractJSON(&doc, prefix) + return + } + } +} + +func (a *AbstractNonNativeLink) extractLink(content *url.URL, prefix string) { + a.Values[prefix+"_!link"] = content.String() + a.Values[prefix+"_!link_protocol"] = content.Scheme + a.Values[prefix+"_!link_host"] = content.Host + a.extractValue(content.Host, prefix+"_!link_host") + a.Values[prefix+"_!link_path"] = content.Path + a.Values[prefix+"_!link_query"] = content.RawQuery + a.Values[prefix+"_!link_fragment"] = content.Fragment + a.Values[prefix+"_!link_userinfo"] = content.User.String() + a.Values[prefix+"_!link_opaque"] = content.Opaque +} + +func (a *AbstractNonNativeLink) extractJSON(content *jsonDocument, prefix string) { + for key, value := range *content { + switch value[0] { + case '{': + a.extractValue(string(value), prefix+"_!json_"+key) + case '"': + var unquoted string + if err := json.Unmarshal(value, &unquoted); err == nil { + a.Values[prefix+"_!json_"+key+"_!unquoted"] = unquoted + } + fallthrough + default: + a.Values[prefix+"_!json_"+key] = string(value) + } + } +} diff --git a/app/subscription/entries/nonnative/nonnativeifce/nonnativeifce.go b/app/subscription/entries/nonnative/nonnativeifce/nonnativeifce.go new file mode 100644 index 000000000..f5c207eef --- /dev/null +++ b/app/subscription/entries/nonnative/nonnativeifce/nonnativeifce.go @@ -0,0 +1,11 @@ +package nonnativeifce + +import ( + "io/fs" + + "github.com/v2fly/v2ray-core/v5/app/subscription/entries" +) + +type NonNativeConverterConstructorT func(fs fs.FS) (entries.Converter, error) + +var NewNonNativeConverterConstructor NonNativeConverterConstructorT diff --git a/app/subscription/entries/outbound/outbound.go b/app/subscription/entries/outbound/outbound.go new file mode 100644 index 000000000..d376d2269 --- /dev/null +++ b/app/subscription/entries/outbound/outbound.go @@ -0,0 +1,33 @@ +package outbound + +import ( + "github.com/v2fly/v2ray-core/v5/app/subscription/entries" + "github.com/v2fly/v2ray-core/v5/app/subscription/specs" + "github.com/v2fly/v2ray-core/v5/common" +) + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen + +// NewOutboundEntriesParser internal api +func NewOutboundEntriesParser() entries.Converter { + return newOutboundEntriesParser() +} + +func newOutboundEntriesParser() entries.Converter { + return &outboundEntriesParser{} +} + +type outboundEntriesParser struct{} + +func (o *outboundEntriesParser) ConvertToAbstractServerConfig(rawConfig []byte, kindHint string) (*specs.SubscriptionServerConfig, error) { + parser := specs.NewOutboundParser() + outbound, err := parser.ParseOutboundConfig(rawConfig) + if err != nil { + return nil, newError("failed to parse outbound config").Base(err).AtWarning() + } + return parser.ToSubscriptionServerConfig(outbound) +} + +func init() { + common.Must(entries.RegisterConverter("outbound", newOutboundEntriesParser())) +} diff --git a/app/subscription/entries/register.go b/app/subscription/entries/register.go new file mode 100644 index 000000000..02249d70b --- /dev/null +++ b/app/subscription/entries/register.go @@ -0,0 +1,54 @@ +package entries + +import "github.com/v2fly/v2ray-core/v5/app/subscription/specs" + +type ConverterRegistry struct { + knownConverters map[string]Converter + parent *ConverterRegistry +} + +var globalConverterRegistry = &ConverterRegistry{knownConverters: map[string]Converter{}} + +func RegisterConverter(kind string, converter Converter) error { + return globalConverterRegistry.RegisterConverter(kind, converter) +} + +func GetOverlayConverterRegistry() *ConverterRegistry { + return globalConverterRegistry.GetOverlayConverterRegistry() +} + +func (c *ConverterRegistry) RegisterConverter(kind string, converter Converter) error { + if _, found := c.knownConverters[kind]; found { + return newError("converter already registered for kind ", kind) + } + c.knownConverters[kind] = converter + return nil +} + +func (c *ConverterRegistry) TryAllConverters(rawConfig []byte, prioritizedConverter, kindHint string) (*specs.SubscriptionServerConfig, error) { + if prioritizedConverter != "" { + if converter, found := c.knownConverters[prioritizedConverter]; found { + serverConfig, err := converter.ConvertToAbstractServerConfig(rawConfig, kindHint) + if err == nil { + return serverConfig, nil + } + } + } + + for _, converter := range c.knownConverters { + serverConfig, err := converter.ConvertToAbstractServerConfig(rawConfig, kindHint) + if err == nil { + return serverConfig, nil + } + } + if c.parent != nil { + if serverConfig, err := c.parent.TryAllConverters(rawConfig, prioritizedConverter, kindHint); err == nil { + return serverConfig, nil + } + } + return nil, newError("no converter found for config") +} + +func (c *ConverterRegistry) GetOverlayConverterRegistry() *ConverterRegistry { + return &ConverterRegistry{knownConverters: map[string]Converter{}, parent: c} +} diff --git a/app/subscription/specs/abstract_spec.proto b/app/subscription/specs/abstract_spec.proto new file mode 100644 index 000000000..1b4951010 --- /dev/null +++ b/app/subscription/specs/abstract_spec.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package v2ray.core.app.subscription.specs; + +option csharp_namespace = "V2Ray.Core.App.Subscription.Specs"; +option go_package = "github.com/v2fly/v2ray-core/v5/app/subscription/specs"; +option java_package = "com.v2ray.core.app.subscription.specs"; +option java_multiple_files = true; + +import "google/protobuf/any.proto"; + +message ServerConfiguration{ + string protocol = 1; + google.protobuf.Any protocol_settings = 2; + string transport = 3; + google.protobuf.Any transport_settings = 4; + string security = 5; + google.protobuf.Any security_settings = 6; +} + +message SubscriptionServerConfig{ + string id = 1; + map metadata = 2; + ServerConfiguration configuration = 3; +} + +message SubscriptionDocument { + map metadata = 2; + repeated SubscriptionServerConfig server = 3; +} \ No newline at end of file diff --git a/app/subscription/specs/outbound_parser.go b/app/subscription/specs/outbound_parser.go new file mode 100644 index 000000000..b309efd11 --- /dev/null +++ b/app/subscription/specs/outbound_parser.go @@ -0,0 +1,90 @@ +package specs + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/golang/protobuf/proto" + + "github.com/v2fly/v2ray-core/v5/common/registry" + "github.com/v2fly/v2ray-core/v5/common/serial" +) + +func NewOutboundParser() *OutboundParser { + return &OutboundParser{} +} + +type OutboundParser struct{} + +func (p *OutboundParser) ParseOutboundConfig(rawConfig []byte) (*OutboundConfig, error) { + skeleton := &OutboundConfig{} + decoder := json.NewDecoder(bytes.NewReader(rawConfig)) + decoder.DisallowUnknownFields() + err := decoder.Decode(skeleton) + if err != nil { + return nil, newError("failed to parse outbound config skeleton").Base(err) + } + return skeleton, nil +} + +func (p *OutboundParser) toAbstractServerSpec(config *OutboundConfig) (*ServerConfiguration, error) { + serverConfig := &ServerConfiguration{} + serverConfig.Protocol = config.Protocol + { + protocolSettings, err := loadHeterogeneousConfigFromRawJSONRestricted("outbound", config.Protocol, config.Settings) + if err != nil { + return nil, newError("failed to parse protocol settings").Base(err) + } + serverConfig.ProtocolSettings = serial.ToTypedMessage(protocolSettings) + } + + if config.StreamSetting != nil { + if config.StreamSetting.Transport == "" { + config.StreamSetting.Transport = "tcp" + } + if config.StreamSetting.Security == "" { + config.StreamSetting.Security = "none" + } + { + serverConfig.Transport = config.StreamSetting.Transport + transportSettings, err := loadHeterogeneousConfigFromRawJSONRestricted( + "transport", config.StreamSetting.Transport, config.StreamSetting.TransportSettings) + if err != nil { + return nil, newError("failed to parse transport settings").Base(err) + } + serverConfig.TransportSettings = serial.ToTypedMessage(transportSettings) + } + { + securitySettings, err := loadHeterogeneousConfigFromRawJSONRestricted( + "security", config.StreamSetting.Security, config.StreamSetting.SecuritySettings) + if err != nil { + return nil, newError("failed to parse security settings").Base(err) + } + + serverConfig.SecuritySettings = serial.ToTypedMessage(securitySettings) + serverConfig.Security = serial.V2Type(serverConfig.SecuritySettings) + } + } + return serverConfig, nil +} + +func (p *OutboundParser) ToSubscriptionServerConfig(config *OutboundConfig) (*SubscriptionServerConfig, error) { + serverSpec, err := p.toAbstractServerSpec(config) + if err != nil { + return nil, newError("unable to parse server specification") + } + return &SubscriptionServerConfig{ + Configuration: serverSpec, + Metadata: config.Metadata, + }, nil +} + +func loadHeterogeneousConfigFromRawJSONRestricted(interfaceType, name string, rawJSON json.RawMessage) (proto.Message, error) { + ctx := context.TODO() + ctx = registry.CreateRestrictedModeContext(ctx) + if len(rawJSON) == 0 { + rawJSON = []byte("{}") + } + return registry.LoadImplementationByAlias(ctx, interfaceType, name, []byte(rawJSON)) +} diff --git a/app/subscription/specs/skeleton.go b/app/subscription/specs/skeleton.go new file mode 100644 index 000000000..42e4f0e8d --- /dev/null +++ b/app/subscription/specs/skeleton.go @@ -0,0 +1,19 @@ +package specs + +import ( + "encoding/json" +) + +type OutboundConfig struct { + Protocol string `json:"protocol"` + Settings json.RawMessage `json:"settings"` + StreamSetting *StreamConfig `json:"streamSettings"` + Metadata map[string]string `json:"metadata"` +} + +type StreamConfig struct { + Transport string `json:"transport"` + TransportSettings json.RawMessage `json:"transportSettings"` + Security string `json:"security"` + SecuritySettings json.RawMessage `json:"securitySettings"` +} diff --git a/app/subscription/specs/specs.go b/app/subscription/specs/specs.go new file mode 100644 index 000000000..14a73792c --- /dev/null +++ b/app/subscription/specs/specs.go @@ -0,0 +1,3 @@ +package specs + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen diff --git a/app/subscription/subscription.go b/app/subscription/subscription.go new file mode 100644 index 000000000..f4cd634fe --- /dev/null +++ b/app/subscription/subscription.go @@ -0,0 +1,3 @@ +package subscription + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen diff --git a/app/subscription/subscriptionmanager/delta.go b/app/subscription/subscriptionmanager/delta.go new file mode 100644 index 000000000..95ca2cbd8 --- /dev/null +++ b/app/subscription/subscriptionmanager/delta.go @@ -0,0 +1,8 @@ +package subscriptionmanager + +type changedDocument struct { + removed []string + added []string + modified []string + unchanged []string +} diff --git a/app/subscription/subscriptionmanager/known_metadata.go b/app/subscription/subscriptionmanager/known_metadata.go new file mode 100644 index 000000000..c978fdcf8 --- /dev/null +++ b/app/subscription/subscriptionmanager/known_metadata.go @@ -0,0 +1,7 @@ +package subscriptionmanager + +const ( + ServerMetadataID = "ID" + ServerMetadataTagName = "TagName" + ServerMetadataFullyQualifiedName = "FullyQualifiedName" +) diff --git a/app/subscription/subscriptionmanager/manager.go b/app/subscription/subscriptionmanager/manager.go new file mode 100644 index 000000000..5f10fcb95 --- /dev/null +++ b/app/subscription/subscriptionmanager/manager.go @@ -0,0 +1,103 @@ +package subscriptionmanager + +import ( + "archive/zip" + "bytes" + "context" + "time" + + core "github.com/v2fly/v2ray-core/v5" + "github.com/v2fly/v2ray-core/v5/app/subscription" + "github.com/v2fly/v2ray-core/v5/app/subscription/entries" + "github.com/v2fly/v2ray-core/v5/app/subscription/entries/nonnative/nonnativeifce" + "github.com/v2fly/v2ray-core/v5/common" + "github.com/v2fly/v2ray-core/v5/common/task" + "github.com/v2fly/v2ray-core/v5/features/extension" +) + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen + +type SubscriptionManagerImpl struct { + config *subscription.Config + ctx context.Context + + s *core.Instance + converter *entries.ConverterRegistry + + trackedSubscriptions map[string]*trackedSubscription + + refreshTask *task.Periodic +} + +func (s *SubscriptionManagerImpl) Type() interface{} { + return extension.SubscriptionManagerType() +} + +func (s *SubscriptionManagerImpl) housekeeping() error { + for subscriptionName := range s.trackedSubscriptions { + if err := s.checkupSubscription(subscriptionName); err != nil { + newError("failed to checkup subscription: ", err).AtWarning().WriteToLog() + } + } + return nil +} + +func (s *SubscriptionManagerImpl) Start() error { + if err := s.refreshTask.Start(); err != nil { + return err + } + return nil +} + +func (s *SubscriptionManagerImpl) Close() error { + if err := s.refreshTask.Close(); err != nil { + return err + } + return nil +} + +func (s *SubscriptionManagerImpl) init() error { + s.refreshTask = &task.Periodic{ + Interval: time.Duration(60) * time.Second, + Execute: s.housekeeping, + } + s.trackedSubscriptions = make(map[string]*trackedSubscription) + s.converter = entries.GetOverlayConverterRegistry() + if s.config.NonnativeConverterOverlay != nil { + zipReader, err := zip.NewReader(bytes.NewReader(s.config.NonnativeConverterOverlay), int64(len(s.config.NonnativeConverterOverlay))) + if err != nil { + return newError("failed to read nonnative converter overlay: ", err) + } + converter, err := nonnativeifce.NewNonNativeConverterConstructor(zipReader) + if err != nil { + return newError("failed to construct nonnative converter: ", err) + } + if err := s.converter.RegisterConverter("user_nonnative", converter); err != nil { + return newError("failed to register user nonnative converter: ", err) + } + } + + for _, v := range s.config.Imports { + tracked, err := newTrackedSubscription(v) + if err != nil { + return newError("failed to init subscription ", v.Name, ": ", err) + } + s.trackedSubscriptions[v.Name] = tracked + } + return nil +} + +func NewSubscriptionManager(ctx context.Context, config *subscription.Config) (*SubscriptionManagerImpl, error) { + instance := core.MustFromContext(ctx) + impl := &SubscriptionManagerImpl{ctx: ctx, s: instance, config: config} + if err := impl.init(); err != nil { + return nil, newError("failed to init subscription manager: ", err) + } + return impl, nil +} + +func init() { + common.Must(common.RegisterConfig((*subscription.Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewSubscriptionManager(ctx, config.(*subscription.Config)) + })) +} diff --git a/app/subscription/subscriptionmanager/serverspec_materialize.go b/app/subscription/subscriptionmanager/serverspec_materialize.go new file mode 100644 index 000000000..53e1cbac6 --- /dev/null +++ b/app/subscription/subscriptionmanager/serverspec_materialize.go @@ -0,0 +1,50 @@ +package subscriptionmanager + +import ( + core "github.com/v2fly/v2ray-core/v5" + "github.com/v2fly/v2ray-core/v5/app/proxyman" + "github.com/v2fly/v2ray-core/v5/app/subscription/specs" + "github.com/v2fly/v2ray-core/v5/common/serial" + "github.com/v2fly/v2ray-core/v5/transport/internet" +) + +func (s *SubscriptionManagerImpl) materialize(subscriptionName, tagName string, serverSpec *specs.SubscriptionServerConfig) (*core.OutboundHandlerConfig, error) { + outboundConf, err := s.getOutboundTemplateForSubscriptionName(subscriptionName) + if err != nil { + return nil, newError("failed to get outbound template for subscription name: ", err) + } + + senderSettingsIfcd, err := serial.GetInstanceOf(outboundConf.SenderSettings) + if err != nil { + return nil, newError("failed to get sender settings: ", err) + } + senderSettings := senderSettingsIfcd.(*proxyman.SenderConfig) + + if serverSpec.Configuration.Transport != "" { + senderSettings.StreamSettings.ProtocolName = serverSpec.Configuration.Transport + senderSettings.StreamSettings.TransportSettings = append(senderSettings.StreamSettings.TransportSettings, + &internet.TransportConfig{ProtocolName: serverSpec.Configuration.Transport, Settings: serverSpec.Configuration.TransportSettings}) + } + + if serverSpec.Configuration.Security != "" { + senderSettings.StreamSettings.SecurityType = serverSpec.Configuration.Security + senderSettings.StreamSettings.SecuritySettings = append(senderSettings.StreamSettings.SecuritySettings, + serverSpec.Configuration.SecuritySettings) + } + + outboundConf.SenderSettings = serial.ToTypedMessage(senderSettings) + + outboundConf.ProxySettings = serverSpec.Configuration.ProtocolSettings + + outboundConf.Tag = tagName + + return outboundConf, nil +} + +func (s *SubscriptionManagerImpl) getOutboundTemplateForSubscriptionName(subscriptionName string) (*core.OutboundHandlerConfig, error) { //nolint: unparam + senderSetting := &proxyman.SenderConfig{ + DomainStrategy: proxyman.SenderConfig_AS_IS, StreamSettings: &internet.StreamConfig{}, + } + + return &core.OutboundHandlerConfig{SenderSettings: serial.ToTypedMessage(senderSetting)}, nil +} diff --git a/app/subscription/subscriptionmanager/subdocapplier.go b/app/subscription/subscriptionmanager/subdocapplier.go new file mode 100644 index 000000000..56756864e --- /dev/null +++ b/app/subscription/subscriptionmanager/subdocapplier.go @@ -0,0 +1,121 @@ +package subscriptionmanager + +import ( + "fmt" + + core "github.com/v2fly/v2ray-core/v5" + "github.com/v2fly/v2ray-core/v5/app/subscription/specs" +) + +func (s *SubscriptionManagerImpl) applySubscriptionTo(name string, document *specs.SubscriptionDocument) error { + var trackedSub *trackedSubscription + if trackedSubFound, found := s.trackedSubscriptions[name]; !found { + return newError("not found") + } else { + trackedSub = trackedSubFound + } + + delta, err := trackedSub.diff(document) + if err != nil { + return err + } + + nameToServerConfig := make(map[string]*specs.SubscriptionServerConfig) + for _, server := range document.Server { + nameToServerConfig[server.Id] = server + } + + for _, serverName := range delta.removed { + if err := s.removeManagedServer(name, serverName); err != nil { + newError("failed to remove managed server: ", err).AtWarning().WriteToLog() + continue + } + trackedSub.recordRemovedServer(serverName) + } + + for _, serverName := range delta.modified { + serverConfig := nameToServerConfig[serverName] + if err := s.updateManagedServer(name, serverName, serverConfig); err != nil { + newError("failed to update managed server: ", err).AtWarning().WriteToLog() + continue + } + trackedSub.recordUpdatedServer(serverName, serverConfig.Metadata[ServerMetadataTagName], serverConfig) + } + + for _, serverName := range delta.added { + serverConfig := nameToServerConfig[serverName] + if err := s.addManagedServer(name, serverName, serverConfig); err != nil { + newError("failed to add managed server: ", err).AtWarning().WriteToLog() + continue + } + trackedSub.recordUpdatedServer(serverName, serverConfig.Metadata[ServerMetadataTagName], serverConfig) + } + + newError("finished applying subscription, ", name, "; ", fmt.Sprintf( + "%v updated, %v added, %v removed, %v unchanged", + len(delta.modified), len(delta.added), len(delta.removed), len(delta.unchanged))).AtInfo().WriteToLog() + + return nil +} + +func (s *SubscriptionManagerImpl) removeManagedServer(subscriptionName, serverName string) error { + var trackedSub *trackedSubscription + if trackedSubFound, found := s.trackedSubscriptions[subscriptionName]; !found { + return newError("not found") + } else { + trackedSub = trackedSubFound + } + + var trackedServer *materializedServer + if trackedServerFound, err := trackedSub.getCurrentServer(serverName); err != nil { + return err + } else { + trackedServer = trackedServerFound + } + + tagName := fmt.Sprintf("%s_%s", trackedSub.importSource.TagPrefix, trackedServer.tagPostfix) + + if err := core.RemoveOutboundHandler(s.s, tagName); err != nil { + return newError("failed to remove handler: ", err) + } + trackedSub.recordRemovedServer(serverName) + return nil +} + +func (s *SubscriptionManagerImpl) addManagedServer(subscriptionName, serverName string, + serverSpec *specs.SubscriptionServerConfig, +) error { + var trackedSub *trackedSubscription + if trackedSubFound, found := s.trackedSubscriptions[subscriptionName]; !found { + return newError("not found") + } else { + trackedSub = trackedSubFound + } + tagPostfix := serverSpec.Metadata[ServerMetadataTagName] + tagName := fmt.Sprintf("%s_%s", trackedSub.importSource.TagPrefix, tagPostfix) + + materialized, err := s.materialize(subscriptionName, tagName, serverSpec) + if err != nil { + return newError("failed to materialize server: ", err) + } + + if err := core.AddOutboundHandler(s.s, materialized); err != nil { + return newError("failed to add handler: ", err) + } + + trackedSub.recordUpdatedServer(serverName, tagPostfix, serverSpec) + + return nil +} + +func (s *SubscriptionManagerImpl) updateManagedServer(subscriptionName, serverName string, + serverSpec *specs.SubscriptionServerConfig, +) error { + if err := s.removeManagedServer(subscriptionName, serverName); err != nil { + return newError("failed to update managed server: ", err).AtWarning() + } + if err := s.addManagedServer(subscriptionName, serverName, serverSpec); err != nil { + return newError("failed to update managed server : ", err).AtWarning() + } + return nil +} diff --git a/app/subscription/subscriptionmanager/subdocchecker.go b/app/subscription/subscriptionmanager/subdocchecker.go new file mode 100644 index 000000000..d876dfd5a --- /dev/null +++ b/app/subscription/subscriptionmanager/subdocchecker.go @@ -0,0 +1,26 @@ +package subscriptionmanager + +import "time" + +func (s *SubscriptionManagerImpl) checkupSubscription(subscriptionName string) error { + var trackedSub *trackedSubscription + if trackedSubFound, found := s.trackedSubscriptions[subscriptionName]; !found { + return newError("not found") + } else { + trackedSub = trackedSubFound + } + + shouldUpdate := false + + if trackedSub.currentDocumentExpireTime.Before(time.Now()) { + shouldUpdate = true + } + + if shouldUpdate { + if err := s.updateSubscription(subscriptionName); err != nil { + return newError("failed to update subscription: ", err) + } + } + + return nil +} diff --git a/app/subscription/subscriptionmanager/subdocupdater.go b/app/subscription/subscriptionmanager/subdocupdater.go new file mode 100644 index 000000000..86013e79e --- /dev/null +++ b/app/subscription/subscriptionmanager/subdocupdater.go @@ -0,0 +1,114 @@ +package subscriptionmanager + +import ( + "fmt" + "strings" + "time" + "unicode" + + "golang.org/x/crypto/sha3" + + "github.com/v2fly/v2ray-core/v5/app/subscription/containers" + "github.com/v2fly/v2ray-core/v5/app/subscription/documentfetcher" + "github.com/v2fly/v2ray-core/v5/app/subscription/specs" +) + +func (s *SubscriptionManagerImpl) updateSubscription(subscriptionName string) error { + var trackedSub *trackedSubscription + if trackedSubFound, found := s.trackedSubscriptions[subscriptionName]; !found { + return newError("not found") + } else { + trackedSub = trackedSubFound + } + importSource := trackedSub.importSource + + docFetcher, err := documentfetcher.GetFetcher("http") + if err != nil { + return newError("failed to get fetcher: ", err) + } + + downloadedDocument, err := docFetcher.DownloadDocument(s.ctx, importSource) + if err != nil { + return newError("failed to download document: ", err) + } + + trackedSub.originalDocument = downloadedDocument + + container, err := containers.TryAllParsers(trackedSub.originalDocument, "") + if err != nil { + return newError("failed to parse document: ", err) + } + + trackedSub.originalContainer = container + + parsedDocument := &specs.SubscriptionDocument{} + parsedDocument.Metadata = container.Metadata + + trackedSub.originalServerConfig = make(map[string]*originalServerConfig) + + for _, server := range trackedSub.originalContainer.ServerSpecs { + documentHash := sha3.Sum256(server.Content) + serverConfigHashName := fmt.Sprintf("%x", documentHash) + parsed, err := s.converter.TryAllConverters(server.Content, "outbound", server.KindHint) + if err != nil { + trackedSub.originalServerConfig[serverConfigHashName] = &originalServerConfig{data: server.Content} + continue + } + s.polyfillServerConfig(parsed, serverConfigHashName) + parsedDocument.Server = append(parsedDocument.Server, parsed) + trackedSub.originalServerConfig[parsed.Id] = &originalServerConfig{data: server.Content} + } + newError("new subscription document fetched and parsed from ", subscriptionName).AtInfo().WriteToLog() + if err := s.applySubscriptionTo(subscriptionName, parsedDocument); err != nil { + return newError("failed to apply subscription: ", err) + } + trackedSub.currentDocument = parsedDocument + trackedSub.currentDocumentExpireTime = time.Now().Add(time.Second * time.Duration(importSource.DefaultExpireSeconds)) + return nil +} + +func (s *SubscriptionManagerImpl) polyfillServerConfig(document *specs.SubscriptionServerConfig, hash string) { + document.Id = hash + + if document.Metadata == nil { + document.Metadata = make(map[string]string) + } + + if id, ok := document.Metadata[ServerMetadataID]; !ok || id == "" { + document.Metadata[ServerMetadataID] = document.Id + } else { + document.Id = document.Metadata[ServerMetadataID] + } + + if fqn, ok := document.Metadata[ServerMetadataFullyQualifiedName]; !ok || fqn == "" { + document.Metadata[ServerMetadataFullyQualifiedName] = hash + } + + if tagName, ok := document.Metadata[ServerMetadataTagName]; !ok || tagName == "" { + document.Metadata[ServerMetadataTagName] = document.Metadata[ServerMetadataID] + } + document.Metadata[ServerMetadataTagName] = s.restrictTagName(document.Metadata[ServerMetadataTagName]) +} + +func (s *SubscriptionManagerImpl) restrictTagName(tagName string) string { + newTagName := &strings.Builder{} + somethingRemoved := false + for _, c := range tagName { + if (unicode.IsLetter(c) || unicode.IsNumber(c)) && c < 128 { + newTagName.WriteRune(c) + } else { + somethingRemoved = true + } + } + newTagNameString := newTagName.String() + if len(newTagNameString) > 24 { + newTagNameString = newTagNameString[:15] + somethingRemoved = true + } + if somethingRemoved { + hashedTagName := sha3.Sum256([]byte(tagName)) + hashedTagNameString := fmt.Sprintf("%x", hashedTagName) + newTagNameString = newTagNameString + "_" + hashedTagNameString[:8] + } + return newTagNameString +} diff --git a/app/subscription/subscriptionmanager/tracked_subscription.go b/app/subscription/subscriptionmanager/tracked_subscription.go new file mode 100644 index 000000000..b473d8ec6 --- /dev/null +++ b/app/subscription/subscriptionmanager/tracked_subscription.go @@ -0,0 +1,78 @@ +package subscriptionmanager + +import ( + "time" + + "github.com/v2fly/v2ray-core/v5/app/subscription" + "github.com/v2fly/v2ray-core/v5/app/subscription/containers" + "github.com/v2fly/v2ray-core/v5/app/subscription/specs" +) + +func newTrackedSubscription(importSource *subscription.ImportSource) (*trackedSubscription, error) { //nolint: unparam + return &trackedSubscription{importSource: importSource, materialized: map[string]*materializedServer{}}, nil +} + +type trackedSubscription struct { + importSource *subscription.ImportSource + + currentDocumentExpireTime time.Time + currentDocument *specs.SubscriptionDocument + + materialized map[string]*materializedServer + + originalDocument []byte + originalContainer *containers.Container + originalServerConfig map[string]*originalServerConfig +} + +type originalServerConfig struct { + data []byte +} + +func (s *trackedSubscription) diff(newDocument *specs.SubscriptionDocument) (changedDocument, error) { //nolint: unparam + delta := changedDocument{} + seen := make(map[string]bool) + + for _, server := range newDocument.Server { + if currentMaterialized, found := s.materialized[server.Id]; found { + if currentMaterialized.serverConfig.Metadata[ServerMetadataFullyQualifiedName] == server.Metadata[ServerMetadataFullyQualifiedName] { + delta.unchanged = append(delta.unchanged, server.Id) + } else { + delta.modified = append(delta.modified, server.Id) + } + seen[server.Id] = true + } else { + delta.added = append(delta.added, server.Id) + } + } + + for name := range s.materialized { + if _, ok := seen[name]; !ok { + delta.removed = append(delta.removed, name) + } + } + + return delta, nil +} + +func (s *trackedSubscription) recordRemovedServer(name string) { + delete(s.materialized, name) +} + +func (s *trackedSubscription) recordUpdatedServer(name, tagPostfix string, serverConfig *specs.SubscriptionServerConfig) { + s.materialized[name] = &materializedServer{tagPostfix: tagPostfix, serverConfig: serverConfig} +} + +func (s *trackedSubscription) getCurrentServer(name string) (*materializedServer, error) { + if materialized, found := s.materialized[name]; found { + return materialized, nil + } else { + return nil, newError("not found") + } +} + +type materializedServer struct { + tagPostfix string + + serverConfig *specs.SubscriptionServerConfig +} diff --git a/common/environment/rootcap.go b/common/environment/rootcap.go index 6fe376d3a..7f32a8acf 100644 --- a/common/environment/rootcap.go +++ b/common/environment/rootcap.go @@ -3,5 +3,6 @@ package environment type RootEnvironment interface { AppEnvironment(tag string) AppEnvironment ProxyEnvironment(tag string) ProxyEnvironment + DropProxyEnvironment(tag string) error doNotImpl() } diff --git a/common/environment/rootcap_impl.go b/common/environment/rootcap_impl.go index 2bbf862e2..adc5567bf 100644 --- a/common/environment/rootcap_impl.go +++ b/common/environment/rootcap_impl.go @@ -58,6 +58,14 @@ func (r *rootEnvImpl) ProxyEnvironment(tag string) ProxyEnvironment { } } +func (r *rootEnvImpl) DropProxyEnvironment(tag string) error { + transientStorage, err := r.transientStorage.NarrowScope(r.ctx, tag) + if err != nil { + return err + } + return transientStorage.DropScope(r.ctx, tag) +} + type appEnvImpl struct { transientStorage storage.ScopedTransientStorage systemDialer internet.SystemDialer @@ -83,7 +91,7 @@ func (a *appEnvImpl) Listener() internet.SystemListener { } func (a *appEnvImpl) OutboundDialer() tagged.DialFunc { - panic("implement me") + return internet.DialTaggedOutbound } func (a *appEnvImpl) OpenFileForReadSeek() fsifce.FileSeekerFunc { diff --git a/common/protoext/extensions.pb.go b/common/protoext/extensions.pb.go index 2a9d23e43..cbeaf2b46 100644 --- a/common/protoext/extensions.pb.go +++ b/common/protoext/extensions.pb.go @@ -23,6 +23,9 @@ type MessageOpt struct { Type []string `protobuf:"bytes,1,rep,name=type,proto3" json:"type,omitempty"` ShortName []string `protobuf:"bytes,2,rep,name=short_name,json=shortName,proto3" json:"short_name,omitempty"` TransportOriginalName string `protobuf:"bytes,86001,opt,name=transport_original_name,json=transportOriginalName,proto3" json:"transport_original_name,omitempty"` + // allow_restricted_mode_load allow this config to be loaded in restricted mode + // this is typically used when a an attacker can control the content + AllowRestrictedModeLoad bool `protobuf:"varint,86002,opt,name=allow_restricted_mode_load,json=allowRestrictedModeLoad,proto3" json:"allow_restricted_mode_load,omitempty"` } func (x *MessageOpt) Reset() { @@ -78,6 +81,13 @@ func (x *MessageOpt) GetTransportOriginalName() string { return "" } +func (x *MessageOpt) GetAllowRestrictedModeLoad() bool { + if x != nil { + return x.AllowRestrictedModeLoad + } + return false +} + type FieldOpt struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -217,14 +227,18 @@ var file_common_protoext_extensions_proto_rawDesc = []byte{ 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x65, 0x78, 0x74, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x22, 0x79, 0x0a, 0x0a, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4f, 0x70, 0x74, 0x12, 0x12, - 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, - 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x38, 0x0a, 0x17, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x6f, - 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0xf1, 0x9f, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x4f, - 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xd0, 0x02, 0x0a, 0x08, + 0x22, 0xb8, 0x01, 0x0a, 0x0a, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4f, 0x70, 0x74, 0x12, + 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x38, 0x0a, 0x17, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x5f, + 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0xf1, 0x9f, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3d, 0x0a, 0x1a, + 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x65, 0x64, + 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x5f, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0xf2, 0x9f, 0x05, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x17, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, + 0x74, 0x65, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x4c, 0x6f, 0x61, 0x64, 0x22, 0xd0, 0x02, 0x0a, 0x08, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x6e, 0x79, 0x5f, 0x77, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x61, 0x6e, 0x79, 0x57, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, diff --git a/common/protoext/extensions.proto b/common/protoext/extensions.proto index e7fd82140..3fd7bf26c 100644 --- a/common/protoext/extensions.proto +++ b/common/protoext/extensions.proto @@ -21,6 +21,10 @@ message MessageOpt{ repeated string short_name = 2; string transport_original_name = 86001; + + // allow_restricted_mode_load allow this config to be loaded in restricted mode + // this is typically used when a an attacker can control the content + bool allow_restricted_mode_load = 86002; } message FieldOpt{ diff --git a/common/protofilter/filter.go b/common/protofilter/filter.go index d83c0942f..521d0d1c4 100644 --- a/common/protofilter/filter.go +++ b/common/protofilter/filter.go @@ -85,8 +85,11 @@ func filterMessage(ctx context.Context, message protoreflect.Message) error { } fsenvironment := envctx.EnvironmentFromContext(ctx) - fsifce := fsenvironment.(filesystemcap.FileSystemCapabilitySet) + fsifce, fsifceOk := fsenvironment.(filesystemcap.FileSystemCapabilitySet) for _, v := range fileReadingQueue { + if !fsifceOk { + return newError("unable to read file as filesystem capability is not given") + } field := message.Descriptor().Fields().ByTextName(v.field) if v.filename == "" { continue diff --git a/common/registry/registry.go b/common/registry/registry.go index b62e9a0cd..8d0731fed 100644 --- a/common/registry/registry.go +++ b/common/registry/registry.go @@ -72,6 +72,13 @@ func (i *implementationRegistry) LoadImplementationByAlias(ctx context.Context, } implementationConfigInstancev2 := proto.MessageV2(implementationConfigInstance) + + if isRestrictedModeContext(ctx) { + if err := enforceRestriction(implementationConfigInstancev2); err != nil { + return nil, err + } + } + if err := protofilter.FilterProtoConfig(ctx, implementationConfigInstancev2); err != nil { return nil, err } diff --git a/common/registry/restrict.go b/common/registry/restrict.go new file mode 100644 index 000000000..47e16793b --- /dev/null +++ b/common/registry/restrict.go @@ -0,0 +1,35 @@ +package registry + +import ( + "context" + + "google.golang.org/protobuf/proto" + + "github.com/v2fly/v2ray-core/v5/common/protoext" +) + +const restrictedLoadModeCtx = "restrictedLoadModeCtx" + +func CreateRestrictedModeContext(ctx context.Context) context.Context { + return context.WithValue(ctx, restrictedLoadModeCtx, true) //nolint: staticcheck +} + +func isRestrictedModeContext(ctx context.Context) bool { + v := ctx.Value(restrictedLoadModeCtx) + if v == nil { + return false + } + return v.(bool) +} + +func enforceRestriction(config proto.Message) error { + configDescriptor := config.ProtoReflect().Descriptor() + msgOpts, err := protoext.GetMessageOptions(configDescriptor) + if err != nil { + return newError("unable to find message options").Base(err) + } + if !msgOpts.AllowRestrictedModeLoad { + return newError("component has not opted in for load in restricted mode") + } + return nil +} diff --git a/features/extension/subscription.go b/features/extension/subscription.go new file mode 100644 index 000000000..cbc51061a --- /dev/null +++ b/features/extension/subscription.go @@ -0,0 +1,11 @@ +package extension + +import "github.com/v2fly/v2ray-core/v5/features" + +type SubscriptionManager interface { + features.Feature +} + +func SubscriptionManagerType() interface{} { + return (*SubscriptionManager)(nil) +} diff --git a/main/commands/all/engineering/engineering.go b/main/commands/all/engineering/engineering.go index d1883d1e1..fb31d8349 100644 --- a/main/commands/all/engineering/engineering.go +++ b/main/commands/all/engineering/engineering.go @@ -9,6 +9,9 @@ var cmdEngineering = &base.Command{ Commands: []*base.Command{ cmdConvertPb, cmdReversePb, + cmdNonNativeLinkExtract, + cmdNonNativeLinkExec, + cmdSubscriptionEntriesExtract, }, } diff --git a/main/commands/all/engineering/nonnativelinkexec.go b/main/commands/all/engineering/nonnativelinkexec.go new file mode 100644 index 000000000..7ae3df168 --- /dev/null +++ b/main/commands/all/engineering/nonnativelinkexec.go @@ -0,0 +1,54 @@ +package engineering + +import ( + "bytes" + "flag" + "io" + "os" + + "github.com/v2fly/v2ray-core/v5/app/subscription/entries/nonnative" + "github.com/v2fly/v2ray-core/v5/main/commands/base" +) + +var cmdNonNativeLinkExecInputName *string + +var cmdNonNativeLinkExecTemplatePath *string + +var cmdNonNativeLinkExec = &base.Command{ + UsageLine: "{{.Exec}} engineering nonnativelinkexec", + Flag: func() flag.FlagSet { + fs := flag.NewFlagSet("", flag.ExitOnError) + cmdNonNativeLinkExecInputName = fs.String("name", "", "") + cmdNonNativeLinkExecTemplatePath = fs.String("templatePath", "", "path for template directory (WARNING: This will not stop templates from reading file outside this directory)") + return *fs + }(), + Run: func(cmd *base.Command, args []string) { + cmd.Flag.Parse(args) + + content, err := io.ReadAll(os.Stdin) + if err != nil { + base.Fatalf("%s", err) + } + flattenedLink := nonnative.ExtractAllValuesFromBytes(content) + + matcher := nonnative.NewDefMatcher() + if *cmdNonNativeLinkExecTemplatePath != "" { + osFs := os.DirFS(*cmdNonNativeLinkExecTemplatePath) + err = matcher.LoadDefinitions(osFs) + if err != nil { + base.Fatalf("%s", err) + } + } else { + err = matcher.LoadEmbeddedDefinitions() + if err != nil { + base.Fatalf("%s", err) + } + } + + spec, err := matcher.ExecuteNamed(flattenedLink, *cmdNonNativeLinkExecInputName) + if err != nil { + base.Fatalf("%s", err) + } + io.Copy(os.Stdout, bytes.NewReader(spec)) + }, +} diff --git a/main/commands/all/engineering/nonnativelinkextract.go b/main/commands/all/engineering/nonnativelinkextract.go new file mode 100644 index 000000000..2d88ebc08 --- /dev/null +++ b/main/commands/all/engineering/nonnativelinkextract.go @@ -0,0 +1,55 @@ +package engineering + +import ( + "flag" + "fmt" + "io" + "os" + "sort" + "strings" + + "github.com/v2fly/v2ray-core/v5/app/subscription/entries/nonnative" + "github.com/v2fly/v2ray-core/v5/main/commands/base" +) + +type valueContainer struct { + key, value string +} + +type orderedValueContainer []valueContainer + +func (o *orderedValueContainer) Len() int { + return len(*o) +} + +func (o *orderedValueContainer) Less(i, j int) bool { + return strings.Compare((*o)[i].key, (*o)[j].key) < 0 +} + +func (o *orderedValueContainer) Swap(i, j int) { + (*o)[i], (*o)[j] = (*o)[j], (*o)[i] +} + +var cmdNonNativeLinkExtract = &base.Command{ + UsageLine: "{{.Exec}} engineering nonnativelinkextract", + Flag: func() flag.FlagSet { + fs := flag.NewFlagSet("", flag.ExitOnError) + return *fs + }(), + Run: func(cmd *base.Command, args []string) { + content, err := io.ReadAll(os.Stdin) + if err != nil { + base.Fatalf("%s", err) + } + flattenedLink := nonnative.ExtractAllValuesFromBytes(content) + var valueContainerOrdered orderedValueContainer + + for key, value := range flattenedLink.Values { + valueContainerOrdered = append(valueContainerOrdered, valueContainer{key, value}) + } + sort.Sort(&valueContainerOrdered) + for _, valueContainer := range valueContainerOrdered { + io.WriteString(os.Stdout, fmt.Sprintf("%s=%s\n", valueContainer.key, valueContainer.value)) + } + }, +} diff --git a/main/commands/all/engineering/subscriptionEntriesExtract.go b/main/commands/all/engineering/subscriptionEntriesExtract.go new file mode 100644 index 000000000..ff6400227 --- /dev/null +++ b/main/commands/all/engineering/subscriptionEntriesExtract.go @@ -0,0 +1,70 @@ +package engineering + +import ( + "archive/zip" + "encoding/json" + "flag" + "fmt" + "io" + "os" + + "golang.org/x/crypto/sha3" + + "github.com/v2fly/v2ray-core/v5/app/subscription/containers" + "github.com/v2fly/v2ray-core/v5/main/commands/base" +) + +var cmdSubscriptionEntriesExtractInputName *string + +var cmdSubscriptionEntriesExtract = &base.Command{ + UsageLine: "{{.Exec}} engineering subscriptionEntriesExtract", + Flag: func() flag.FlagSet { + fs := flag.NewFlagSet("", flag.ExitOnError) + cmdSubscriptionEntriesExtractInputName = fs.String("input", "", "") + return *fs + }(), + Run: func(cmd *base.Command, args []string) { + cmd.Flag.Parse(args) + inputReader := os.Stdin + if *cmdSubscriptionEntriesExtractInputName != "" { + file, err := os.Open(*cmdSubscriptionEntriesExtractInputName) + if err != nil { + base.Fatalf("%s", err) + } + inputReader = file + defer file.Close() + } + content, err := io.ReadAll(inputReader) + if err != nil { + base.Fatalf("%s", err) + } + parsed, err := containers.TryAllParsers(content, "") + if err != nil { + base.Fatalf("%s", err) + } + zipWriter := zip.NewWriter(os.Stdout) + { + writer, err := zipWriter.Create("meta.json") + if err != nil { + base.Fatalf("%s", err) + } + err = json.NewEncoder(writer).Encode(parsed.Metadata) + if err != nil { + base.Fatalf("%s", err) + } + } + for k, entry := range parsed.ServerSpecs { + hash := sha3.Sum256(entry.Content) + fileName := fmt.Sprintf("entry_%v_%x", k, hash[:8]) + writer, err := zipWriter.Create(fileName) + if err != nil { + base.Fatalf("%s", err) + } + _, err = writer.Write(entry.Content) + if err != nil { + base.Fatalf("%s", err) + } + } + zipWriter.Close() + }, +} diff --git a/main/distro/all/all.go b/main/distro/all/all.go index 4475e4b06..ccf4fed00 100644 --- a/main/distro/all/all.go +++ b/main/distro/all/all.go @@ -112,4 +112,19 @@ import ( _ "github.com/v2fly/v2ray-core/v5/proxy/shadowsocks/simplified" _ "github.com/v2fly/v2ray-core/v5/proxy/socks/simplified" _ "github.com/v2fly/v2ray-core/v5/proxy/trojan/simplified" + + // Subscription Supports + _ "github.com/v2fly/v2ray-core/v5/app/subscription/subscriptionmanager" + + // Subscription Containers: general purpose + _ "github.com/v2fly/v2ray-core/v5/app/subscription/containers/base64urlline" + _ "github.com/v2fly/v2ray-core/v5/app/subscription/containers/jsonfieldarray" + _ "github.com/v2fly/v2ray-core/v5/app/subscription/containers/jsonfieldarray/jsonified" + + // Subscription Fetchers + _ "github.com/v2fly/v2ray-core/v5/app/subscription/documentfetcher/httpfetcher" + + // Subscription Entries Converters + _ "github.com/v2fly/v2ray-core/v5/app/subscription/entries/nonnative" + _ "github.com/v2fly/v2ray-core/v5/app/subscription/entries/outbound" // Natively Supported Outbound Format ) diff --git a/v2ray.go b/v2ray.go index 2d0397d91..0828df985 100644 --- a/v2ray.go +++ b/v2ray.go @@ -142,6 +142,18 @@ func AddOutboundHandler(server *Instance, config *OutboundHandlerConfig) error { return nil } +func RemoveOutboundHandler(server *Instance, tag string) error { + outboundManager := server.GetFeature(outbound.ManagerType()).(outbound.Manager) + if err := outboundManager.RemoveHandler(server.ctx, tag); err != nil { + return err + } + + if err := server.env.DropProxyEnvironment("o" + tag); err != nil { + return err + } + return nil +} + func addOutboundHandlers(server *Instance, configs []*OutboundHandlerConfig) error { for _, outboundConfig := range configs { if err := AddOutboundHandler(server, outboundConfig); err != nil {