1
0
mirror of https://github.com/v2fly/v2ray-core.git synced 2024-12-21 09:36:34 -05:00

Add subscription manager

This commit is contained in:
Shelikhoo 2023-11-21 23:03:20 +00:00 committed by Xiaokang Wang (Shelikhoo)
parent b91354901c
commit cc77e90254
54 changed files with 1959 additions and 45 deletions

View File

@ -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 (

View File

@ -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";
}

View File

@ -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)
}
}
}

View File

@ -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) {

View File

@ -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
}

View File

@ -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.

View File

@ -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"];
}

View File

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

View File

@ -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()))
}

View File

@ -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
}

View File

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

View File

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

View File

@ -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()))
}

View File

@ -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()))
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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}}
}
}

View File

@ -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}}
}
}

View File

@ -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}}
}
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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()))
}

View File

@ -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}
}

View File

@ -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;
}

View File

@ -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))
}

View File

@ -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"`
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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))
}))
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -3,5 +3,6 @@ package environment
type RootEnvironment interface {
AppEnvironment(tag string) AppEnvironment
ProxyEnvironment(tag string) ProxyEnvironment
DropProxyEnvironment(tag string) error
doNotImpl()
}

View File

@ -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 {

View File

@ -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,

View File

@ -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{

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

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

View File

@ -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))
},
}

View File

@ -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))
}
},
}

View File

@ -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()
},
}

View File

@ -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
)

View File

@ -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 {