Browse Source

Add subscription manager

Shelikhoo 2 years ago
parent
commit
cc77e90254
54 changed files with 1959 additions and 45 deletions
  1. 34 29
      app/observatory/command/command.pb.go
  2. 5 1
      app/observatory/command/command.proto
  3. 9 0
      app/observatory/observer.go
  4. 1 1
      app/proxyman/command/command.go
  5. 6 0
      app/proxyman/outbound/handler.go
  6. 11 4
      app/proxyman/outbound/outbound.go
  7. 31 0
      app/subscription/config.proto
  8. 3 0
      app/subscription/containers/base64urlline/base64urlline.go
  9. 46 0
      app/subscription/containers/base64urlline/parser.go
  10. 28 0
      app/subscription/containers/containers.go
  11. 3 0
      app/subscription/containers/jsonfieldarray/jsonfieldarray.go
  12. 3 0
      app/subscription/containers/jsonfieldarray/jsonified/jsonified.go
  13. 36 0
      app/subscription/containers/jsonfieldarray/jsonified/parser.go
  14. 68 0
      app/subscription/containers/jsonfieldarray/parser.go
  15. 20 0
      app/subscription/containers/tryall.go
  16. 32 0
      app/subscription/documentfetcher/fetcher.go
  17. 60 0
      app/subscription/documentfetcher/httpfetcher/http.go
  18. 9 0
      app/subscription/entries/entries.go
  19. 51 0
      app/subscription/entries/nonnative/converter.go
  20. 35 0
      app/subscription/entries/nonnative/definitions/shadowsocks.jsont
  21. 51 0
      app/subscription/entries/nonnative/definitions/shadowsocks2022.jsont
  22. 66 0
      app/subscription/entries/nonnative/definitions/vmess.jsont
  23. 191 0
      app/subscription/entries/nonnative/matchdef.go
  24. 108 0
      app/subscription/entries/nonnative/nonnative.go
  25. 11 0
      app/subscription/entries/nonnative/nonnativeifce/nonnativeifce.go
  26. 33 0
      app/subscription/entries/outbound/outbound.go
  27. 54 0
      app/subscription/entries/register.go
  28. 30 0
      app/subscription/specs/abstract_spec.proto
  29. 90 0
      app/subscription/specs/outbound_parser.go
  30. 19 0
      app/subscription/specs/skeleton.go
  31. 3 0
      app/subscription/specs/specs.go
  32. 3 0
      app/subscription/subscription.go
  33. 8 0
      app/subscription/subscriptionmanager/delta.go
  34. 7 0
      app/subscription/subscriptionmanager/known_metadata.go
  35. 103 0
      app/subscription/subscriptionmanager/manager.go
  36. 50 0
      app/subscription/subscriptionmanager/serverspec_materialize.go
  37. 121 0
      app/subscription/subscriptionmanager/subdocapplier.go
  38. 26 0
      app/subscription/subscriptionmanager/subdocchecker.go
  39. 114 0
      app/subscription/subscriptionmanager/subdocupdater.go
  40. 78 0
      app/subscription/subscriptionmanager/tracked_subscription.go
  41. 1 0
      common/environment/rootcap.go
  42. 9 1
      common/environment/rootcap_impl.go
  43. 22 8
      common/protoext/extensions.pb.go
  44. 4 0
      common/protoext/extensions.proto
  45. 4 1
      common/protofilter/filter.go
  46. 7 0
      common/registry/registry.go
  47. 35 0
      common/registry/restrict.go
  48. 11 0
      features/extension/subscription.go
  49. 3 0
      main/commands/all/engineering/engineering.go
  50. 54 0
      main/commands/all/engineering/nonnativelinkexec.go
  51. 55 0
      main/commands/all/engineering/nonnativelinkextract.go
  52. 70 0
      main/commands/all/engineering/subscriptionEntriesExtract.go
  53. 15 0
      main/distro/all/all.go
  54. 12 0
      v2ray.go

