Browse Source

Add Persistent Storage Support to V2Ray (#3300)

* update protogen to strip unused part

* add persistent storage support

* fix coding style

* update linter setting

* update github integration
Xiaokang Wang (Shelikhoo) 9 months ago
parent
commit
78cd513b82

+ 3 - 3
.github/linters/.golangci.yml

@@ -1,8 +1,5 @@
 run:
 run:
   timeout: 5m
   timeout: 5m
-  skip-files:
-    - generated.*
-    - .pb.go
 
 
 issues:
 issues:
   new: true
   new: true
@@ -13,6 +10,9 @@ issues:
     - linters:
     - linters:
         - stylecheck
         - stylecheck
       text: "ST1016:"
       text: "ST1016:"
+  exclude-files:
+    - generated.*
+    - .pb.go
 
 
 linters:
 linters:
   enable:
   enable:

+ 1 - 1
.github/workflows/deb.yml

@@ -53,7 +53,7 @@ jobs:
           cp ../*.deb ./
           cp ../*.deb ./
 
 
       - name: Upload artifact
       - name: Upload artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
         with:
           name: v2ray-debian-packages
           name: v2ray-debian-packages
           path: ./*.deb
           path: ./*.deb

+ 5 - 5
.github/workflows/release.yml

@@ -188,7 +188,7 @@ jobs:
           openssl dgst -sha512 $FILE | sed 's/([^)]*)//g' >>$DGST
           openssl dgst -sha512 $FILE | sed 's/([^)]*)//g' >>$DGST
 
 
       - name: Upload ZIP file to Artifacts
       - name: Upload ZIP file to Artifacts
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
         with:
           name: v2ray-${{ steps.get_filename.outputs.ASSET_NAME }}.zip
           name: v2ray-${{ steps.get_filename.outputs.ASSET_NAME }}.zip
           path: v2ray-${{ steps.get_filename.outputs.ASSET_NAME }}.zip
           path: v2ray-${{ steps.get_filename.outputs.ASSET_NAME }}.zip
@@ -217,7 +217,7 @@ jobs:
         with:
         with:
           go-version: ^1.23
           go-version: ^1.23
 
 
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
         with:
           path: build_artifacts
           path: build_artifacts
 
 
@@ -252,17 +252,17 @@ jobs:
           openssl dgst -sha256 $FILE | sed 's/([^)]*)//g' >>$DGST
           openssl dgst -sha256 $FILE | sed 's/([^)]*)//g' >>$DGST
           openssl dgst -sha512 $FILE | sed 's/([^)]*)//g' >>$DGST
           openssl dgst -sha512 $FILE | sed 's/([^)]*)//g' >>$DGST
 
 
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
         with:
           name: Release.unsigned
           name: Release.unsigned
           path: build_artifacts/Release.unsigned
           path: build_artifacts/Release.unsigned
 
 
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
         with:
           name: Release.unsigned.dgst
           name: Release.unsigned.dgst
           path: build_artifacts/Release.unsigned.dgst
           path: build_artifacts/Release.unsigned.dgst
 
 
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
         with:
           name: v2ray-extra.zip
           name: v2ray-extra.zip
           path: build_artifacts/v2ray-extra.zip
           path: build_artifacts/v2ray-extra.zip

+ 29 - 17
app/observatory/config.pb.go

@@ -365,11 +365,12 @@ func (x *Intensity) GetProbeInterval() uint32 {
 type Config struct {
 type Config struct {
 	state protoimpl.MessageState `protogen:"open.v1"`
 	state protoimpl.MessageState `protogen:"open.v1"`
 	// @Document The selectors for outbound under observation
 	// @Document The selectors for outbound under observation
-	SubjectSelector []string `protobuf:"bytes,2,rep,name=subject_selector,json=subjectSelector,proto3" json:"subject_selector,omitempty"`
-	ProbeUrl        string   `protobuf:"bytes,3,opt,name=probe_url,json=probeUrl,proto3" json:"probe_url,omitempty"`
-	ProbeInterval   int64    `protobuf:"varint,4,opt,name=probe_interval,json=probeInterval,proto3" json:"probe_interval,omitempty"`
-	unknownFields   protoimpl.UnknownFields
-	sizeCache       protoimpl.SizeCache
+	SubjectSelector       []string `protobuf:"bytes,2,rep,name=subject_selector,json=subjectSelector,proto3" json:"subject_selector,omitempty"`
+	ProbeUrl              string   `protobuf:"bytes,3,opt,name=probe_url,json=probeUrl,proto3" json:"probe_url,omitempty"`
+	ProbeInterval         int64    `protobuf:"varint,4,opt,name=probe_interval,json=probeInterval,proto3" json:"probe_interval,omitempty"`
+	PersistentProbeResult bool     `protobuf:"varint,5,opt,name=persistent_probe_result,json=persistentProbeResult,proto3" json:"persistent_probe_result,omitempty"`
+	unknownFields         protoimpl.UnknownFields
+	sizeCache             protoimpl.SizeCache
 }
 }
 
 
 func (x *Config) Reset() {
 func (x *Config) Reset() {
@@ -423,6 +424,13 @@ func (x *Config) GetProbeInterval() int64 {
 	return 0
 	return 0
 }
 }
 
 
+func (x *Config) GetPersistentProbeResult() bool {
+	if x != nil {
+		return x.PersistentProbeResult
+	}
+	return false
+}
+
 var File_app_observatory_config_proto protoreflect.FileDescriptor
 var File_app_observatory_config_proto protoreflect.FileDescriptor
 
 
 var file_app_observatory_config_proto_rawDesc = string([]byte{
 var file_app_observatory_config_proto_rawDesc = string([]byte{
@@ -476,24 +484,28 @@ var file_app_observatory_config_proto_rawDesc = string([]byte{
 	0x22, 0x32, 0x0a, 0x09, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x79, 0x12, 0x25, 0x0a,
 	0x22, 0x32, 0x0a, 0x09, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x79, 0x12, 0x25, 0x0a,
 	0x0e, 0x70, 0x72, 0x6f, 0x62, 0x65, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18,
 	0x0e, 0x70, 0x72, 0x6f, 0x62, 0x65, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18,
 	0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x62, 0x65, 0x49, 0x6e, 0x74, 0x65,
 	0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x62, 0x65, 0x49, 0x6e, 0x74, 0x65,
-	0x72, 0x76, 0x61, 0x6c, 0x22, 0x9d, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
+	0x72, 0x76, 0x61, 0x6c, 0x22, 0xd5, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
 	0x29, 0x0a, 0x10, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63,
 	0x29, 0x0a, 0x10, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63,
 	0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x75, 0x62, 0x6a, 0x65,
 	0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x75, 0x62, 0x6a, 0x65,
 	0x63, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x72,
 	0x63, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x72,
 	0x6f, 0x62, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70,
 	0x6f, 0x62, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70,
 	0x72, 0x6f, 0x62, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x62, 0x65,
 	0x72, 0x6f, 0x62, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x62, 0x65,
 	0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52,
 	0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52,
-	0x0d, 0x70, 0x72, 0x6f, 0x62, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x3a, 0x24,
-	0x82, 0xb5, 0x18, 0x20, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x15, 0x62,
-	0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61,
-	0x74, 0x6f, 0x72, 0x79, 0x42, 0x6f, 0x0a, 0x1e, 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, 0x50, 0x01, 0x5a, 0x2e, 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, 0xaa, 0x02, 0x1a, 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, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x0d, 0x70, 0x72, 0x6f, 0x62, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x36,
+	0x0a, 0x17, 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x70, 0x72, 0x6f,
+	0x62, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52,
+	0x15, 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x62, 0x65,
+	0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x3a, 0x24, 0x82, 0xb5, 0x18, 0x20, 0x0a, 0x07, 0x73, 0x65,
+	0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x15, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e,
+	0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x6f, 0x0a, 0x1e,
+	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, 0x50, 0x01,
+	0x5a, 0x2e, 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,
+	0xaa, 0x02, 0x1a, 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, 0x62, 0x06, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x33,
 })
 })
 
 
 var (
 var (

+ 2 - 0
app/observatory/config.proto

@@ -84,4 +84,6 @@ message Config {
   string probe_url = 3;
   string probe_url = 3;
 
 
   int64 probe_interval = 4;
   int64 probe_interval = 4;
+
+  bool persistent_probe_result = 5;
 }
 }

+ 54 - 8
app/observatory/observer.go

@@ -12,6 +12,11 @@ import (
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
+	"github.com/v2fly/v2ray-core/v5/app/persistentstorage"
+	"github.com/v2fly/v2ray-core/v5/app/persistentstorage/protostorage"
+	"github.com/v2fly/v2ray-core/v5/common/environment"
+	"github.com/v2fly/v2ray-core/v5/common/environment/envctx"
+
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/proto"
 
 
 	core "github.com/v2fly/v2ray-core/v5"
 	core "github.com/v2fly/v2ray-core/v5"
@@ -34,7 +39,10 @@ type Observer struct {
 
 
 	finished *done.Instance
 	finished *done.Instance
 
 
-	ohm outbound.Manager
+	ohm            outbound.Manager
+	persistStorage persistentstorage.ScopedPersistentStorage
+
+	persistOutboundStatusProtoStorage protostorage.ProtoPersistentStorage
 }
 }
 
 
 func (o *Observer) GetObservation(ctx context.Context) (proto.Message, error) {
 func (o *Observer) GetObservation(ctx context.Context) (proto.Message, error) {
@@ -47,6 +55,24 @@ func (o *Observer) Type() interface{} {
 
 
 func (o *Observer) Start() error {
 func (o *Observer) Start() error {
 	if o.config != nil && len(o.config.SubjectSelector) != 0 {
 	if o.config != nil && len(o.config.SubjectSelector) != 0 {
+		if o.config.PersistentProbeResult {
+			appEnvironment := envctx.EnvironmentFromContext(o.ctx).(environment.AppEnvironment)
+			o.persistStorage = appEnvironment.PersistentStorage()
+
+			outboundStatusStorage, err := o.persistStorage.NarrowScope(o.ctx, []byte("outbound_status"))
+			if err != nil {
+				return newError("failed to get persistent storage for outbound_status").Base(err)
+			}
+			o.persistOutboundStatusProtoStorage = outboundStatusStorage.(protostorage.ProtoPersistentStorage)
+			list, err := outboundStatusStorage.List(o.ctx, []byte(""))
+			if err != nil {
+				newError("failed to list persisted outbound status").Base(err).WriteToLog()
+			} else {
+				for _, v := range list {
+					o.loadOutboundStatus(string(v))
+				}
+			}
+		}
 		o.finished = done.New()
 		o.finished = done.New()
 		go o.background()
 		go o.background()
 	}
 	}
@@ -195,6 +221,12 @@ func (o *Observer) updateStatusForResult(outbound string, result *ProbeResult) {
 		status.LastErrorReason = result.LastErrorReason
 		status.LastErrorReason = result.LastErrorReason
 		status.Delay = 99999999
 		status.Delay = 99999999
 	}
 	}
+	if o.config.PersistentProbeResult {
+		err := o.persistOutboundStatusProtoStorage.PutProto(o.ctx, outbound, status)
+		if err != nil {
+			newError("failed to persist outbound status").Base(err).WriteToLog()
+		}
+	}
 }
 }
 
 
 func (o *Observer) findStatusLocationLockHolderOnly(outbound string) int {
 func (o *Observer) findStatusLocationLockHolderOnly(outbound string) int {
@@ -206,19 +238,33 @@ func (o *Observer) findStatusLocationLockHolderOnly(outbound string) int {
 	return -1
 	return -1
 }
 }
 
 
+func (o *Observer) loadOutboundStatus(name string) {
+	if o.persistOutboundStatusProtoStorage == nil {
+		return
+	}
+	status := &OutboundStatus{}
+	err := o.persistOutboundStatusProtoStorage.GetProto(o.ctx, name, status)
+	if err != nil {
+		newError("failed to load outbound status").Base(err).WriteToLog()
+		return
+	}
+	o.status = append(o.status, status)
+}
+
 func New(ctx context.Context, config *Config) (*Observer, error) {
 func New(ctx context.Context, config *Config) (*Observer, error) {
-	var outboundManager outbound.Manager
+	obs := &Observer{
+		config: config,
+		ctx:    ctx,
+	}
+
 	err := core.RequireFeatures(ctx, func(om outbound.Manager) {
 	err := core.RequireFeatures(ctx, func(om outbound.Manager) {
-		outboundManager = om
+		obs.ohm = om
 	})
 	})
 	if err != nil {
 	if err != nil {
 		return nil, newError("Cannot get depended features").Base(err)
 		return nil, newError("Cannot get depended features").Base(err)
 	}
 	}
-	return &Observer{
-		config: config,
-		ctx:    ctx,
-		ohm:    outboundManager,
-	}, nil
+
+	return obs, nil
 }
 }
 
 
 func init() {
 func init() {

+ 214 - 0
app/persistentstorage/filesystemstorage/config.pb.go

@@ -0,0 +1,214 @@
+package filesystemstorage
+
+import (
+	_ "github.com/v2fly/v2ray-core/v5/common/protoext"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type StateStorageRoot int32
+
+const (
+	StateStorageRoot_WorkDir StateStorageRoot = 0
+)
+
+// Enum value maps for StateStorageRoot.
+var (
+	StateStorageRoot_name = map[int32]string{
+		0: "WorkDir",
+	}
+	StateStorageRoot_value = map[string]int32{
+		"WorkDir": 0,
+	}
+)
+
+func (x StateStorageRoot) Enum() *StateStorageRoot {
+	p := new(StateStorageRoot)
+	*p = x
+	return p
+}
+
+func (x StateStorageRoot) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (StateStorageRoot) Descriptor() protoreflect.EnumDescriptor {
+	return file_app_persistentstorage_filesystemstorage_config_proto_enumTypes[0].Descriptor()
+}
+
+func (StateStorageRoot) Type() protoreflect.EnumType {
+	return &file_app_persistentstorage_filesystemstorage_config_proto_enumTypes[0]
+}
+
+func (x StateStorageRoot) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use StateStorageRoot.Descriptor instead.
+func (StateStorageRoot) EnumDescriptor() ([]byte, []int) {
+	return file_app_persistentstorage_filesystemstorage_config_proto_rawDescGZIP(), []int{0}
+}
+
+type Config struct {
+	state            protoimpl.MessageState `protogen:"open.v1"`
+	StateStorageRoot StateStorageRoot       `protobuf:"varint,1,opt,name=state_storage_root,json=stateStorageRoot,proto3,enum=v2ray.core.app.persistentstorage.filesystemstorage.StateStorageRoot" json:"state_storage_root,omitempty"`
+	InstanceName     string                 `protobuf:"bytes,4,opt,name=instance_name,json=instanceName,proto3" json:"instance_name,omitempty"`
+	Protojson        bool                   `protobuf:"varint,5,opt,name=protojson,proto3" json:"protojson,omitempty"`
+	unknownFields    protoimpl.UnknownFields
+	sizeCache        protoimpl.SizeCache
+}
+
+func (x *Config) Reset() {
+	*x = Config{}
+	mi := &file_app_persistentstorage_filesystemstorage_config_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Config) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Config) ProtoMessage() {}
+
+func (x *Config) ProtoReflect() protoreflect.Message {
+	mi := &file_app_persistentstorage_filesystemstorage_config_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Config.ProtoReflect.Descriptor instead.
+func (*Config) Descriptor() ([]byte, []int) {
+	return file_app_persistentstorage_filesystemstorage_config_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Config) GetStateStorageRoot() StateStorageRoot {
+	if x != nil {
+		return x.StateStorageRoot
+	}
+	return StateStorageRoot_WorkDir
+}
+
+func (x *Config) GetInstanceName() string {
+	if x != nil {
+		return x.InstanceName
+	}
+	return ""
+}
+
+func (x *Config) GetProtojson() bool {
+	if x != nil {
+		return x.Protojson
+	}
+	return false
+}
+
+var File_app_persistentstorage_filesystemstorage_config_proto protoreflect.FileDescriptor
+
+var file_app_persistentstorage_filesystemstorage_config_proto_rawDesc = string([]byte{
+	0x0a, 0x34, 0x61, 0x70, 0x70, 0x2f, 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74,
+	0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
+	0x65, 0x6d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
+	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x32, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f,
+	0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e,
+	0x74, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
+	0x74, 0x65, 0x6d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 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, 0x22, 0xe1, 0x01, 0x0a,
+	0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x72, 0x0a, 0x12, 0x73, 0x74, 0x61, 0x74, 0x65,
+	0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x0e, 0x32, 0x44, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65,
+	0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x73,
+	0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65,
+	0x6d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x74,
+	0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x52, 0x10, 0x73, 0x74, 0x61, 0x74, 0x65,
+	0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x69,
+	0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65,
+	0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20,
+	0x01, 0x28, 0x08, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x20,
+	0x82, 0xb5, 0x18, 0x1c, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x11, 0x66,
+	0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
+	0x2a, 0x1f, 0x0a, 0x10, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
+	0x52, 0x6f, 0x6f, 0x74, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x6f, 0x72, 0x6b, 0x44, 0x69, 0x72, 0x10,
+	0x00, 0x42, 0xb3, 0x01, 0x0a, 0x32, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e,
+	0x63, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x73,
+	0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65,
+	0x6d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x01, 0x5a, 0x46, 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, 0x70,
+	0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
+	0x2f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x74, 0x6f, 0x72, 0x61,
+	0x67, 0x65, 0xaa, 0x02, 0x32, 0x56, 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e,
+	0x41, 0x70, 0x70, 0x2e, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x74,
+	0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
+	0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+})
+
+var (
+	file_app_persistentstorage_filesystemstorage_config_proto_rawDescOnce sync.Once
+	file_app_persistentstorage_filesystemstorage_config_proto_rawDescData []byte
+)
+
+func file_app_persistentstorage_filesystemstorage_config_proto_rawDescGZIP() []byte {
+	file_app_persistentstorage_filesystemstorage_config_proto_rawDescOnce.Do(func() {
+		file_app_persistentstorage_filesystemstorage_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_persistentstorage_filesystemstorage_config_proto_rawDesc), len(file_app_persistentstorage_filesystemstorage_config_proto_rawDesc)))
+	})
+	return file_app_persistentstorage_filesystemstorage_config_proto_rawDescData
+}
+
+var file_app_persistentstorage_filesystemstorage_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_app_persistentstorage_filesystemstorage_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_app_persistentstorage_filesystemstorage_config_proto_goTypes = []any{
+	(StateStorageRoot)(0), // 0: v2ray.core.app.persistentstorage.filesystemstorage.StateStorageRoot
+	(*Config)(nil),        // 1: v2ray.core.app.persistentstorage.filesystemstorage.Config
+}
+var file_app_persistentstorage_filesystemstorage_config_proto_depIdxs = []int32{
+	0, // 0: v2ray.core.app.persistentstorage.filesystemstorage.Config.state_storage_root:type_name -> v2ray.core.app.persistentstorage.filesystemstorage.StateStorageRoot
+	1, // [1:1] is the sub-list for method output_type
+	1, // [1:1] is the sub-list for method input_type
+	1, // [1:1] is the sub-list for extension type_name
+	1, // [1:1] is the sub-list for extension extendee
+	0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_app_persistentstorage_filesystemstorage_config_proto_init() }
+func file_app_persistentstorage_filesystemstorage_config_proto_init() {
+	if File_app_persistentstorage_filesystemstorage_config_proto != nil {
+		return
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_persistentstorage_filesystemstorage_config_proto_rawDesc), len(file_app_persistentstorage_filesystemstorage_config_proto_rawDesc)),
+			NumEnums:      1,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_app_persistentstorage_filesystemstorage_config_proto_goTypes,
+		DependencyIndexes: file_app_persistentstorage_filesystemstorage_config_proto_depIdxs,
+		EnumInfos:         file_app_persistentstorage_filesystemstorage_config_proto_enumTypes,
+		MessageInfos:      file_app_persistentstorage_filesystemstorage_config_proto_msgTypes,
+	}.Build()
+	File_app_persistentstorage_filesystemstorage_config_proto = out.File
+	file_app_persistentstorage_filesystemstorage_config_proto_goTypes = nil
+	file_app_persistentstorage_filesystemstorage_config_proto_depIdxs = nil
+}

+ 23 - 0
app/persistentstorage/filesystemstorage/config.proto

@@ -0,0 +1,23 @@
+syntax = "proto3";
+
+package v2ray.core.app.persistentstorage.filesystemstorage;
+option csharp_namespace = "V2Ray.Core.App.Persistentstorage.Filesystemstorage";
+option go_package = "github.com/v2fly/v2ray-core/v5/app/persistentstorage/filesystemstorage";
+option java_package = "com.v2ray.core.persistentstorage.filesystemstorage";
+option java_multiple_files = true;
+
+import "common/protoext/extensions.proto";
+
+enum StateStorageRoot {
+  WorkDir = 0;
+}
+
+message Config {
+  option (v2ray.core.common.protoext.message_opt).type = "service";
+  option (v2ray.core.common.protoext.message_opt).short_name = "filesystemstorage";
+
+  StateStorageRoot state_storage_root = 1;
+  string instance_name = 4;
+
+  bool protojson = 5;
+}

+ 133 - 0
app/persistentstorage/filesystemstorage/fs.go

@@ -0,0 +1,133 @@
+package filesystemstorage
+
+import (
+	"bytes"
+	"context"
+	"io"
+	"path/filepath"
+	"strings"
+
+	"google.golang.org/protobuf/proto"
+
+	"github.com/v2fly/v2ray-core/v5/app/persistentstorage/protostorage"
+	"github.com/v2fly/v2ray-core/v5/common"
+	"github.com/v2fly/v2ray-core/v5/common/environment"
+	"github.com/v2fly/v2ray-core/v5/common/environment/envctx"
+	"github.com/v2fly/v2ray-core/v5/features/extension/storage"
+)
+
+func newFileSystemStorage(ctx context.Context, config *Config) storage.ScopedPersistentStorageService {
+	appEnvironment := envctx.EnvironmentFromContext(ctx).(environment.AppEnvironment)
+	fss := &fileSystemStorage{
+		fs:              appEnvironment,
+		pathRoot:        config.InstanceName,
+		currentLocation: nil,
+		config:          config,
+	}
+
+	protoStorageInst := protostorage.NewProtoStorage(fss, config.Protojson)
+	fss.proto = protoStorageInst
+	return fss
+}
+
+type fileSystemStorage struct {
+	fs    environment.FileSystemCapabilitySet
+	proto protostorage.ProtoPersistentStorage
+
+	pathRoot        string
+	currentLocation []string
+	config          *Config
+}
+
+func (f *fileSystemStorage) Type() interface{} {
+	return storage.ScopedPersistentStorageServiceType
+}
+
+func (f *fileSystemStorage) Start() error {
+	return nil
+}
+
+func (f *fileSystemStorage) Close() error {
+	return nil
+}
+
+func (f *fileSystemStorage) PutProto(ctx context.Context, key string, pb proto.Message) error {
+	return f.proto.PutProto(ctx, key, pb)
+}
+
+func (f *fileSystemStorage) GetProto(ctx context.Context, key string, pb proto.Message) error {
+	return f.proto.GetProto(ctx, key, pb)
+}
+
+func (f *fileSystemStorage) ScopedPersistentStorageEngine() {
+}
+
+func (f *fileSystemStorage) Put(ctx context.Context, key []byte, value []byte) error {
+	finalPath := filepath.Join(f.pathRoot, filepath.Join(f.currentLocation...), string(key))
+	if value == nil {
+		return f.fs.RemoveFile()(finalPath)
+	}
+	writer, err := f.fs.OpenFileForWrite()(finalPath)
+	if err != nil {
+		return err
+	}
+	defer writer.Close()
+	_, err = io.Copy(writer, io.NopCloser(bytes.NewReader(value)))
+	return err
+}
+
+func (f *fileSystemStorage) Get(ctx context.Context, key []byte) ([]byte, error) {
+	finalPath := filepath.Join(f.pathRoot, filepath.Join(f.currentLocation...), string(key))
+	reader, err := f.fs.OpenFileForRead()(finalPath)
+	if err != nil {
+		return nil, err
+	}
+	defer reader.Close()
+	return io.ReadAll(reader)
+}
+
+func (f *fileSystemStorage) List(ctx context.Context, keyPrefix []byte) ([][]byte, error) {
+	res, err := f.fs.ReadDir()(filepath.Join(f.pathRoot, filepath.Join(f.currentLocation...)))
+	if err != nil {
+		return nil, err
+	}
+	var result [][]byte
+	for _, entry := range res {
+		if !entry.IsDir() && bytes.HasPrefix([]byte(entry.Name()), keyPrefix) {
+			result = append(result, []byte(entry.Name()))
+		}
+	}
+	return result, nil
+}
+
+func (f *fileSystemStorage) Clear(ctx context.Context) {
+	allFile, err := f.List(ctx, []byte{})
+	if err != nil {
+		return
+	}
+	for _, file := range allFile {
+		_ = f.Put(ctx, file, nil)
+	}
+}
+
+func (f *fileSystemStorage) NarrowScope(ctx context.Context, key []byte) (storage.ScopedPersistentStorage, error) {
+	escapedKey := strings.ReplaceAll(string(key), "/", "_")
+	fss := &fileSystemStorage{
+		fs:              f.fs,
+		pathRoot:        f.pathRoot,
+		currentLocation: append(f.currentLocation, escapedKey),
+		config:          f.config,
+	}
+	fss.proto = protostorage.NewProtoStorage(fss, f.config.Protojson)
+	return fss, nil
+}
+
+func (f *fileSystemStorage) DropScope(ctx context.Context, key []byte) error {
+	panic("unimplemented")
+}
+
+func init() {
+	common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
+		return newFileSystemStorage(ctx, config.(*Config)), nil
+	}))
+}

+ 51 - 0
app/persistentstorage/protostorage/protokv.go

@@ -0,0 +1,51 @@
+package protostorage
+
+import (
+	"context"
+
+	"google.golang.org/protobuf/encoding/protojson"
+	"google.golang.org/protobuf/proto"
+
+	"github.com/v2fly/v2ray-core/v5/features/extension/storage"
+)
+
+type ProtoPersistentStorage interface {
+	PutProto(ctx context.Context, key string, pb proto.Message) error
+	GetProto(ctx context.Context, key string, pb proto.Message) error
+}
+
+type protoStorage struct {
+	storage    storage.ScopedPersistentStorage
+	textFormat bool
+}
+
+func (p *protoStorage) PutProto(ctx context.Context, key string, pb proto.Message) error {
+	if !p.textFormat {
+		data, err := proto.Marshal(pb)
+		if err != nil {
+			return err
+		}
+		return p.storage.Put(ctx, []byte(key), data)
+	} else {
+		protojsonStr := protojson.Format(pb)
+		return p.storage.Put(ctx, []byte(key), []byte(protojsonStr))
+	}
+}
+
+func (p *protoStorage) GetProto(ctx context.Context, key string, pb proto.Message) error {
+	data, err := p.storage.Get(ctx, []byte(key))
+	if err != nil {
+		return err
+	}
+	if !p.textFormat {
+		return proto.Unmarshal(data, pb)
+	}
+	return protojson.Unmarshal(data, pb)
+}
+
+func NewProtoStorage(storage storage.ScopedPersistentStorage, textFormat bool) ProtoPersistentStorage {
+	return &protoStorage{
+		storage:    storage,
+		textFormat: textFormat,
+	}
+}

+ 5 - 0
app/persistentstorage/storage.go

@@ -0,0 +1,5 @@
+package persistentstorage
+
+import "github.com/v2fly/v2ray-core/v5/features/extension/storage"
+
+type ScopedPersistentStorage = storage.ScopedPersistentStorage

+ 16 - 8
app/proxyman/outbound/handler_test.go

@@ -5,18 +5,18 @@ import (
 	"testing"
 	"testing"
 	_ "unsafe"
 	_ "unsafe"
 
 
-	"github.com/v2fly/v2ray-core/v5/common/environment/systemnetworkimpl"
-
-	"github.com/v2fly/v2ray-core/v5/common/environment"
-	"github.com/v2fly/v2ray-core/v5/common/environment/envctx"
-	"github.com/v2fly/v2ray-core/v5/common/environment/transientstorageimpl"
-
 	"google.golang.org/protobuf/types/known/anypb"
 	"google.golang.org/protobuf/types/known/anypb"
 
 
 	core "github.com/v2fly/v2ray-core/v5"
 	core "github.com/v2fly/v2ray-core/v5"
 	"github.com/v2fly/v2ray-core/v5/app/policy"
 	"github.com/v2fly/v2ray-core/v5/app/policy"
 	. "github.com/v2fly/v2ray-core/v5/app/proxyman/outbound"
 	. "github.com/v2fly/v2ray-core/v5/app/proxyman/outbound"
 	"github.com/v2fly/v2ray-core/v5/app/stats"
 	"github.com/v2fly/v2ray-core/v5/app/stats"
+	"github.com/v2fly/v2ray-core/v5/common/environment"
+	"github.com/v2fly/v2ray-core/v5/common/environment/deferredpersistentstorage"
+	"github.com/v2fly/v2ray-core/v5/common/environment/envctx"
+	"github.com/v2fly/v2ray-core/v5/common/environment/filesystemimpl"
+	"github.com/v2fly/v2ray-core/v5/common/environment/systemnetworkimpl"
+	"github.com/v2fly/v2ray-core/v5/common/environment/transientstorageimpl"
 	"github.com/v2fly/v2ray-core/v5/common/net"
 	"github.com/v2fly/v2ray-core/v5/common/net"
 	"github.com/v2fly/v2ray-core/v5/common/serial"
 	"github.com/v2fly/v2ray-core/v5/common/serial"
 	"github.com/v2fly/v2ray-core/v5/features/outbound"
 	"github.com/v2fly/v2ray-core/v5/features/outbound"
@@ -50,7 +50,11 @@ func TestOutboundWithoutStatCounter(t *testing.T) {
 	v.AddFeature((outbound.Manager)(new(Manager)))
 	v.AddFeature((outbound.Manager)(new(Manager)))
 	ctx := toContext(context.Background(), v)
 	ctx := toContext(context.Background(), v)
 	defaultNetworkImpl := systemnetworkimpl.NewSystemNetworkDefault()
 	defaultNetworkImpl := systemnetworkimpl.NewSystemNetworkDefault()
-	rootEnv := environment.NewRootEnvImpl(ctx, transientstorageimpl.NewScopedTransientStorageImpl(), defaultNetworkImpl.Dialer(), defaultNetworkImpl.Listener())
+	defaultFilesystemImpl := filesystemimpl.NewDefaultFileSystemDefaultImpl()
+	deferredPersistentStorageImpl := deferredpersistentstorage.NewDeferredPersistentStorage(ctx)
+	rootEnv := environment.NewRootEnvImpl(ctx,
+		transientstorageimpl.NewScopedTransientStorageImpl(), defaultNetworkImpl.Dialer(), defaultNetworkImpl.Listener(),
+		defaultFilesystemImpl, deferredPersistentStorageImpl)
 	proxyEnvironment := rootEnv.ProxyEnvironment("o")
 	proxyEnvironment := rootEnv.ProxyEnvironment("o")
 	ctx = envctx.ContextWithEnvironment(ctx, proxyEnvironment)
 	ctx = envctx.ContextWithEnvironment(ctx, proxyEnvironment)
 	h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
 	h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
@@ -83,7 +87,11 @@ func TestOutboundWithStatCounter(t *testing.T) {
 	v.AddFeature((outbound.Manager)(new(Manager)))
 	v.AddFeature((outbound.Manager)(new(Manager)))
 	ctx := toContext(context.Background(), v)
 	ctx := toContext(context.Background(), v)
 	defaultNetworkImpl := systemnetworkimpl.NewSystemNetworkDefault()
 	defaultNetworkImpl := systemnetworkimpl.NewSystemNetworkDefault()
-	rootEnv := environment.NewRootEnvImpl(ctx, transientstorageimpl.NewScopedTransientStorageImpl(), defaultNetworkImpl.Dialer(), defaultNetworkImpl.Listener())
+	defaultFilesystemImpl := filesystemimpl.NewDefaultFileSystemDefaultImpl()
+	deferredPersistentStorageImpl := deferredpersistentstorage.NewDeferredPersistentStorage(ctx)
+	rootEnv := environment.NewRootEnvImpl(ctx,
+		transientstorageimpl.NewScopedTransientStorageImpl(), defaultNetworkImpl.Dialer(), defaultNetworkImpl.Listener(),
+		defaultFilesystemImpl, deferredPersistentStorageImpl)
 	proxyEnvironment := rootEnv.ProxyEnvironment("o")
 	proxyEnvironment := rootEnv.ProxyEnvironment("o")
 	ctx = envctx.ContextWithEnvironment(ctx, proxyEnvironment)
 	ctx = envctx.ContextWithEnvironment(ctx, proxyEnvironment)
 	h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
 	h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{

+ 111 - 0
common/environment/deferredpersistentstorage/defereredPersistentStorage.go

@@ -0,0 +1,111 @@
+package deferredpersistentstorage
+
+import (
+	"context"
+
+	"github.com/v2fly/v2ray-core/v5/app/persistentstorage"
+	"github.com/v2fly/v2ray-core/v5/common/errors"
+	"github.com/v2fly/v2ray-core/v5/features/extension/storage"
+)
+
+type DeferredPersistentStorage interface {
+	storage.ScopedPersistentStorage
+	ProvideInner(ctx context.Context, inner persistentstorage.ScopedPersistentStorage)
+}
+
+var errNotExist = errors.New("persistent storage does not exist")
+
+type deferredPersistentStorage struct {
+	ready context.Context
+	done  context.CancelFunc
+	inner persistentstorage.ScopedPersistentStorage
+
+	awaitingChildren []*deferredPersistentStorage
+
+	intoScopes []string
+}
+
+func (d *deferredPersistentStorage) ScopedPersistentStorageEngine() {
+}
+
+func (d *deferredPersistentStorage) Put(ctx context.Context, key []byte, value []byte) error {
+	<-d.ready.Done()
+	if d.inner == nil {
+		return errNotExist
+	}
+	return d.inner.Put(ctx, key, value)
+}
+
+func (d *deferredPersistentStorage) Get(ctx context.Context, key []byte) ([]byte, error) {
+	<-d.ready.Done()
+	if d.inner == nil {
+		return nil, errNotExist
+	}
+	return d.inner.Get(ctx, key)
+}
+
+func (d *deferredPersistentStorage) List(ctx context.Context, keyPrefix []byte) ([][]byte, error) {
+	<-d.ready.Done()
+	if d.inner == nil {
+		return nil, errNotExist
+	}
+	return d.inner.List(ctx, keyPrefix)
+}
+
+func (d *deferredPersistentStorage) Clear(ctx context.Context) {
+	<-d.ready.Done()
+	if d.inner == nil {
+		return
+	}
+	d.inner.Clear(ctx)
+}
+
+func (d *deferredPersistentStorage) NarrowScope(ctx context.Context, key []byte) (storage.ScopedPersistentStorage, error) {
+	if d.ready.Err() != nil {
+		return d.inner.NarrowScope(ctx, key)
+	}
+	ready, done := context.WithCancel(ctx)
+	swallowCopyScopes := d.intoScopes
+	dps := &deferredPersistentStorage{
+		ready:      ready,
+		done:       done,
+		inner:      nil,
+		intoScopes: append(swallowCopyScopes, string(key)),
+	}
+	d.awaitingChildren = append(d.awaitingChildren, dps)
+	return dps, nil
+}
+
+func (d *deferredPersistentStorage) DropScope(ctx context.Context, key []byte) error {
+	<-d.ready.Done()
+	if d.inner == nil {
+		return errNotExist
+	}
+	return d.inner.DropScope(ctx, key)
+}
+
+func (d *deferredPersistentStorage) ProvideInner(ctx context.Context, inner persistentstorage.ScopedPersistentStorage) {
+	d.inner = inner
+	if inner != nil {
+		for _, scope := range d.intoScopes {
+			newScope, err := inner.NarrowScope(ctx, []byte(scope))
+			if err != nil {
+				panic(err)
+			}
+			d.inner = newScope
+		}
+	}
+	for _, child := range d.awaitingChildren {
+		child.ProvideInner(ctx, d.inner)
+	}
+	d.done()
+}
+
+func NewDeferredPersistentStorage(ctx context.Context) DeferredPersistentStorage {
+	ready, done := context.WithCancel(ctx)
+	return &deferredPersistentStorage{
+		ready: ready,
+		done:  done,
+		inner: nil,
+	}
+}

+ 8 - 0
common/environment/envimpl/fs.go

@@ -8,6 +8,14 @@ import (
 
 
 type fileSystemDefaultImpl struct{}
 type fileSystemDefaultImpl struct{}
 
 
+func (f fileSystemDefaultImpl) ReadDir() fsifce.FileReadDirFunc {
+	return filesystem.NewFileReadDir
+}
+
+func (f fileSystemDefaultImpl) RemoveFile() fsifce.FileRemoveFunc {
+	return filesystem.NewFileRemover
+}
+
 func (f fileSystemDefaultImpl) OpenFileForReadSeek() fsifce.FileSeekerFunc {
 func (f fileSystemDefaultImpl) OpenFileForReadSeek() fsifce.FileSeekerFunc {
 	return filesystem.NewFileSeeker
 	return filesystem.NewFileSeeker
 }
 }

+ 2 - 0
common/environment/filesystemcap/fscap.go

@@ -6,4 +6,6 @@ type FileSystemCapabilitySet interface {
 	OpenFileForReadSeek() fsifce.FileSeekerFunc
 	OpenFileForReadSeek() fsifce.FileSeekerFunc
 	OpenFileForRead() fsifce.FileReaderFunc
 	OpenFileForRead() fsifce.FileReaderFunc
 	OpenFileForWrite() fsifce.FileWriterFunc
 	OpenFileForWrite() fsifce.FileWriterFunc
+	ReadDir() fsifce.FileReadDirFunc
+	RemoveFile() fsifce.FileRemoveFunc
 }
 }

+ 33 - 0
common/environment/filesystemimpl/fsimpl.go

@@ -0,0 +1,33 @@
+package filesystemimpl
+
+import (
+	"github.com/v2fly/v2ray-core/v5/common/environment"
+	"github.com/v2fly/v2ray-core/v5/common/platform/filesystem"
+	"github.com/v2fly/v2ray-core/v5/common/platform/filesystem/fsifce"
+)
+
+func NewDefaultFileSystemDefaultImpl() environment.FileSystemCapabilitySet {
+	return fsCapImpl{}
+}
+
+type fsCapImpl struct{}
+
+func (f fsCapImpl) OpenFileForReadSeek() fsifce.FileSeekerFunc {
+	return filesystem.NewFileSeeker
+}
+
+func (f fsCapImpl) OpenFileForRead() fsifce.FileReaderFunc {
+	return filesystem.NewFileReader
+}
+
+func (f fsCapImpl) OpenFileForWrite() fsifce.FileWriterFunc {
+	return filesystem.NewFileWriter
+}
+
+func (f fsCapImpl) ReadDir() fsifce.FileReadDirFunc {
+	return filesystem.NewFileReadDir
+}
+
+func (f fsCapImpl) RemoveFile() fsifce.FileRemoveFunc {
+	return filesystem.NewFileRemover
+}

+ 25 - 4
common/environment/rootcap_impl.go

@@ -11,19 +11,24 @@ import (
 
 
 func NewRootEnvImpl(ctx context.Context, transientStorage storage.ScopedTransientStorage,
 func NewRootEnvImpl(ctx context.Context, transientStorage storage.ScopedTransientStorage,
 	systemDialer internet.SystemDialer, systemListener internet.SystemListener,
 	systemDialer internet.SystemDialer, systemListener internet.SystemListener,
+	filesystem FileSystemCapabilitySet, persistStorage storage.ScopedPersistentStorage,
 ) RootEnvironment {
 ) RootEnvironment {
 	return &rootEnvImpl{
 	return &rootEnvImpl{
 		transientStorage: transientStorage,
 		transientStorage: transientStorage,
 		systemListener:   systemListener,
 		systemListener:   systemListener,
 		systemDialer:     systemDialer,
 		systemDialer:     systemDialer,
+		filesystem:       filesystem,
+		persistStorage:   persistStorage,
 		ctx:              ctx,
 		ctx:              ctx,
 	}
 	}
 }
 }
 
 
 type rootEnvImpl struct {
 type rootEnvImpl struct {
+	persistStorage   storage.ScopedPersistentStorage
 	transientStorage storage.ScopedTransientStorage
 	transientStorage storage.ScopedTransientStorage
 	systemDialer     internet.SystemDialer
 	systemDialer     internet.SystemDialer
 	systemListener   internet.SystemListener
 	systemListener   internet.SystemListener
+	filesystem       FileSystemCapabilitySet
 
 
 	ctx context.Context
 	ctx context.Context
 }
 }
@@ -37,10 +42,16 @@ func (r *rootEnvImpl) AppEnvironment(tag string) AppEnvironment {
 	if err != nil {
 	if err != nil {
 		return nil
 		return nil
 	}
 	}
+	persistStorage, err := r.persistStorage.NarrowScope(r.ctx, []byte(tag))
+	if err != nil {
+		return nil
+	}
 	return &appEnvImpl{
 	return &appEnvImpl{
 		transientStorage: transientStorage,
 		transientStorage: transientStorage,
+		persistStorage:   persistStorage,
 		systemListener:   r.systemListener,
 		systemListener:   r.systemListener,
 		systemDialer:     r.systemDialer,
 		systemDialer:     r.systemDialer,
+		filesystem:       r.filesystem,
 		ctx:              r.ctx,
 		ctx:              r.ctx,
 	}
 	}
 }
 }
@@ -67,9 +78,11 @@ func (r *rootEnvImpl) DropProxyEnvironment(tag string) error {
 }
 }
 
 
 type appEnvImpl struct {
 type appEnvImpl struct {
+	persistStorage   storage.ScopedPersistentStorage
 	transientStorage storage.ScopedTransientStorage
 	transientStorage storage.ScopedTransientStorage
 	systemDialer     internet.SystemDialer
 	systemDialer     internet.SystemDialer
 	systemListener   internet.SystemListener
 	systemListener   internet.SystemListener
+	filesystem       FileSystemCapabilitySet
 
 
 	ctx context.Context
 	ctx context.Context
 }
 }
@@ -95,19 +108,27 @@ func (a *appEnvImpl) OutboundDialer() tagged.DialFunc {
 }
 }
 
 
 func (a *appEnvImpl) OpenFileForReadSeek() fsifce.FileSeekerFunc {
 func (a *appEnvImpl) OpenFileForReadSeek() fsifce.FileSeekerFunc {
-	panic("implement me")
+	return a.filesystem.OpenFileForReadSeek()
 }
 }
 
 
 func (a *appEnvImpl) OpenFileForRead() fsifce.FileReaderFunc {
 func (a *appEnvImpl) OpenFileForRead() fsifce.FileReaderFunc {
-	panic("implement me")
+	return a.filesystem.OpenFileForRead()
 }
 }
 
 
 func (a *appEnvImpl) OpenFileForWrite() fsifce.FileWriterFunc {
 func (a *appEnvImpl) OpenFileForWrite() fsifce.FileWriterFunc {
-	panic("implement me")
+	return a.filesystem.OpenFileForWrite()
+}
+
+func (a *appEnvImpl) ReadDir() fsifce.FileReadDirFunc {
+	return a.filesystem.ReadDir()
+}
+
+func (a *appEnvImpl) RemoveFile() fsifce.FileRemoveFunc {
+	return a.filesystem.RemoveFile()
 }
 }
 
 
 func (a *appEnvImpl) PersistentStorage() storage.ScopedPersistentStorage {
 func (a *appEnvImpl) PersistentStorage() storage.ScopedPersistentStorage {
-	panic("implement me")
+	return a.persistStorage
 }
 }
 
 
 func (a *appEnvImpl) TransientStorage() storage.ScopedTransientStorage {
 func (a *appEnvImpl) TransientStorage() storage.ScopedTransientStorage {

+ 9 - 0
common/platform/filesystem/file.go

@@ -3,6 +3,7 @@ package filesystem
 import (
 import (
 	"io"
 	"io"
 	"os"
 	"os"
+	"path/filepath"
 
 
 	"github.com/v2fly/v2ray-core/v5/common/buf"
 	"github.com/v2fly/v2ray-core/v5/common/buf"
 	"github.com/v2fly/v2ray-core/v5/common/platform"
 	"github.com/v2fly/v2ray-core/v5/common/platform"
@@ -18,9 +19,17 @@ var NewFileReader fsifce.FileReaderFunc = func(path string) (io.ReadCloser, erro
 }
 }
 
 
 var NewFileWriter fsifce.FileWriterFunc = func(path string) (io.WriteCloser, error) {
 var NewFileWriter fsifce.FileWriterFunc = func(path string) (io.WriteCloser, error) {
+	basePath := filepath.Dir(path)
+	if err := os.MkdirAll(basePath, 0o700); err != nil {
+		return nil, err
+	}
 	return os.Create(path)
 	return os.Create(path)
 }
 }
 
 
+var NewFileRemover fsifce.FileRemoveFunc = os.Remove
+
+var NewFileReadDir fsifce.FileReadDirFunc = os.ReadDir
+
 func ReadFile(path string) ([]byte, error) {
 func ReadFile(path string) ([]byte, error) {
 	reader, err := NewFileReader(path)
 	reader, err := NewFileReader(path)
 	if err != nil {
 	if err != nil {

+ 8 - 1
common/platform/filesystem/fsifce/ifce.go

@@ -1,9 +1,16 @@
 package fsifce
 package fsifce
 
 
-import "io"
+import (
+	"io"
+	"io/fs"
+)
 
 
 type FileSeekerFunc func(path string) (io.ReadSeekCloser, error)
 type FileSeekerFunc func(path string) (io.ReadSeekCloser, error)
 
 
 type FileReaderFunc func(path string) (io.ReadCloser, error)
 type FileReaderFunc func(path string) (io.ReadCloser, error)
 
 
 type FileWriterFunc func(path string) (io.WriteCloser, error)
 type FileWriterFunc func(path string) (io.WriteCloser, error)
+
+type FileReadDirFunc func(path string) ([]fs.DirEntry, error)
+
+type FileRemoveFunc func(path string) error

+ 9 - 0
features/extension/storage/storage.go

@@ -2,6 +2,8 @@ package storage
 
 
 import (
 import (
 	"context"
 	"context"
+
+	"github.com/v2fly/v2ray-core/v5/features"
 )
 )
 
 
 type ScopedPersistentStorage interface {
 type ScopedPersistentStorage interface {
@@ -23,3 +25,10 @@ type ScopedTransientStorage interface {
 	NarrowScope(ctx context.Context, key string) (ScopedTransientStorage, error)
 	NarrowScope(ctx context.Context, key string) (ScopedTransientStorage, error)
 	DropScope(ctx context.Context, key string) error
 	DropScope(ctx context.Context, key string) error
 }
 }
+
+type ScopedPersistentStorageService interface {
+	ScopedPersistentStorage
+	features.Feature
+}
+
+var ScopedPersistentStorageServiceType = (*ScopedPersistentStorageService)(nil)

+ 2 - 9
infra/vprotogen/main.go

@@ -108,9 +108,9 @@ func getProjectProtocVersion(url string) (string, error) {
 	if err != nil {
 	if err != nil {
 		return "", fmt.Errorf("can not read from body")
 		return "", fmt.Errorf("can not read from body")
 	}
 	}
-	versionRegexp := regexp.MustCompile(`\/\/\s*protoc\s*v(\d+\.\d+\.\d+)`)
+	versionRegexp := regexp.MustCompile(`\/\/\s*protoc\s*v(\d+\.(\d+\.\d+))`)
 	matched := versionRegexp.FindStringSubmatch(string(body))
 	matched := versionRegexp.FindStringSubmatch(string(body))
-	return matched[1], nil
+	return matched[2], nil
 }
 }
 
 
 func getInstalledProtocVersion(protocPath string) (string, error) {
 func getInstalledProtocVersion(protocPath string) (string, error) {
@@ -126,10 +126,6 @@ func getInstalledProtocVersion(protocPath string) (string, error) {
 	if len(matched) == 0 {
 	if len(matched) == 0 {
 		return "", errors.New("can not parse protoc version")
 		return "", errors.New("can not parse protoc version")
 	}
 	}
-
-	if len(matched) == 2 {
-		installedVersion += "4." // in contrast to getProjectProtocVersion()
-	}
 	installedVersion += matched[1]
 	installedVersion += matched[1]
 	fmt.Println("Using protoc version: " + installedVersion)
 	fmt.Println("Using protoc version: " + installedVersion)
 	return installedVersion, nil
 	return installedVersion, nil
@@ -139,9 +135,6 @@ func parseVersion(s string, width int) int64 {
 	strList := strings.Split(s, ".")
 	strList := strings.Split(s, ".")
 	format := fmt.Sprintf("%%s%%0%ds", width)
 	format := fmt.Sprintf("%%s%%0%ds", width)
 	v := ""
 	v := ""
-	if len(strList) == 2 {
-		strList = append([]string{"4"}, strList...)
-	}
 	for _, value := range strList {
 	for _, value := range strList {
 		v = fmt.Sprintf(format, v, value)
 		v = fmt.Sprintf(format, v, value)
 	}
 	}

+ 1 - 0
main/distro/all/all.go

@@ -34,6 +34,7 @@ import (
 	_ "github.com/v2fly/v2ray-core/v5/app/commander/webcommander"
 	_ "github.com/v2fly/v2ray-core/v5/app/commander/webcommander"
 	_ "github.com/v2fly/v2ray-core/v5/app/instman"
 	_ "github.com/v2fly/v2ray-core/v5/app/instman"
 	_ "github.com/v2fly/v2ray-core/v5/app/observatory"
 	_ "github.com/v2fly/v2ray-core/v5/app/observatory"
+	_ "github.com/v2fly/v2ray-core/v5/app/persistentstorage/filesystemstorage"
 	_ "github.com/v2fly/v2ray-core/v5/app/tun"
 	_ "github.com/v2fly/v2ray-core/v5/app/tun"
 
 
 	// Inbound and outbound proxies.
 	// Inbound and outbound proxies.

+ 8 - 1
transport/internet/kcp/kcp_test.go

@@ -7,6 +7,9 @@ import (
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
+	"github.com/v2fly/v2ray-core/v5/common/environment/deferredpersistentstorage"
+	"github.com/v2fly/v2ray-core/v5/common/environment/filesystemimpl"
+
 	"github.com/v2fly/v2ray-core/v5/common/environment"
 	"github.com/v2fly/v2ray-core/v5/common/environment"
 	"github.com/v2fly/v2ray-core/v5/common/environment/envctx"
 	"github.com/v2fly/v2ray-core/v5/common/environment/envctx"
 	"github.com/v2fly/v2ray-core/v5/common/environment/systemnetworkimpl"
 	"github.com/v2fly/v2ray-core/v5/common/environment/systemnetworkimpl"
@@ -25,7 +28,11 @@ import (
 func TestDialAndListen(t *testing.T) {
 func TestDialAndListen(t *testing.T) {
 	ctx := context.Background()
 	ctx := context.Background()
 	defaultNetworkImpl := systemnetworkimpl.NewSystemNetworkDefault()
 	defaultNetworkImpl := systemnetworkimpl.NewSystemNetworkDefault()
-	rootEnv := environment.NewRootEnvImpl(ctx, transientstorageimpl.NewScopedTransientStorageImpl(), defaultNetworkImpl.Dialer(), defaultNetworkImpl.Listener())
+	defaultFilesystemImpl := filesystemimpl.NewDefaultFileSystemDefaultImpl()
+	deferredPersistentStorageImpl := deferredpersistentstorage.NewDeferredPersistentStorage(ctx)
+	rootEnv := environment.NewRootEnvImpl(ctx,
+		transientstorageimpl.NewScopedTransientStorageImpl(), defaultNetworkImpl.Dialer(), defaultNetworkImpl.Listener(),
+		defaultFilesystemImpl, deferredPersistentStorageImpl)
 	proxyEnvironment := rootEnv.ProxyEnvironment("o")
 	proxyEnvironment := rootEnv.ProxyEnvironment("o")
 	transportEnvironment, err := proxyEnvironment.NarrowScopeToTransport("kcp")
 	transportEnvironment, err := proxyEnvironment.NarrowScopeToTransport("kcp")
 	if err != nil {
 	if err != nil {

+ 1 - 0
transport/internet/tls/ech.go

@@ -13,6 +13,7 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/miekg/dns"
 	"github.com/miekg/dns"
+
 	"github.com/v2fly/v2ray-core/v5/common/net"
 	"github.com/v2fly/v2ray-core/v5/common/net"
 	"github.com/v2fly/v2ray-core/v5/transport/internet"
 	"github.com/v2fly/v2ray-core/v5/transport/internet"
 )
 )

+ 18 - 1
v2ray.go

@@ -5,6 +5,10 @@ import (
 	"reflect"
 	"reflect"
 	sync "sync"
 	sync "sync"
 
 
+	"github.com/v2fly/v2ray-core/v5/common/environment/deferredpersistentstorage"
+	"github.com/v2fly/v2ray-core/v5/common/environment/filesystemimpl"
+	"github.com/v2fly/v2ray-core/v5/features/extension/storage"
+
 	"github.com/v2fly/v2ray-core/v5/common"
 	"github.com/v2fly/v2ray-core/v5/common"
 	"github.com/v2fly/v2ray-core/v5/common/environment"
 	"github.com/v2fly/v2ray-core/v5/common/environment"
 	"github.com/v2fly/v2ray-core/v5/common/environment/systemnetworkimpl"
 	"github.com/v2fly/v2ray-core/v5/common/environment/systemnetworkimpl"
@@ -205,7 +209,14 @@ func initInstanceWithConfig(config *Config, server *Instance) (bool, error) {
 	}
 	}
 
 
 	defaultNetworkImpl := systemnetworkimpl.NewSystemNetworkDefault()
 	defaultNetworkImpl := systemnetworkimpl.NewSystemNetworkDefault()
-	server.env = environment.NewRootEnvImpl(server.ctx, transientstorageimpl.NewScopedTransientStorageImpl(), defaultNetworkImpl.Dialer(), defaultNetworkImpl.Listener())
+	defaultFilesystemImpl := filesystemimpl.NewDefaultFileSystemDefaultImpl()
+	deferredPersistentStorageImpl := deferredpersistentstorage.NewDeferredPersistentStorage(server.ctx)
+	server.env = environment.NewRootEnvImpl(server.ctx,
+		transientstorageimpl.NewScopedTransientStorageImpl(),
+		defaultNetworkImpl.Dialer(),
+		defaultNetworkImpl.Listener(),
+		defaultFilesystemImpl,
+		deferredPersistentStorageImpl)
 
 
 	for _, appSettings := range config.App {
 	for _, appSettings := range config.App {
 		settings, err := serial.GetInstanceOf(appSettings)
 		settings, err := serial.GetInstanceOf(appSettings)
@@ -247,6 +258,12 @@ func initInstanceWithConfig(config *Config, server *Instance) (bool, error) {
 		return true, newError("not all dependency are resolved.")
 		return true, newError("not all dependency are resolved.")
 	}
 	}
 
 
+	if persistentStorageService := server.GetFeature(storage.ScopedPersistentStorageServiceType); persistentStorageService != nil {
+		deferredPersistentStorageImpl.ProvideInner(server.ctx, persistentStorageService.(storage.ScopedPersistentStorage))
+	} else {
+		deferredPersistentStorageImpl.ProvideInner(server.ctx, nil)
+	}
+
 	if err := addInboundHandlers(server, config.Inbound); err != nil {
 	if err := addInboundHandlers(server, config.Inbound); err != nil {
 		return true, err
 		return true, err
 	}
 	}