+ 34 - 29
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 (

+ 5 - 1
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 {}
+message Config {
+  option (v2ray.core.common.protoext.message_opt).type = "grpcservice";
+  option (v2ray.core.common.protoext.message_opt).short_name = "observatory";
+}

+ 9 - 0
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)
 		}
 	}
 }

+ 1 - 1
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) {

+ 6 - 0
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
 }

+ 11 - 4
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.

+ 31 - 0
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"];
+}

+ 3 - 0
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

+ 46 - 0
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()))
+}

+ 28 - 0
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
+}

+ 3 - 0
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

+ 3 - 0
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

+ 36 - 0
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()))
+}

+ 68 - 0
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()))
+}

+ 20 - 0
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")
+}

+ 32 - 0
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")
+}

+ 60 - 0
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
+}

+ 9 - 0
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)
+}

+ 51 - 0
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
+}

+ 35 - 0
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" "<default>"}}
+
+{{$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}}
+
+    }
+}

+ 51 - 0
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" "<default>"}}
+
+{{ $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" "<default>"}}
+{{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}}
+
+    }
+
+    }

+ 66 - 0
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" "<default>"}}
+{{ $transport_type = $transport_type | unalias "tcp" ""}}
+
+{{ $name_annotation := tryGet . "root_!link_host_!base64_!json_ps_!unquoted" "root_!json_name_!unquoted" "<default>"}}
+
+{{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" "<default>"}}
+    {{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" "<default>"}}
+    {{end}}
+
+{{ $security_type := tryGet . "root_!link_host_!base64_!json_tls_!unquoted" "root_!json_tls" "<default>"}}
+{{ $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" "<default>"}}
+{{ $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}}
+
+  }
+}

+ 191 - 0
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 == "<default>" {
+					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
+}

+ 108 - 0
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)
+		}
+	}
+}

+ 11 - 0
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

+ 33 - 0
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()))
+}

+ 54 - 0
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}
+}

+ 30 - 0
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<string, string> metadata = 2;
+  ServerConfiguration configuration = 3;
+}
+
+message SubscriptionDocument {
+  map<string, string> metadata = 2;
+  repeated SubscriptionServerConfig server = 3;
+}

+ 90 - 0
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))
+}

+ 19 - 0
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"`
+}

+ 3 - 0
app/subscription/specs/specs.go

@@ -0,0 +1,3 @@
+package specs
+
+//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen

+ 3 - 0
app/subscription/subscription.go

@@ -0,0 +1,3 @@
+package subscription
+
+//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen

+ 8 - 0
app/subscription/subscriptionmanager/delta.go

@@ -0,0 +1,8 @@
+package subscriptionmanager
+
+type changedDocument struct {
+	removed   []string
+	added     []string
+	modified  []string
+	unchanged []string
+}

+ 7 - 0
app/subscription/subscriptionmanager/known_metadata.go

@@ -0,0 +1,7 @@
+package subscriptionmanager
+
+const (
+	ServerMetadataID                 = "ID"
+	ServerMetadataTagName            = "TagName"
+	ServerMetadataFullyQualifiedName = "FullyQualifiedName"
+)

+ 103 - 0
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))
+	}))
+}

+ 50 - 0
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
+}

+ 121 - 0
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
+}

+ 26 - 0
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
+}

+ 114 - 0
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
+}

+ 78 - 0
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
+}

+ 1 - 0
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()
 }

+ 9 - 1
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 {

+ 22 - 8
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,

+ 4 - 0
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{

+ 4 - 1
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

+ 7 - 0
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
 	}

+ 35 - 0
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
+}

+ 11 - 0
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)
+}

+ 3 - 0
main/commands/all/engineering/engineering.go

@@ -9,6 +9,9 @@ var cmdEngineering = &base.Command{
 	Commands: []*base.Command{
 		cmdConvertPb,
 		cmdReversePb,
+		cmdNonNativeLinkExtract,
+		cmdNonNativeLinkExec,
+		cmdSubscriptionEntriesExtract,
 	},
 }
 

+ 54 - 0
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))
+	},
+}

+ 55 - 0
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))
+		}
+	},
+}

+ 70 - 0
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()
+	},
+}

+ 15 - 0
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
 )

+ 12 - 0
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 {