This commit is contained in:
RPRX 2020-11-25 19:01:53 +08:00
parent 47d23e9972
commit c7f7c08ead
711 changed files with 82154 additions and 2 deletions

2
app/app.go Normal file
View file

@ -0,0 +1,2 @@
// Package app contains feature implementations of Xray. The features may be enabled during runtime.
package app

110
app/commander/commander.go Normal file
View file

@ -0,0 +1,110 @@
// +build !confonly
package commander
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
import (
"context"
"net"
"sync"
"google.golang.org/grpc"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/signal/done"
core "github.com/xtls/xray-core/v1/core"
"github.com/xtls/xray-core/v1/features/outbound"
)
// Commander is a Xray feature that provides gRPC methods to external clients.
type Commander struct {
sync.Mutex
server *grpc.Server
services []Service
ohm outbound.Manager
tag string
}
// NewCommander creates a new Commander based on the given config.
func NewCommander(ctx context.Context, config *Config) (*Commander, error) {
c := &Commander{
tag: config.Tag,
}
common.Must(core.RequireFeatures(ctx, func(om outbound.Manager) {
c.ohm = om
}))
for _, rawConfig := range config.Service {
config, err := rawConfig.GetInstance()
if err != nil {
return nil, err
}
rawService, err := common.CreateObject(ctx, config)
if err != nil {
return nil, err
}
service, ok := rawService.(Service)
if !ok {
return nil, newError("not a Service.")
}
c.services = append(c.services, service)
}
return c, nil
}
// Type implements common.HasType.
func (c *Commander) Type() interface{} {
return (*Commander)(nil)
}
// Start implements common.Runnable.
func (c *Commander) Start() error {
c.Lock()
c.server = grpc.NewServer()
for _, service := range c.services {
service.Register(c.server)
}
c.Unlock()
listener := &OutboundListener{
buffer: make(chan net.Conn, 4),
done: done.New(),
}
go func() {
if err := c.server.Serve(listener); err != nil {
newError("failed to start grpc server").Base(err).AtError().WriteToLog()
}
}()
if err := c.ohm.RemoveHandler(context.Background(), c.tag); err != nil {
newError("failed to remove existing handler").WriteToLog()
}
return c.ohm.AddHandler(context.Background(), &Outbound{
tag: c.tag,
listener: listener,
})
}
// Close implements common.Closable.
func (c *Commander) Close() error {
c.Lock()
defer c.Unlock()
if c.server != nil {
c.server.Stop()
c.server = nil
}
return nil
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) {
return NewCommander(ctx, cfg.(*Config))
}))
}

227
app/commander/config.pb.go Normal file
View file

@ -0,0 +1,227 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.14.0
// source: app/commander/config.proto
package commander
import (
proto "github.com/golang/protobuf/proto"
serial "github.com/xtls/xray-core/v1/common/serial"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
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)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
// Config is the settings for Commander.
type Config struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Tag of the outbound handler that handles grpc connections.
Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
// Services that supported by this server. All services must implement Service
// interface.
Service []*serial.TypedMessage `protobuf:"bytes,2,rep,name=service,proto3" json:"service,omitempty"`
}
func (x *Config) Reset() {
*x = Config{}
if protoimpl.UnsafeEnabled {
mi := &file_app_commander_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_commander_config_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && 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_commander_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetTag() string {
if x != nil {
return x.Tag
}
return ""
}
func (x *Config) GetService() []*serial.TypedMessage {
if x != nil {
return x.Service
}
return nil
}
// ReflectionConfig is the placeholder config for ReflectionService.
type ReflectionConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *ReflectionConfig) Reset() {
*x = ReflectionConfig{}
if protoimpl.UnsafeEnabled {
mi := &file_app_commander_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ReflectionConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReflectionConfig) ProtoMessage() {}
func (x *ReflectionConfig) ProtoReflect() protoreflect.Message {
mi := &file_app_commander_config_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReflectionConfig.ProtoReflect.Descriptor instead.
func (*ReflectionConfig) Descriptor() ([]byte, []int) {
return file_app_commander_config_proto_rawDescGZIP(), []int{1}
}
var File_app_commander_config_proto protoreflect.FileDescriptor
var file_app_commander_config_proto_rawDesc = []byte{
0x0a, 0x1a, 0x61, 0x70, 0x70, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x65, 0x72, 0x2f,
0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x12, 0x78, 0x72,
0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x65, 0x72,
0x1a, 0x21, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2f,
0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x22, 0x56, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a,
0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12,
0x3a, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73,
0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x52,
0x65, 0x66, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42,
0x5b, 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e,
0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x65, 0x72, 0x50, 0x01, 0x5a, 0x2a, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61,
0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x63, 0x6f,
0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x65, 0x72, 0xaa, 0x02, 0x12, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41,
0x70, 0x70, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
}
var (
file_app_commander_config_proto_rawDescOnce sync.Once
file_app_commander_config_proto_rawDescData = file_app_commander_config_proto_rawDesc
)
func file_app_commander_config_proto_rawDescGZIP() []byte {
file_app_commander_config_proto_rawDescOnce.Do(func() {
file_app_commander_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_commander_config_proto_rawDescData)
})
return file_app_commander_config_proto_rawDescData
}
var file_app_commander_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_app_commander_config_proto_goTypes = []interface{}{
(*Config)(nil), // 0: xray.app.commander.Config
(*ReflectionConfig)(nil), // 1: xray.app.commander.ReflectionConfig
(*serial.TypedMessage)(nil), // 2: xray.common.serial.TypedMessage
}
var file_app_commander_config_proto_depIdxs = []int32{
2, // 0: xray.app.commander.Config.service:type_name -> xray.common.serial.TypedMessage
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_commander_config_proto_init() }
func file_app_commander_config_proto_init() {
if File_app_commander_config_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_app_commander_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_commander_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReflectionConfig); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_commander_config_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_app_commander_config_proto_goTypes,
DependencyIndexes: file_app_commander_config_proto_depIdxs,
MessageInfos: file_app_commander_config_proto_msgTypes,
}.Build()
File_app_commander_config_proto = out.File
file_app_commander_config_proto_rawDesc = nil
file_app_commander_config_proto_goTypes = nil
file_app_commander_config_proto_depIdxs = nil
}

View file

@ -0,0 +1,21 @@
syntax = "proto3";
package xray.app.commander;
option csharp_namespace = "Xray.App.Commander";
option go_package = "github.com/xtls/xray-core/v1/app/commander";
option java_package = "com.xray.app.commander";
option java_multiple_files = true;
import "common/serial/typed_message.proto";
// Config is the settings for Commander.
message Config {
// Tag of the outbound handler that handles grpc connections.
string tag = 1;
// Services that supported by this server. All services must implement Service
// interface.
repeated xray.common.serial.TypedMessage service = 2;
}
// ReflectionConfig is the placeholder config for ReflectionService.
message ReflectionConfig {}

View file

@ -0,0 +1,9 @@
package commander
import "github.com/xtls/xray-core/v1/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

110
app/commander/outbound.go Normal file
View file

@ -0,0 +1,110 @@
// +build !confonly
package commander
import (
"context"
"sync"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/signal/done"
"github.com/xtls/xray-core/v1/transport"
)
// OutboundListener is a net.Listener for listening gRPC connections.
type OutboundListener struct {
buffer chan net.Conn
done *done.Instance
}
func (l *OutboundListener) add(conn net.Conn) {
select {
case l.buffer <- conn:
case <-l.done.Wait():
conn.Close()
default:
conn.Close()
}
}
// Accept implements net.Listener.
func (l *OutboundListener) Accept() (net.Conn, error) {
select {
case <-l.done.Wait():
return nil, newError("listen closed")
case c := <-l.buffer:
return c, nil
}
}
// Close implement net.Listener.
func (l *OutboundListener) Close() error {
common.Must(l.done.Close())
L:
for {
select {
case c := <-l.buffer:
c.Close()
default:
break L
}
}
return nil
}
// Addr implements net.Listener.
func (l *OutboundListener) Addr() net.Addr {
return &net.TCPAddr{
IP: net.IP{0, 0, 0, 0},
Port: 0,
}
}
// Outbound is a outbound.Handler that handles gRPC connections.
type Outbound struct {
tag string
listener *OutboundListener
access sync.RWMutex
closed bool
}
// Dispatch implements outbound.Handler.
func (co *Outbound) Dispatch(ctx context.Context, link *transport.Link) {
co.access.RLock()
if co.closed {
common.Interrupt(link.Reader)
common.Interrupt(link.Writer)
co.access.RUnlock()
return
}
closeSignal := done.New()
c := net.NewConnection(net.ConnectionInputMulti(link.Writer), net.ConnectionOutputMulti(link.Reader), net.ConnectionOnClose(closeSignal))
co.listener.add(c)
co.access.RUnlock()
<-closeSignal.Wait()
}
// Tag implements outbound.Handler.
func (co *Outbound) Tag() string {
return co.tag
}
// Start implements common.Runnable.
func (co *Outbound) Start() error {
co.access.Lock()
co.closed = false
co.access.Unlock()
return nil
}
// Close implements common.Closable.
func (co *Outbound) Close() error {
co.access.Lock()
defer co.access.Unlock()
co.closed = true
return co.listener.Close()
}

29
app/commander/service.go Normal file
View file

@ -0,0 +1,29 @@
// +build !confonly
package commander
import (
"context"
"github.com/xtls/xray-core/v1/common"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
// Service is a Commander service.
type Service interface {
// Register registers the service itself to a gRPC server.
Register(*grpc.Server)
}
type reflectionService struct{}
func (r reflectionService) Register(s *grpc.Server) {
reflection.Register(s)
}
func init() {
common.Must(common.RegisterConfig((*ReflectionConfig)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) {
return reflectionService{}, nil
}))
}

209
app/dispatcher/config.pb.go Normal file
View file

@ -0,0 +1,209 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.14.0
// source: app/dispatcher/config.proto
package dispatcher
import (
proto "github.com/golang/protobuf/proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
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)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type SessionConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *SessionConfig) Reset() {
*x = SessionConfig{}
if protoimpl.UnsafeEnabled {
mi := &file_app_dispatcher_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SessionConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SessionConfig) ProtoMessage() {}
func (x *SessionConfig) ProtoReflect() protoreflect.Message {
mi := &file_app_dispatcher_config_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SessionConfig.ProtoReflect.Descriptor instead.
func (*SessionConfig) Descriptor() ([]byte, []int) {
return file_app_dispatcher_config_proto_rawDescGZIP(), []int{0}
}
type Config struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Settings *SessionConfig `protobuf:"bytes,1,opt,name=settings,proto3" json:"settings,omitempty"`
}
func (x *Config) Reset() {
*x = Config{}
if protoimpl.UnsafeEnabled {
mi := &file_app_dispatcher_config_proto_msgTypes[1]
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_dispatcher_config_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && 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_dispatcher_config_proto_rawDescGZIP(), []int{1}
}
func (x *Config) GetSettings() *SessionConfig {
if x != nil {
return x.Settings
}
return nil
}
var File_app_dispatcher_config_proto protoreflect.FileDescriptor
var file_app_dispatcher_config_proto_rawDesc = []byte{
0x0a, 0x1b, 0x61, 0x70, 0x70, 0x2f, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72,
0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x13, 0x78,
0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68,
0x65, 0x72, 0x22, 0x15, 0x0a, 0x0d, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x48, 0x0a, 0x06, 0x43, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18,
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x73, 0x73,
0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69,
0x6e, 0x67, 0x73, 0x42, 0x5e, 0x0a, 0x17, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e,
0x61, 0x70, 0x70, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x50, 0x01,
0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c,
0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61,
0x70, 0x70, 0x2f, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0xaa, 0x02, 0x13,
0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63,
0x68, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_app_dispatcher_config_proto_rawDescOnce sync.Once
file_app_dispatcher_config_proto_rawDescData = file_app_dispatcher_config_proto_rawDesc
)
func file_app_dispatcher_config_proto_rawDescGZIP() []byte {
file_app_dispatcher_config_proto_rawDescOnce.Do(func() {
file_app_dispatcher_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_dispatcher_config_proto_rawDescData)
})
return file_app_dispatcher_config_proto_rawDescData
}
var file_app_dispatcher_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_app_dispatcher_config_proto_goTypes = []interface{}{
(*SessionConfig)(nil), // 0: xray.app.dispatcher.SessionConfig
(*Config)(nil), // 1: xray.app.dispatcher.Config
}
var file_app_dispatcher_config_proto_depIdxs = []int32{
0, // 0: xray.app.dispatcher.Config.settings:type_name -> xray.app.dispatcher.SessionConfig
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_dispatcher_config_proto_init() }
func file_app_dispatcher_config_proto_init() {
if File_app_dispatcher_config_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_app_dispatcher_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SessionConfig); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_dispatcher_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_dispatcher_config_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_app_dispatcher_config_proto_goTypes,
DependencyIndexes: file_app_dispatcher_config_proto_depIdxs,
MessageInfos: file_app_dispatcher_config_proto_msgTypes,
}.Build()
File_app_dispatcher_config_proto = out.File
file_app_dispatcher_config_proto_rawDesc = nil
file_app_dispatcher_config_proto_goTypes = nil
file_app_dispatcher_config_proto_depIdxs = nil
}

View file

@ -0,0 +1,15 @@
syntax = "proto3";
package xray.app.dispatcher;
option csharp_namespace = "Xray.App.Dispatcher";
option go_package = "github.com/xtls/xray-core/v1/app/dispatcher";
option java_package = "com.xray.app.dispatcher";
option java_multiple_files = true;
message SessionConfig {
reserved 1;
}
message Config {
SessionConfig settings = 1;
}

301
app/dispatcher/default.go Normal file
View file

@ -0,0 +1,301 @@
// +build !confonly
package dispatcher
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
import (
"context"
"strings"
"sync"
"time"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/buf"
"github.com/xtls/xray-core/v1/common/log"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/protocol"
"github.com/xtls/xray-core/v1/common/session"
"github.com/xtls/xray-core/v1/core"
"github.com/xtls/xray-core/v1/features/outbound"
"github.com/xtls/xray-core/v1/features/policy"
"github.com/xtls/xray-core/v1/features/routing"
routing_session "github.com/xtls/xray-core/v1/features/routing/session"
"github.com/xtls/xray-core/v1/features/stats"
"github.com/xtls/xray-core/v1/transport"
"github.com/xtls/xray-core/v1/transport/pipe"
)
var (
errSniffingTimeout = newError("timeout on sniffing")
)
type cachedReader struct {
sync.Mutex
reader *pipe.Reader
cache buf.MultiBuffer
}
func (r *cachedReader) Cache(b *buf.Buffer) {
mb, _ := r.reader.ReadMultiBufferTimeout(time.Millisecond * 100)
r.Lock()
if !mb.IsEmpty() {
r.cache, _ = buf.MergeMulti(r.cache, mb)
}
b.Clear()
rawBytes := b.Extend(buf.Size)
n := r.cache.Copy(rawBytes)
b.Resize(0, int32(n))
r.Unlock()
}
func (r *cachedReader) readInternal() buf.MultiBuffer {
r.Lock()
defer r.Unlock()
if r.cache != nil && !r.cache.IsEmpty() {
mb := r.cache
r.cache = nil
return mb
}
return nil
}
func (r *cachedReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
mb := r.readInternal()
if mb != nil {
return mb, nil
}
return r.reader.ReadMultiBuffer()
}
func (r *cachedReader) ReadMultiBufferTimeout(timeout time.Duration) (buf.MultiBuffer, error) {
mb := r.readInternal()
if mb != nil {
return mb, nil
}
return r.reader.ReadMultiBufferTimeout(timeout)
}
func (r *cachedReader) Interrupt() {
r.Lock()
if r.cache != nil {
r.cache = buf.ReleaseMulti(r.cache)
}
r.Unlock()
r.reader.Interrupt()
}
// DefaultDispatcher is a default implementation of Dispatcher.
type DefaultDispatcher struct {
ohm outbound.Manager
router routing.Router
policy policy.Manager
stats stats.Manager
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
d := new(DefaultDispatcher)
if err := core.RequireFeatures(ctx, func(om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager) error {
return d.Init(config.(*Config), om, router, pm, sm)
}); err != nil {
return nil, err
}
return d, nil
}))
}
// Init initializes DefaultDispatcher.
func (d *DefaultDispatcher) Init(config *Config, om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager) error {
d.ohm = om
d.router = router
d.policy = pm
d.stats = sm
return nil
}
// Type implements common.HasType.
func (*DefaultDispatcher) Type() interface{} {
return routing.DispatcherType()
}
// Start implements common.Runnable.
func (*DefaultDispatcher) Start() error {
return nil
}
// Close implements common.Closable.
func (*DefaultDispatcher) Close() error { return nil }
func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *transport.Link) {
opt := pipe.OptionsFromContext(ctx)
uplinkReader, uplinkWriter := pipe.New(opt...)
downlinkReader, downlinkWriter := pipe.New(opt...)
inboundLink := &transport.Link{
Reader: downlinkReader,
Writer: uplinkWriter,
}
outboundLink := &transport.Link{
Reader: uplinkReader,
Writer: downlinkWriter,
}
sessionInbound := session.InboundFromContext(ctx)
var user *protocol.MemoryUser
if sessionInbound != nil {
user = sessionInbound.User
}
if user != nil && len(user.Email) > 0 {
p := d.policy.ForLevel(user.Level)
if p.Stats.UserUplink {
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
inboundLink.Writer = &SizeStatWriter{
Counter: c,
Writer: inboundLink.Writer,
}
}
}
if p.Stats.UserDownlink {
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
outboundLink.Writer = &SizeStatWriter{
Counter: c,
Writer: outboundLink.Writer,
}
}
}
}
return inboundLink, outboundLink
}
func shouldOverride(result SniffResult, domainOverride []string) bool {
for _, p := range domainOverride {
if strings.HasPrefix(result.Protocol(), p) {
return true
}
}
return false
}
// Dispatch implements routing.Dispatcher.
func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destination) (*transport.Link, error) {
if !destination.IsValid() {
panic("Dispatcher: Invalid destination.")
}
ob := &session.Outbound{
Target: destination,
}
ctx = session.ContextWithOutbound(ctx, ob)
inbound, outbound := d.getLink(ctx)
content := session.ContentFromContext(ctx)
if content == nil {
content = new(session.Content)
ctx = session.ContextWithContent(ctx, content)
}
sniffingRequest := content.SniffingRequest
if destination.Network != net.Network_TCP || !sniffingRequest.Enabled {
go d.routedDispatch(ctx, outbound, destination)
} else {
go func() {
cReader := &cachedReader{
reader: outbound.Reader.(*pipe.Reader),
}
outbound.Reader = cReader
result, err := sniffer(ctx, cReader)
if err == nil {
content.Protocol = result.Protocol()
}
if err == nil && shouldOverride(result, sniffingRequest.OverrideDestinationForProtocol) {
domain := result.Domain()
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
destination.Address = net.ParseAddress(domain)
ob.Target = destination
}
d.routedDispatch(ctx, outbound, destination)
}()
}
return inbound, nil
}
func sniffer(ctx context.Context, cReader *cachedReader) (SniffResult, error) {
payload := buf.New()
defer payload.Release()
sniffer := NewSniffer()
totalAttempt := 0
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
totalAttempt++
if totalAttempt > 2 {
return nil, errSniffingTimeout
}
cReader.Cache(payload)
if !payload.IsEmpty() {
result, err := sniffer.Sniff(payload.Bytes())
if err != common.ErrNoClue {
return result, err
}
}
if payload.IsFull() {
return nil, errUnknownContent
}
}
}
}
func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination) {
var handler outbound.Handler
skipRoutePick := false
if content := session.ContentFromContext(ctx); content != nil {
skipRoutePick = content.SkipRoutePick
}
if d.router != nil && !skipRoutePick {
if route, err := d.router.PickRoute(routing_session.AsRoutingContext(ctx)); err == nil {
tag := route.GetOutboundTag()
if h := d.ohm.GetHandler(tag); h != nil {
newError("taking detour [", tag, "] for [", destination, "]").WriteToLog(session.ExportIDToError(ctx))
handler = h
} else {
newError("non existing tag: ", tag).AtWarning().WriteToLog(session.ExportIDToError(ctx))
}
} else {
newError("default route for ", destination).WriteToLog(session.ExportIDToError(ctx))
}
}
if handler == nil {
handler = d.ohm.GetDefaultHandler()
}
if handler == nil {
newError("default outbound handler not exist").WriteToLog(session.ExportIDToError(ctx))
common.Close(link.Writer)
common.Interrupt(link.Reader)
return
}
if accessMessage := log.AccessMessageFromContext(ctx); accessMessage != nil {
if tag := handler.Tag(); tag != "" {
accessMessage.Detour = tag
}
log.Record(accessMessage)
}
handler.Dispatch(ctx, link)
}

View file

@ -0,0 +1,5 @@
// +build !confonly
package dispatcher
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen

View file

@ -0,0 +1,9 @@
package dispatcher
import "github.com/xtls/xray-core/v1/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

55
app/dispatcher/sniffer.go Normal file
View file

@ -0,0 +1,55 @@
// +build !confonly
package dispatcher
import (
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/protocol/bittorrent"
"github.com/xtls/xray-core/v1/common/protocol/http"
"github.com/xtls/xray-core/v1/common/protocol/tls"
)
type SniffResult interface {
Protocol() string
Domain() string
}
type protocolSniffer func([]byte) (SniffResult, error)
type Sniffer struct {
sniffer []protocolSniffer
}
func NewSniffer() *Sniffer {
return &Sniffer{
sniffer: []protocolSniffer{
func(b []byte) (SniffResult, error) { return http.SniffHTTP(b) },
func(b []byte) (SniffResult, error) { return tls.SniffTLS(b) },
func(b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) },
},
}
}
var errUnknownContent = newError("unknown content")
func (s *Sniffer) Sniff(payload []byte) (SniffResult, error) {
var pendingSniffer []protocolSniffer
for _, s := range s.sniffer {
result, err := s(payload)
if err == common.ErrNoClue {
pendingSniffer = append(pendingSniffer, s)
continue
}
if err == nil && result != nil {
return result, nil
}
}
if len(pendingSniffer) > 0 {
s.sniffer = pendingSniffer
return nil, common.ErrNoClue
}
return nil, errUnknownContent
}

27
app/dispatcher/stats.go Normal file
View file

@ -0,0 +1,27 @@
// +build !confonly
package dispatcher
import (
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/buf"
"github.com/xtls/xray-core/v1/features/stats"
)
type SizeStatWriter struct {
Counter stats.Counter
Writer buf.Writer
}
func (w *SizeStatWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
w.Counter.Add(int64(mb.Len()))
return w.Writer.WriteMultiBuffer(mb)
}
func (w *SizeStatWriter) Close() error {
return common.Close(w.Writer)
}
func (w *SizeStatWriter) Interrupt() {
common.Interrupt(w.Writer)
}

View file

@ -0,0 +1,44 @@
package dispatcher_test
import (
"testing"
. "github.com/xtls/xray-core/v1/app/dispatcher"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/buf"
)
type TestCounter int64
func (c *TestCounter) Value() int64 {
return int64(*c)
}
func (c *TestCounter) Add(v int64) int64 {
x := int64(*c) + v
*c = TestCounter(x)
return x
}
func (c *TestCounter) Set(v int64) int64 {
*c = TestCounter(v)
return v
}
func TestStatsWriter(t *testing.T) {
var c TestCounter
writer := &SizeStatWriter{
Counter: &c,
Writer: buf.Discard,
}
mb := buf.MergeBytes(nil, []byte("abcd"))
common.Must(writer.WriteMultiBuffer(mb))
mb = buf.MergeBytes(nil, []byte("efg"))
common.Must(writer.WriteMultiBuffer(mb))
if c.Value() != 7 {
t.Fatal("unexpected counter value. want 7, but got ", c.Value())
}
}

654
app/dns/config.pb.go Normal file
View file

@ -0,0 +1,654 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.14.0
// source: app/dns/config.proto
package dns
import (
proto "github.com/golang/protobuf/proto"
router "github.com/xtls/xray-core/v1/app/router"
net "github.com/xtls/xray-core/v1/common/net"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
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)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type DomainMatchingType int32
const (
DomainMatchingType_Full DomainMatchingType = 0
DomainMatchingType_Subdomain DomainMatchingType = 1
DomainMatchingType_Keyword DomainMatchingType = 2
DomainMatchingType_Regex DomainMatchingType = 3
)
// Enum value maps for DomainMatchingType.
var (
DomainMatchingType_name = map[int32]string{
0: "Full",
1: "Subdomain",
2: "Keyword",
3: "Regex",
}
DomainMatchingType_value = map[string]int32{
"Full": 0,
"Subdomain": 1,
"Keyword": 2,
"Regex": 3,
}
)
func (x DomainMatchingType) Enum() *DomainMatchingType {
p := new(DomainMatchingType)
*p = x
return p
}
func (x DomainMatchingType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (DomainMatchingType) Descriptor() protoreflect.EnumDescriptor {
return file_app_dns_config_proto_enumTypes[0].Descriptor()
}
func (DomainMatchingType) Type() protoreflect.EnumType {
return &file_app_dns_config_proto_enumTypes[0]
}
func (x DomainMatchingType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use DomainMatchingType.Descriptor instead.
func (DomainMatchingType) EnumDescriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{0}
}
type NameServer struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Address *net.Endpoint `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
PrioritizedDomain []*NameServer_PriorityDomain `protobuf:"bytes,2,rep,name=prioritized_domain,json=prioritizedDomain,proto3" json:"prioritized_domain,omitempty"`
Geoip []*router.GeoIP `protobuf:"bytes,3,rep,name=geoip,proto3" json:"geoip,omitempty"`
OriginalRules []*NameServer_OriginalRule `protobuf:"bytes,4,rep,name=original_rules,json=originalRules,proto3" json:"original_rules,omitempty"`
}
func (x *NameServer) Reset() {
*x = NameServer{}
if protoimpl.UnsafeEnabled {
mi := &file_app_dns_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *NameServer) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NameServer) ProtoMessage() {}
func (x *NameServer) ProtoReflect() protoreflect.Message {
mi := &file_app_dns_config_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NameServer.ProtoReflect.Descriptor instead.
func (*NameServer) Descriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{0}
}
func (x *NameServer) GetAddress() *net.Endpoint {
if x != nil {
return x.Address
}
return nil
}
func (x *NameServer) GetPrioritizedDomain() []*NameServer_PriorityDomain {
if x != nil {
return x.PrioritizedDomain
}
return nil
}
func (x *NameServer) GetGeoip() []*router.GeoIP {
if x != nil {
return x.Geoip
}
return nil
}
func (x *NameServer) GetOriginalRules() []*NameServer_OriginalRule {
if x != nil {
return x.OriginalRules
}
return nil
}
type Config struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Nameservers used by this DNS. Only traditional UDP servers are support at
// the moment. A special value 'localhost' as a domain address can be set to
// use DNS on local system.
//
// Deprecated: Do not use.
NameServers []*net.Endpoint `protobuf:"bytes,1,rep,name=NameServers,proto3" json:"NameServers,omitempty"`
// NameServer list used by this DNS client.
NameServer []*NameServer `protobuf:"bytes,5,rep,name=name_server,json=nameServer,proto3" json:"name_server,omitempty"`
// Static hosts. Domain to IP.
// Deprecated. Use static_hosts.
//
// Deprecated: Do not use.
Hosts map[string]*net.IPOrDomain `protobuf:"bytes,2,rep,name=Hosts,proto3" json:"Hosts,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
// Client IP for EDNS client subnet. Must be 4 bytes (IPv4) or 16 bytes
// (IPv6).
ClientIp []byte `protobuf:"bytes,3,opt,name=client_ip,json=clientIp,proto3" json:"client_ip,omitempty"`
StaticHosts []*Config_HostMapping `protobuf:"bytes,4,rep,name=static_hosts,json=staticHosts,proto3" json:"static_hosts,omitempty"`
// Tag is the inbound tag of DNS client.
Tag string `protobuf:"bytes,6,opt,name=tag,proto3" json:"tag,omitempty"`
}
func (x *Config) Reset() {
*x = Config{}
if protoimpl.UnsafeEnabled {
mi := &file_app_dns_config_proto_msgTypes[1]
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_dns_config_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && 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_dns_config_proto_rawDescGZIP(), []int{1}
}
// Deprecated: Do not use.
func (x *Config) GetNameServers() []*net.Endpoint {
if x != nil {
return x.NameServers
}
return nil
}
func (x *Config) GetNameServer() []*NameServer {
if x != nil {
return x.NameServer
}
return nil
}
// Deprecated: Do not use.
func (x *Config) GetHosts() map[string]*net.IPOrDomain {
if x != nil {
return x.Hosts
}
return nil
}
func (x *Config) GetClientIp() []byte {
if x != nil {
return x.ClientIp
}
return nil
}
func (x *Config) GetStaticHosts() []*Config_HostMapping {
if x != nil {
return x.StaticHosts
}
return nil
}
func (x *Config) GetTag() string {
if x != nil {
return x.Tag
}
return ""
}
type NameServer_PriorityDomain struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Type DomainMatchingType `protobuf:"varint,1,opt,name=type,proto3,enum=xray.app.dns.DomainMatchingType" json:"type,omitempty"`
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
}
func (x *NameServer_PriorityDomain) Reset() {
*x = NameServer_PriorityDomain{}
if protoimpl.UnsafeEnabled {
mi := &file_app_dns_config_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *NameServer_PriorityDomain) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NameServer_PriorityDomain) ProtoMessage() {}
func (x *NameServer_PriorityDomain) ProtoReflect() protoreflect.Message {
mi := &file_app_dns_config_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NameServer_PriorityDomain.ProtoReflect.Descriptor instead.
func (*NameServer_PriorityDomain) Descriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{0, 0}
}
func (x *NameServer_PriorityDomain) GetType() DomainMatchingType {
if x != nil {
return x.Type
}
return DomainMatchingType_Full
}
func (x *NameServer_PriorityDomain) GetDomain() string {
if x != nil {
return x.Domain
}
return ""
}
type NameServer_OriginalRule struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Rule string `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"`
Size uint32 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"`
}
func (x *NameServer_OriginalRule) Reset() {
*x = NameServer_OriginalRule{}
if protoimpl.UnsafeEnabled {
mi := &file_app_dns_config_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *NameServer_OriginalRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NameServer_OriginalRule) ProtoMessage() {}
func (x *NameServer_OriginalRule) ProtoReflect() protoreflect.Message {
mi := &file_app_dns_config_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NameServer_OriginalRule.ProtoReflect.Descriptor instead.
func (*NameServer_OriginalRule) Descriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{0, 1}
}
func (x *NameServer_OriginalRule) GetRule() string {
if x != nil {
return x.Rule
}
return ""
}
func (x *NameServer_OriginalRule) GetSize() uint32 {
if x != nil {
return x.Size
}
return 0
}
type Config_HostMapping struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Type DomainMatchingType `protobuf:"varint,1,opt,name=type,proto3,enum=xray.app.dns.DomainMatchingType" json:"type,omitempty"`
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
Ip [][]byte `protobuf:"bytes,3,rep,name=ip,proto3" json:"ip,omitempty"`
// ProxiedDomain indicates the mapped domain has the same IP address on this
// domain. Xray will use this domain for IP queries. This field is only
// effective if ip is empty.
ProxiedDomain string `protobuf:"bytes,4,opt,name=proxied_domain,json=proxiedDomain,proto3" json:"proxied_domain,omitempty"`
}
func (x *Config_HostMapping) Reset() {
*x = Config_HostMapping{}
if protoimpl.UnsafeEnabled {
mi := &file_app_dns_config_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Config_HostMapping) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config_HostMapping) ProtoMessage() {}
func (x *Config_HostMapping) ProtoReflect() protoreflect.Message {
mi := &file_app_dns_config_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && 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_HostMapping.ProtoReflect.Descriptor instead.
func (*Config_HostMapping) Descriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{1, 1}
}
func (x *Config_HostMapping) GetType() DomainMatchingType {
if x != nil {
return x.Type
}
return DomainMatchingType_Full
}
func (x *Config_HostMapping) GetDomain() string {
if x != nil {
return x.Domain
}
return ""
}
func (x *Config_HostMapping) GetIp() [][]byte {
if x != nil {
return x.Ip
}
return nil
}
func (x *Config_HostMapping) GetProxiedDomain() string {
if x != nil {
return x.ProxiedDomain
}
return ""
}
var File_app_dns_config_proto protoreflect.FileDescriptor
var file_app_dns_config_proto_rawDesc = []byte{
0x0a, 0x14, 0x61, 0x70, 0x70, 0x2f, 0x64, 0x6e, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
0x2e, 0x64, 0x6e, 0x73, 0x1a, 0x18, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74,
0x2f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c,
0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74, 0x2f, 0x64, 0x65, 0x73, 0x74, 0x69,
0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x61, 0x70,
0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xad, 0x03, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65,
0x72, 0x76, 0x65, 0x72, 0x12, 0x33, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18,
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d,
0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x56, 0x0a, 0x12, 0x70, 0x72, 0x69,
0x6f, 0x72, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18,
0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e,
0x50, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x11,
0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x64, 0x44, 0x6f, 0x6d, 0x61, 0x69,
0x6e, 0x12, 0x2c, 0x0a, 0x05, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x16, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74,
0x65, 0x72, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x52, 0x05, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x12,
0x4c, 0x0a, 0x0e, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x75, 0x6c, 0x65,
0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61,
0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65,
0x72, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d,
0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x1a, 0x5e, 0x0a,
0x0e, 0x50, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e,
0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d,
0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52,
0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x1a, 0x36, 0x0a,
0x0c, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a,
0x04, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x75, 0x6c,
0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
0x04, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x9f, 0x04, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x12, 0x3f, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18,
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d,
0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
0x42, 0x02, 0x18, 0x01, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
0x73, 0x12, 0x39, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70,
0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x05,
0x48, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x78, 0x72,
0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x02, 0x18, 0x01,
0x52, 0x05, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e,
0x74, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65,
0x6e, 0x74, 0x49, 0x70, 0x12, 0x43, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x5f, 0x68,
0x6f, 0x73, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61,
0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x2e, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0b, 0x73, 0x74,
0x61, 0x74, 0x69, 0x63, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67,
0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x1a, 0x55, 0x0a, 0x0a, 0x48,
0x6f, 0x73, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x05, 0x76,
0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61,
0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x49, 0x50, 0x4f,
0x72, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
0x38, 0x01, 0x1a, 0x92, 0x01, 0x0a, 0x0b, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69,
0x6e, 0x67, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e,
0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e,
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79,
0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61,
0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70,
0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65, 0x64, 0x5f, 0x64, 0x6f, 0x6d, 0x61,
0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65,
0x64, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2a, 0x45, 0x0a, 0x12, 0x44, 0x6f, 0x6d, 0x61, 0x69,
0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a,
0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x64, 0x6f,
0x6d, 0x61, 0x69, 0x6e, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72,
0x64, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x03, 0x42, 0x49,
0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64,
0x6e, 0x73, 0x50, 0x01, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f,
0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x64, 0x6e, 0x73, 0xaa, 0x02, 0x0c, 0x58, 0x72, 0x61,
0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x44, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
}
var (
file_app_dns_config_proto_rawDescOnce sync.Once
file_app_dns_config_proto_rawDescData = file_app_dns_config_proto_rawDesc
)
func file_app_dns_config_proto_rawDescGZIP() []byte {
file_app_dns_config_proto_rawDescOnce.Do(func() {
file_app_dns_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_dns_config_proto_rawDescData)
})
return file_app_dns_config_proto_rawDescData
}
var file_app_dns_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_app_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_app_dns_config_proto_goTypes = []interface{}{
(DomainMatchingType)(0), // 0: xray.app.dns.DomainMatchingType
(*NameServer)(nil), // 1: xray.app.dns.NameServer
(*Config)(nil), // 2: xray.app.dns.Config
(*NameServer_PriorityDomain)(nil), // 3: xray.app.dns.NameServer.PriorityDomain
(*NameServer_OriginalRule)(nil), // 4: xray.app.dns.NameServer.OriginalRule
nil, // 5: xray.app.dns.Config.HostsEntry
(*Config_HostMapping)(nil), // 6: xray.app.dns.Config.HostMapping
(*net.Endpoint)(nil), // 7: xray.common.net.Endpoint
(*router.GeoIP)(nil), // 8: xray.app.router.GeoIP
(*net.IPOrDomain)(nil), // 9: xray.common.net.IPOrDomain
}
var file_app_dns_config_proto_depIdxs = []int32{
7, // 0: xray.app.dns.NameServer.address:type_name -> xray.common.net.Endpoint
3, // 1: xray.app.dns.NameServer.prioritized_domain:type_name -> xray.app.dns.NameServer.PriorityDomain
8, // 2: xray.app.dns.NameServer.geoip:type_name -> xray.app.router.GeoIP
4, // 3: xray.app.dns.NameServer.original_rules:type_name -> xray.app.dns.NameServer.OriginalRule
7, // 4: xray.app.dns.Config.NameServers:type_name -> xray.common.net.Endpoint
1, // 5: xray.app.dns.Config.name_server:type_name -> xray.app.dns.NameServer
5, // 6: xray.app.dns.Config.Hosts:type_name -> xray.app.dns.Config.HostsEntry
6, // 7: xray.app.dns.Config.static_hosts:type_name -> xray.app.dns.Config.HostMapping
0, // 8: xray.app.dns.NameServer.PriorityDomain.type:type_name -> xray.app.dns.DomainMatchingType
9, // 9: xray.app.dns.Config.HostsEntry.value:type_name -> xray.common.net.IPOrDomain
0, // 10: xray.app.dns.Config.HostMapping.type:type_name -> xray.app.dns.DomainMatchingType
11, // [11:11] is the sub-list for method output_type
11, // [11:11] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
}
func init() { file_app_dns_config_proto_init() }
func file_app_dns_config_proto_init() {
if File_app_dns_config_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_app_dns_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NameServer); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_dns_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_dns_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NameServer_PriorityDomain); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_dns_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NameServer_OriginalRule); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_dns_config_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config_HostMapping); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_dns_config_proto_rawDesc,
NumEnums: 1,
NumMessages: 6,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_app_dns_config_proto_goTypes,
DependencyIndexes: file_app_dns_config_proto_depIdxs,
EnumInfos: file_app_dns_config_proto_enumTypes,
MessageInfos: file_app_dns_config_proto_msgTypes,
}.Build()
File_app_dns_config_proto = out.File
file_app_dns_config_proto_rawDesc = nil
file_app_dns_config_proto_goTypes = nil
file_app_dns_config_proto_depIdxs = nil
}

71
app/dns/config.proto Normal file
View file

@ -0,0 +1,71 @@
syntax = "proto3";
package xray.app.dns;
option csharp_namespace = "Xray.App.Dns";
option go_package = "github.com/xtls/xray-core/v1/app/dns";
option java_package = "com.xray.app.dns";
option java_multiple_files = true;
import "common/net/address.proto";
import "common/net/destination.proto";
import "app/router/config.proto";
message NameServer {
xray.common.net.Endpoint address = 1;
message PriorityDomain {
DomainMatchingType type = 1;
string domain = 2;
}
message OriginalRule {
string rule = 1;
uint32 size = 2;
}
repeated PriorityDomain prioritized_domain = 2;
repeated xray.app.router.GeoIP geoip = 3;
repeated OriginalRule original_rules = 4;
}
enum DomainMatchingType {
Full = 0;
Subdomain = 1;
Keyword = 2;
Regex = 3;
}
message Config {
// Nameservers used by this DNS. Only traditional UDP servers are support at
// the moment. A special value 'localhost' as a domain address can be set to
// use DNS on local system.
repeated xray.common.net.Endpoint NameServers = 1 [deprecated = true];
// NameServer list used by this DNS client.
repeated NameServer name_server = 5;
// Static hosts. Domain to IP.
// Deprecated. Use static_hosts.
map<string, xray.common.net.IPOrDomain> Hosts = 2 [deprecated = true];
// Client IP for EDNS client subnet. Must be 4 bytes (IPv4) or 16 bytes
// (IPv6).
bytes client_ip = 3;
message HostMapping {
DomainMatchingType type = 1;
string domain = 2;
repeated bytes ip = 3;
// ProxiedDomain indicates the mapped domain has the same IP address on this
// domain. Xray will use this domain for IP queries. This field is only
// effective if ip is empty.
string proxied_domain = 4;
}
repeated HostMapping static_hosts = 4;
// Tag is the inbound tag of DNS client.
string tag = 6;
}

4
app/dns/dns.go Normal file
View file

@ -0,0 +1,4 @@
// Package dns is an implementation of core.DNS feature.
package dns
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen

230
app/dns/dnscommon.go Normal file
View file

@ -0,0 +1,230 @@
// +build !confonly
package dns
import (
"encoding/binary"
"time"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/errors"
"github.com/xtls/xray-core/v1/common/net"
dns_feature "github.com/xtls/xray-core/v1/features/dns"
"golang.org/x/net/dns/dnsmessage"
)
// Fqdn normalize domain make sure it ends with '.'
func Fqdn(domain string) string {
if len(domain) > 0 && domain[len(domain)-1] == '.' {
return domain
}
return domain + "."
}
type record struct {
A *IPRecord
AAAA *IPRecord
}
// IPRecord is a cacheable item for a resolved domain
type IPRecord struct {
ReqID uint16
IP []net.Address
Expire time.Time
RCode dnsmessage.RCode
}
func (r *IPRecord) getIPs() ([]net.Address, error) {
if r == nil || r.Expire.Before(time.Now()) {
return nil, errRecordNotFound
}
if r.RCode != dnsmessage.RCodeSuccess {
return nil, dns_feature.RCodeError(r.RCode)
}
return r.IP, nil
}
func isNewer(baseRec *IPRecord, newRec *IPRecord) bool {
if newRec == nil {
return false
}
if baseRec == nil {
return true
}
return baseRec.Expire.Before(newRec.Expire)
}
var (
errRecordNotFound = errors.New("record not found")
)
type dnsRequest struct {
reqType dnsmessage.Type
domain string
start time.Time
expire time.Time
msg *dnsmessage.Message
}
func genEDNS0Options(clientIP net.IP) *dnsmessage.Resource {
if len(clientIP) == 0 {
return nil
}
var netmask int
var family uint16
if len(clientIP) == 4 {
family = 1
netmask = 24 // 24 for IPV4, 96 for IPv6
} else {
family = 2
netmask = 96
}
b := make([]byte, 4)
binary.BigEndian.PutUint16(b[0:], family)
b[2] = byte(netmask)
b[3] = 0
switch family {
case 1:
ip := clientIP.To4().Mask(net.CIDRMask(netmask, net.IPv4len*8))
needLength := (netmask + 8 - 1) / 8 // division rounding up
b = append(b, ip[:needLength]...)
case 2:
ip := clientIP.Mask(net.CIDRMask(netmask, net.IPv6len*8))
needLength := (netmask + 8 - 1) / 8 // division rounding up
b = append(b, ip[:needLength]...)
}
const EDNS0SUBNET = 0x08
opt := new(dnsmessage.Resource)
common.Must(opt.Header.SetEDNS0(1350, 0xfe00, true))
opt.Body = &dnsmessage.OPTResource{
Options: []dnsmessage.Option{
{
Code: EDNS0SUBNET,
Data: b,
},
},
}
return opt
}
func buildReqMsgs(domain string, option IPOption, reqIDGen func() uint16, reqOpts *dnsmessage.Resource) []*dnsRequest {
qA := dnsmessage.Question{
Name: dnsmessage.MustNewName(domain),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
}
qAAAA := dnsmessage.Question{
Name: dnsmessage.MustNewName(domain),
Type: dnsmessage.TypeAAAA,
Class: dnsmessage.ClassINET,
}
var reqs []*dnsRequest
now := time.Now()
if option.IPv4Enable {
msg := new(dnsmessage.Message)
msg.Header.ID = reqIDGen()
msg.Header.RecursionDesired = true
msg.Questions = []dnsmessage.Question{qA}
if reqOpts != nil {
msg.Additionals = append(msg.Additionals, *reqOpts)
}
reqs = append(reqs, &dnsRequest{
reqType: dnsmessage.TypeA,
domain: domain,
start: now,
msg: msg,
})
}
if option.IPv6Enable {
msg := new(dnsmessage.Message)
msg.Header.ID = reqIDGen()
msg.Header.RecursionDesired = true
msg.Questions = []dnsmessage.Question{qAAAA}
if reqOpts != nil {
msg.Additionals = append(msg.Additionals, *reqOpts)
}
reqs = append(reqs, &dnsRequest{
reqType: dnsmessage.TypeAAAA,
domain: domain,
start: now,
msg: msg,
})
}
return reqs
}
// parseResponse parse DNS answers from the returned payload
func parseResponse(payload []byte) (*IPRecord, error) {
var parser dnsmessage.Parser
h, err := parser.Start(payload)
if err != nil {
return nil, newError("failed to parse DNS response").Base(err).AtWarning()
}
if err := parser.SkipAllQuestions(); err != nil {
return nil, newError("failed to skip questions in DNS response").Base(err).AtWarning()
}
now := time.Now()
ipRecord := &IPRecord{
ReqID: h.ID,
RCode: h.RCode,
Expire: now.Add(time.Second * 600),
}
L:
for {
ah, err := parser.AnswerHeader()
if err != nil {
if err != dnsmessage.ErrSectionDone {
newError("failed to parse answer section for domain: ", ah.Name.String()).Base(err).WriteToLog()
}
break
}
ttl := ah.TTL
if ttl == 0 {
ttl = 600
}
expire := now.Add(time.Duration(ttl) * time.Second)
if ipRecord.Expire.After(expire) {
ipRecord.Expire = expire
}
switch ah.Type {
case dnsmessage.TypeA:
ans, err := parser.AResource()
if err != nil {
newError("failed to parse A record for domain: ", ah.Name).Base(err).WriteToLog()
break L
}
ipRecord.IP = append(ipRecord.IP, net.IPAddress(ans.A[:]))
case dnsmessage.TypeAAAA:
ans, err := parser.AAAAResource()
if err != nil {
newError("failed to parse A record for domain: ", ah.Name).Base(err).WriteToLog()
break L
}
ipRecord.IP = append(ipRecord.IP, net.IPAddress(ans.AAAA[:]))
default:
if err := parser.SkipAnswer(); err != nil {
newError("failed to skip answer").Base(err).WriteToLog()
break L
}
continue
}
}
return ipRecord, nil
}

160
app/dns/dnscommon_test.go Normal file
View file

@ -0,0 +1,160 @@
// +build !confonly
package dns
import (
"math/rand"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/miekg/dns"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/net"
"golang.org/x/net/dns/dnsmessage"
)
func Test_parseResponse(t *testing.T) {
var p [][]byte
ans := new(dns.Msg)
ans.Id = 0
p = append(p, common.Must2(ans.Pack()).([]byte))
p = append(p, []byte{})
ans = new(dns.Msg)
ans.Id = 1
ans.Answer = append(ans.Answer,
common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")).(dns.RR),
common.Must2(dns.NewRR("google.com. IN CNAME fake.google.com")).(dns.RR),
common.Must2(dns.NewRR("google.com. IN A 8.8.8.8")).(dns.RR),
common.Must2(dns.NewRR("google.com. IN A 8.8.4.4")).(dns.RR),
)
p = append(p, common.Must2(ans.Pack()).([]byte))
ans = new(dns.Msg)
ans.Id = 2
ans.Answer = append(ans.Answer,
common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")).(dns.RR),
common.Must2(dns.NewRR("google.com. IN CNAME fake.google.com")).(dns.RR),
common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")).(dns.RR),
common.Must2(dns.NewRR("google.com. IN CNAME test.google.com")).(dns.RR),
common.Must2(dns.NewRR("google.com. IN AAAA 2001::123:8888")).(dns.RR),
common.Must2(dns.NewRR("google.com. IN AAAA 2001::123:8844")).(dns.RR),
)
p = append(p, common.Must2(ans.Pack()).([]byte))
tests := []struct {
name string
want *IPRecord
wantErr bool
}{
{"empty",
&IPRecord{0, []net.Address(nil), time.Time{}, dnsmessage.RCodeSuccess},
false,
},
{"error",
nil,
true,
},
{"a record",
&IPRecord{1, []net.Address{net.ParseAddress("8.8.8.8"), net.ParseAddress("8.8.4.4")},
time.Time{}, dnsmessage.RCodeSuccess},
false,
},
{"aaaa record",
&IPRecord{2, []net.Address{net.ParseAddress("2001::123:8888"), net.ParseAddress("2001::123:8844")}, time.Time{}, dnsmessage.RCodeSuccess},
false,
},
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseResponse(p[i])
if (err != nil) != tt.wantErr {
t.Errorf("handleResponse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != nil {
// reset the time
got.Expire = time.Time{}
}
if cmp.Diff(got, tt.want) != "" {
t.Errorf(cmp.Diff(got, tt.want))
// t.Errorf("handleResponse() = %#v, want %#v", got, tt.want)
}
})
}
}
func Test_buildReqMsgs(t *testing.T) {
stubID := func() uint16 {
return uint16(rand.Uint32())
}
type args struct {
domain string
option IPOption
reqOpts *dnsmessage.Resource
}
tests := []struct {
name string
args args
want int
}{
{"dual stack", args{"test.com", IPOption{true, true}, nil}, 2},
{"ipv4 only", args{"test.com", IPOption{true, false}, nil}, 1},
{"ipv6 only", args{"test.com", IPOption{false, true}, nil}, 1},
{"none/error", args{"test.com", IPOption{false, false}, nil}, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := buildReqMsgs(tt.args.domain, tt.args.option, stubID, tt.args.reqOpts); !(len(got) == tt.want) {
t.Errorf("buildReqMsgs() = %v, want %v", got, tt.want)
}
})
}
}
func Test_genEDNS0Options(t *testing.T) {
type args struct {
clientIP net.IP
}
tests := []struct {
name string
args args
want *dnsmessage.Resource
}{
// TODO: Add test cases.
{"ipv4", args{net.ParseIP("4.3.2.1")}, nil},
{"ipv6", args{net.ParseIP("2001::4321")}, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := genEDNS0Options(tt.args.clientIP); got == nil {
t.Errorf("genEDNS0Options() = %v, want %v", got, tt.want)
}
})
}
}
func TestFqdn(t *testing.T) {
type args struct {
domain string
}
tests := []struct {
name string
args args
want string
}{
{"with fqdn", args{"www.example.com."}, "www.example.com."},
{"without fqdn", args{"www.example.com"}, "www.example.com."},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Fqdn(tt.args.domain); got != tt.want {
t.Errorf("Fqdn() = %v, want %v", got, tt.want)
}
})
}
}

383
app/dns/dohdns.go Normal file
View file

@ -0,0 +1,383 @@
// +build !confonly
package dns
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"sync"
"sync/atomic"
"time"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/protocol/dns"
"github.com/xtls/xray-core/v1/common/session"
"github.com/xtls/xray-core/v1/common/signal/pubsub"
"github.com/xtls/xray-core/v1/common/task"
dns_feature "github.com/xtls/xray-core/v1/features/dns"
"github.com/xtls/xray-core/v1/features/routing"
"github.com/xtls/xray-core/v1/transport/internet"
"golang.org/x/net/dns/dnsmessage"
)
// DoHNameServer implemented DNS over HTTPS (RFC8484) Wire Format,
// which is compatible with traditional dns over udp(RFC1035),
// thus most of the DOH implementation is copied from udpns.go
type DoHNameServer struct {
sync.RWMutex
ips map[string]record
pub *pubsub.Service
cleanup *task.Periodic
reqID uint32
clientIP net.IP
httpClient *http.Client
dohURL string
name string
}
// NewDoHNameServer creates DOH client object for remote resolving
func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, clientIP net.IP) (*DoHNameServer, error) {
newError("DNS: created Remote DOH client for ", url.String()).AtInfo().WriteToLog()
s := baseDOHNameServer(url, "DOH", clientIP)
// Dispatched connection will be closed (interrupted) after each request
// This makes DOH inefficient without a keep-alived connection
// See: core/app/proxyman/outbound/handler.go:113
// Using mux (https request wrapped in a stream layer) improves the situation.
// Recommend to use NewDoHLocalNameServer (DOHL:) if xray instance is running on
// a normal network eg. the server side of xray
tr := &http.Transport{
MaxIdleConns: 30,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 30 * time.Second,
ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dest, err := net.ParseDestination(network + ":" + addr)
if err != nil {
return nil, err
}
link, err := dispatcher.Dispatch(ctx, dest)
if err != nil {
return nil, err
}
return net.NewConnection(
net.ConnectionInputMulti(link.Writer),
net.ConnectionOutputMulti(link.Reader),
), nil
},
}
dispatchedClient := &http.Client{
Transport: tr,
Timeout: 60 * time.Second,
}
s.httpClient = dispatchedClient
return s, nil
}
// NewDoHLocalNameServer creates DOH client object for local resolving
func NewDoHLocalNameServer(url *url.URL, clientIP net.IP) *DoHNameServer {
url.Scheme = "https"
s := baseDOHNameServer(url, "DOHL", clientIP)
tr := &http.Transport{
IdleConnTimeout: 90 * time.Second,
ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dest, err := net.ParseDestination(network + ":" + addr)
if err != nil {
return nil, err
}
conn, err := internet.DialSystem(ctx, dest, nil)
if err != nil {
return nil, err
}
return conn, nil
},
}
s.httpClient = &http.Client{
Timeout: time.Second * 180,
Transport: tr,
}
newError("DNS: created Local DOH client for ", url.String()).AtInfo().WriteToLog()
return s
}
func baseDOHNameServer(url *url.URL, prefix string, clientIP net.IP) *DoHNameServer {
s := &DoHNameServer{
ips: make(map[string]record),
clientIP: clientIP,
pub: pubsub.NewService(),
name: prefix + "//" + url.Host,
dohURL: url.String(),
}
s.cleanup = &task.Periodic{
Interval: time.Minute,
Execute: s.Cleanup,
}
return s
}
// Name returns client name
func (s *DoHNameServer) Name() string {
return s.name
}
// Cleanup clears expired items from cache
func (s *DoHNameServer) Cleanup() error {
now := time.Now()
s.Lock()
defer s.Unlock()
if len(s.ips) == 0 {
return newError("nothing to do. stopping...")
}
for domain, record := range s.ips {
if record.A != nil && record.A.Expire.Before(now) {
record.A = nil
}
if record.AAAA != nil && record.AAAA.Expire.Before(now) {
record.AAAA = nil
}
if record.A == nil && record.AAAA == nil {
newError(s.name, " cleanup ", domain).AtDebug().WriteToLog()
delete(s.ips, domain)
} else {
s.ips[domain] = record
}
}
if len(s.ips) == 0 {
s.ips = make(map[string]record)
}
return nil
}
func (s *DoHNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) {
elapsed := time.Since(req.start)
s.Lock()
rec := s.ips[req.domain]
updated := false
switch req.reqType {
case dnsmessage.TypeA:
if isNewer(rec.A, ipRec) {
rec.A = ipRec
updated = true
}
case dnsmessage.TypeAAAA:
addr := make([]net.Address, 0)
for _, ip := range ipRec.IP {
if len(ip.IP()) == net.IPv6len {
addr = append(addr, ip)
}
}
ipRec.IP = addr
if isNewer(rec.AAAA, ipRec) {
rec.AAAA = ipRec
updated = true
}
}
newError(s.name, " got answer: ", req.domain, " ", req.reqType, " -> ", ipRec.IP, " ", elapsed).AtInfo().WriteToLog()
if updated {
s.ips[req.domain] = rec
}
switch req.reqType {
case dnsmessage.TypeA:
s.pub.Publish(req.domain+"4", nil)
case dnsmessage.TypeAAAA:
s.pub.Publish(req.domain+"6", nil)
}
s.Unlock()
common.Must(s.cleanup.Start())
}
func (s *DoHNameServer) newReqID() uint16 {
return uint16(atomic.AddUint32(&s.reqID, 1))
}
func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, option IPOption) {
newError(s.name, " querying: ", domain).AtInfo().WriteToLog(session.ExportIDToError(ctx))
reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP))
var deadline time.Time
if d, ok := ctx.Deadline(); ok {
deadline = d
} else {
deadline = time.Now().Add(time.Second * 5)
}
for _, req := range reqs {
go func(r *dnsRequest) {
// generate new context for each req, using same context
// may cause reqs all aborted if any one encounter an error
dnsCtx := context.Background()
// reserve internal dns server requested Inbound
if inbound := session.InboundFromContext(ctx); inbound != nil {
dnsCtx = session.ContextWithInbound(dnsCtx, inbound)
}
dnsCtx = session.ContextWithContent(dnsCtx, &session.Content{
Protocol: "https",
SkipRoutePick: true,
})
// forced to use mux for DOH
dnsCtx = session.ContextWithMuxPrefered(dnsCtx, true)
var cancel context.CancelFunc
dnsCtx, cancel = context.WithDeadline(dnsCtx, deadline)
defer cancel()
b, err := dns.PackMessage(r.msg)
if err != nil {
newError("failed to pack dns query").Base(err).AtError().WriteToLog()
return
}
resp, err := s.dohHTTPSContext(dnsCtx, b.Bytes())
if err != nil {
newError("failed to retrieve response").Base(err).AtError().WriteToLog()
return
}
rec, err := parseResponse(resp)
if err != nil {
newError("failed to handle DOH response").Base(err).AtError().WriteToLog()
return
}
s.updateIP(r, rec)
}(req)
}
}
func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte, error) {
body := bytes.NewBuffer(b)
req, err := http.NewRequest("POST", s.dohURL, body)
if err != nil {
return nil, err
}
req.Header.Add("Accept", "application/dns-message")
req.Header.Add("Content-Type", "application/dns-message")
resp, err := s.httpClient.Do(req.WithContext(ctx))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
io.Copy(ioutil.Discard, resp.Body) // flush resp.Body so that the conn is reusable
return nil, fmt.Errorf("DOH server returned code %d", resp.StatusCode)
}
return ioutil.ReadAll(resp.Body)
}
func (s *DoHNameServer) findIPsForDomain(domain string, option IPOption) ([]net.IP, error) {
s.RLock()
record, found := s.ips[domain]
s.RUnlock()
if !found {
return nil, errRecordNotFound
}
var ips []net.Address
var lastErr error
if option.IPv6Enable && record.AAAA != nil && record.AAAA.RCode == dnsmessage.RCodeSuccess {
aaaa, err := record.AAAA.getIPs()
if err != nil {
lastErr = err
}
ips = append(ips, aaaa...)
}
if option.IPv4Enable && record.A != nil && record.A.RCode == dnsmessage.RCodeSuccess {
a, err := record.A.getIPs()
if err != nil {
lastErr = err
}
ips = append(ips, a...)
}
if len(ips) > 0 {
return toNetIP(ips), nil
}
if lastErr != nil {
return nil, lastErr
}
if (option.IPv4Enable && record.A != nil) || (option.IPv6Enable && record.AAAA != nil) {
return nil, dns_feature.ErrEmptyResponse
}
return nil, errRecordNotFound
}
// QueryIP is called from dns.Server->queryIPTimeout
func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) {
fqdn := Fqdn(domain)
ips, err := s.findIPsForDomain(fqdn, option)
if err != errRecordNotFound {
newError(s.name, " cache HIT ", domain, " -> ", ips).Base(err).AtDebug().WriteToLog()
return ips, err
}
// ipv4 and ipv6 belong to different subscription groups
var sub4, sub6 *pubsub.Subscriber
if option.IPv4Enable {
sub4 = s.pub.Subscribe(fqdn + "4")
defer sub4.Close()
}
if option.IPv6Enable {
sub6 = s.pub.Subscribe(fqdn + "6")
defer sub6.Close()
}
done := make(chan interface{})
go func() {
if sub4 != nil {
select {
case <-sub4.Wait():
case <-ctx.Done():
}
}
if sub6 != nil {
select {
case <-sub6.Wait():
case <-ctx.Done():
}
}
close(done)
}()
s.sendQuery(ctx, fqdn, option)
for {
ips, err := s.findIPsForDomain(fqdn, option)
if err != errRecordNotFound {
return ips, err
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-done:
}
}
}

View file

@ -0,0 +1,9 @@
package dns
import "github.com/xtls/xray-core/v1/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

124
app/dns/hosts.go Normal file
View file

@ -0,0 +1,124 @@
// +build !confonly
package dns
import (
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/strmatcher"
"github.com/xtls/xray-core/v1/features"
)
// StaticHosts represents static domain-ip mapping in DNS server.
type StaticHosts struct {
ips [][]net.Address
matchers *strmatcher.MatcherGroup
}
var typeMap = map[DomainMatchingType]strmatcher.Type{
DomainMatchingType_Full: strmatcher.Full,
DomainMatchingType_Subdomain: strmatcher.Domain,
DomainMatchingType_Keyword: strmatcher.Substr,
DomainMatchingType_Regex: strmatcher.Regex,
}
func toStrMatcher(t DomainMatchingType, domain string) (strmatcher.Matcher, error) {
strMType, f := typeMap[t]
if !f {
return nil, newError("unknown mapping type", t).AtWarning()
}
matcher, err := strMType.New(domain)
if err != nil {
return nil, newError("failed to create str matcher").Base(err)
}
return matcher, nil
}
// NewStaticHosts creates a new StaticHosts instance.
func NewStaticHosts(hosts []*Config_HostMapping, legacy map[string]*net.IPOrDomain) (*StaticHosts, error) {
g := new(strmatcher.MatcherGroup)
sh := &StaticHosts{
ips: make([][]net.Address, len(hosts)+len(legacy)+16),
matchers: g,
}
if legacy != nil {
features.PrintDeprecatedFeatureWarning("simple host mapping")
for domain, ip := range legacy {
matcher, err := strmatcher.Full.New(domain)
common.Must(err)
id := g.Add(matcher)
address := ip.AsAddress()
if address.Family().IsDomain() {
return nil, newError("invalid domain address in static hosts: ", address.Domain()).AtWarning()
}
sh.ips[id] = []net.Address{address}
}
}
for _, mapping := range hosts {
matcher, err := toStrMatcher(mapping.Type, mapping.Domain)
if err != nil {
return nil, newError("failed to create domain matcher").Base(err)
}
id := g.Add(matcher)
ips := make([]net.Address, 0, len(mapping.Ip)+1)
switch {
case len(mapping.Ip) > 0:
for _, ip := range mapping.Ip {
addr := net.IPAddress(ip)
if addr == nil {
return nil, newError("invalid IP address in static hosts: ", ip).AtWarning()
}
ips = append(ips, addr)
}
case len(mapping.ProxiedDomain) > 0:
ips = append(ips, net.DomainAddress(mapping.ProxiedDomain))
default:
return nil, newError("neither IP address nor proxied domain specified for domain: ", mapping.Domain).AtWarning()
}
// Special handling for localhost IPv6. This is a dirty workaround as JSON config supports only single IP mapping.
if len(ips) == 1 && ips[0] == net.LocalHostIP {
ips = append(ips, net.LocalHostIPv6)
}
sh.ips[id] = ips
}
return sh, nil
}
func filterIP(ips []net.Address, option IPOption) []net.Address {
filtered := make([]net.Address, 0, len(ips))
for _, ip := range ips {
if (ip.Family().IsIPv4() && option.IPv4Enable) || (ip.Family().IsIPv6() && option.IPv6Enable) {
filtered = append(filtered, ip)
}
}
if len(filtered) == 0 {
return nil
}
return filtered
}
// LookupIP returns IP address for the given domain, if exists in this StaticHosts.
func (h *StaticHosts) LookupIP(domain string, option IPOption) []net.Address {
indices := h.matchers.Match(domain)
if len(indices) == 0 {
return nil
}
ips := []net.Address{}
for _, id := range indices {
ips = append(ips, h.ips[id]...)
}
if len(ips) == 1 && ips[0].Family().IsDomain() {
return ips
}
return filterIP(ips, option)
}

79
app/dns/hosts_test.go Normal file
View file

@ -0,0 +1,79 @@
package dns_test
import (
"testing"
"github.com/google/go-cmp/cmp"
. "github.com/xtls/xray-core/v1/app/dns"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/net"
)
func TestStaticHosts(t *testing.T) {
pb := []*Config_HostMapping{
{
Type: DomainMatchingType_Full,
Domain: "example.com",
Ip: [][]byte{
{1, 1, 1, 1},
},
},
{
Type: DomainMatchingType_Subdomain,
Domain: "example.cn",
Ip: [][]byte{
{2, 2, 2, 2},
},
},
{
Type: DomainMatchingType_Subdomain,
Domain: "baidu.com",
Ip: [][]byte{
{127, 0, 0, 1},
},
},
}
hosts, err := NewStaticHosts(pb, nil)
common.Must(err)
{
ips := hosts.LookupIP("example.com", IPOption{
IPv4Enable: true,
IPv6Enable: true,
})
if len(ips) != 1 {
t.Error("expect 1 IP, but got ", len(ips))
}
if diff := cmp.Diff([]byte(ips[0].IP()), []byte{1, 1, 1, 1}); diff != "" {
t.Error(diff)
}
}
{
ips := hosts.LookupIP("www.example.cn", IPOption{
IPv4Enable: true,
IPv6Enable: true,
})
if len(ips) != 1 {
t.Error("expect 1 IP, but got ", len(ips))
}
if diff := cmp.Diff([]byte(ips[0].IP()), []byte{2, 2, 2, 2}); diff != "" {
t.Error(diff)
}
}
{
ips := hosts.LookupIP("baidu.com", IPOption{
IPv4Enable: false,
IPv6Enable: true,
})
if len(ips) != 1 {
t.Error("expect 1 IP, but got ", len(ips))
}
if diff := cmp.Diff([]byte(ips[0].IP()), []byte(net.LocalHostIPv6.IP())); diff != "" {
t.Error(diff)
}
}
}

56
app/dns/nameserver.go Normal file
View file

@ -0,0 +1,56 @@
// +build !confonly
package dns
import (
"context"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/features/dns/localdns"
)
// IPOption is an object for IP query options.
type IPOption struct {
IPv4Enable bool
IPv6Enable bool
}
// Client is the interface for DNS client.
type Client interface {
// Name of the Client.
Name() string
// QueryIP sends IP queries to its configured server.
QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error)
}
type LocalNameServer struct {
client *localdns.Client
}
func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) {
if option.IPv4Enable && option.IPv6Enable {
return s.client.LookupIP(domain)
}
if option.IPv4Enable {
return s.client.LookupIPv4(domain)
}
if option.IPv6Enable {
return s.client.LookupIPv6(domain)
}
return nil, newError("neither IPv4 nor IPv6 is enabled")
}
func (s *LocalNameServer) Name() string {
return "localhost"
}
func NewLocalNameServer() *LocalNameServer {
newError("DNS: created localhost client").AtInfo().WriteToLog()
return &LocalNameServer{
client: localdns.New(),
}
}

View file

@ -0,0 +1,24 @@
package dns_test
import (
"context"
"testing"
"time"
. "github.com/xtls/xray-core/v1/app/dns"
"github.com/xtls/xray-core/v1/common"
)
func TestLocalNameServer(t *testing.T) {
s := NewLocalNameServer()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
ips, err := s.QueryIP(ctx, "google.com", IPOption{
IPv4Enable: true,
IPv6Enable: true,
})
cancel()
common.Must(err)
if len(ips) == 0 {
t.Error("expect some ips, but got 0")
}
}

449
app/dns/server.go Normal file
View file

@ -0,0 +1,449 @@
// +build !confonly
package dns
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
import (
"context"
"fmt"
"log"
"net/url"
"strings"
"sync"
"time"
"github.com/xtls/xray-core/v1/app/router"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/errors"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/session"
"github.com/xtls/xray-core/v1/common/strmatcher"
"github.com/xtls/xray-core/v1/common/uuid"
core "github.com/xtls/xray-core/v1/core"
"github.com/xtls/xray-core/v1/features"
"github.com/xtls/xray-core/v1/features/dns"
"github.com/xtls/xray-core/v1/features/routing"
)
// Server is a DNS rely server.
type Server struct {
sync.Mutex
hosts *StaticHosts
clientIP net.IP
clients []Client // clientIdx -> Client
ipIndexMap []*MultiGeoIPMatcher // clientIdx -> *MultiGeoIPMatcher
domainRules [][]string // clientIdx -> domainRuleIdx -> DomainRule
domainMatcher strmatcher.IndexMatcher
matcherInfos []DomainMatcherInfo // matcherIdx -> DomainMatcherInfo
tag string
}
// DomainMatcherInfo contains information attached to index returned by Server.domainMatcher
type DomainMatcherInfo struct {
clientIdx uint16
domainRuleIdx uint16
}
// MultiGeoIPMatcher for match
type MultiGeoIPMatcher struct {
matchers []*router.GeoIPMatcher
}
var errExpectedIPNonMatch = errors.New("expectIPs not match")
// Match check ip match
func (c *MultiGeoIPMatcher) Match(ip net.IP) bool {
for _, matcher := range c.matchers {
if matcher.Match(ip) {
return true
}
}
return false
}
// HasMatcher check has matcher
func (c *MultiGeoIPMatcher) HasMatcher() bool {
return len(c.matchers) > 0
}
func generateRandomTag() string {
id := uuid.New()
return "xray.system." + id.String()
}
// New creates a new DNS server with given configuration.
func New(ctx context.Context, config *Config) (*Server, error) {
server := &Server{
clients: make([]Client, 0, len(config.NameServers)+len(config.NameServer)),
tag: config.Tag,
}
if server.tag == "" {
server.tag = generateRandomTag()
}
if len(config.ClientIp) > 0 {
if len(config.ClientIp) != net.IPv4len && len(config.ClientIp) != net.IPv6len {
return nil, newError("unexpected IP length", len(config.ClientIp))
}
server.clientIP = net.IP(config.ClientIp)
}
hosts, err := NewStaticHosts(config.StaticHosts, config.Hosts)
if err != nil {
return nil, newError("failed to create hosts").Base(err)
}
server.hosts = hosts
addNameServer := func(ns *NameServer) int {
endpoint := ns.Address
address := endpoint.Address.AsAddress()
switch {
case address.Family().IsDomain() && address.Domain() == "localhost":
server.clients = append(server.clients, NewLocalNameServer())
// Priotize local domains with specific TLDs or without any dot to local DNS
// References:
// https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml
// https://unix.stackexchange.com/questions/92441/whats-the-difference-between-local-home-and-lan
localTLDsAndDotlessDomains := []*NameServer_PriorityDomain{
{Type: DomainMatchingType_Regex, Domain: "^[^.]+$"}, // This will only match domains without any dot
{Type: DomainMatchingType_Subdomain, Domain: "local"},
{Type: DomainMatchingType_Subdomain, Domain: "localdomain"},
{Type: DomainMatchingType_Subdomain, Domain: "localhost"},
{Type: DomainMatchingType_Subdomain, Domain: "lan"},
{Type: DomainMatchingType_Subdomain, Domain: "home.arpa"},
{Type: DomainMatchingType_Subdomain, Domain: "example"},
{Type: DomainMatchingType_Subdomain, Domain: "invalid"},
{Type: DomainMatchingType_Subdomain, Domain: "test"},
}
ns.PrioritizedDomain = append(ns.PrioritizedDomain, localTLDsAndDotlessDomains...)
case address.Family().IsDomain() && strings.HasPrefix(address.Domain(), "https+local://"):
// URI schemed string treated as domain
// DOH Local mode
u, err := url.Parse(address.Domain())
if err != nil {
log.Fatalln(newError("DNS config error").Base(err))
}
server.clients = append(server.clients, NewDoHLocalNameServer(u, server.clientIP))
case address.Family().IsDomain() && strings.HasPrefix(address.Domain(), "https://"):
// DOH Remote mode
u, err := url.Parse(address.Domain())
if err != nil {
log.Fatalln(newError("DNS config error").Base(err))
}
idx := len(server.clients)
server.clients = append(server.clients, nil)
// need the core dispatcher, register DOHClient at callback
common.Must(core.RequireFeatures(ctx, func(d routing.Dispatcher) {
c, err := NewDoHNameServer(u, d, server.clientIP)
if err != nil {
log.Fatalln(newError("DNS config error").Base(err))
}
server.clients[idx] = c
}))
default:
// UDP classic DNS mode
dest := endpoint.AsDestination()
if dest.Network == net.Network_Unknown {
dest.Network = net.Network_UDP
}
if dest.Network == net.Network_UDP {
idx := len(server.clients)
server.clients = append(server.clients, nil)
common.Must(core.RequireFeatures(ctx, func(d routing.Dispatcher) {
server.clients[idx] = NewClassicNameServer(dest, d, server.clientIP)
}))
}
}
server.ipIndexMap = append(server.ipIndexMap, nil)
return len(server.clients) - 1
}
if len(config.NameServers) > 0 {
features.PrintDeprecatedFeatureWarning("simple DNS server")
for _, destPB := range config.NameServers {
addNameServer(&NameServer{Address: destPB})
}
}
if len(config.NameServer) > 0 {
clientIndices := []int{}
domainRuleCount := 0
for _, ns := range config.NameServer {
idx := addNameServer(ns)
clientIndices = append(clientIndices, idx)
domainRuleCount += len(ns.PrioritizedDomain)
}
domainRules := make([][]string, len(server.clients))
domainMatcher := &strmatcher.MatcherGroup{}
matcherInfos := make([]DomainMatcherInfo, domainRuleCount+1) // matcher index starts from 1
var geoIPMatcherContainer router.GeoIPMatcherContainer
for nidx, ns := range config.NameServer {
idx := clientIndices[nidx]
// Establish domain rule matcher
rules := []string{}
ruleCurr := 0
ruleIter := 0
for _, domain := range ns.PrioritizedDomain {
matcher, err := toStrMatcher(domain.Type, domain.Domain)
if err != nil {
return nil, newError("failed to create prioritized domain").Base(err).AtWarning()
}
midx := domainMatcher.Add(matcher)
if midx >= uint32(len(matcherInfos)) { // This rarely happens according to current matcher's implementation
newError("expanding domain matcher info array to size ", midx, " when adding ", matcher).AtDebug().WriteToLog()
matcherInfos = append(matcherInfos, make([]DomainMatcherInfo, midx-uint32(len(matcherInfos))+1)...)
}
info := &matcherInfos[midx]
info.clientIdx = uint16(idx)
if ruleCurr < len(ns.OriginalRules) {
info.domainRuleIdx = uint16(ruleCurr)
rule := ns.OriginalRules[ruleCurr]
if ruleCurr >= len(rules) {
rules = append(rules, rule.Rule)
}
ruleIter++
if ruleIter >= int(rule.Size) {
ruleIter = 0
ruleCurr++
}
} else { // No original rule, generate one according to current domain matcher (majorly for compatibility with tests)
info.domainRuleIdx = uint16(len(rules))
rules = append(rules, matcher.String())
}
}
domainRules[idx] = rules
// only add to ipIndexMap if GeoIP is configured
if len(ns.Geoip) > 0 {
var matchers []*router.GeoIPMatcher
for _, geoip := range ns.Geoip {
matcher, err := geoIPMatcherContainer.Add(geoip)
if err != nil {
return nil, newError("failed to create ip matcher").Base(err).AtWarning()
}
matchers = append(matchers, matcher)
}
matcher := &MultiGeoIPMatcher{matchers: matchers}
server.ipIndexMap[idx] = matcher
}
}
server.domainRules = domainRules
server.domainMatcher = domainMatcher
server.matcherInfos = matcherInfos
}
if len(server.clients) == 0 {
server.clients = append(server.clients, NewLocalNameServer())
server.ipIndexMap = append(server.ipIndexMap, nil)
}
return server, nil
}
// Type implements common.HasType.
func (*Server) Type() interface{} {
return dns.ClientType()
}
// Start implements common.Runnable.
func (s *Server) Start() error {
return nil
}
// Close implements common.Closable.
func (s *Server) Close() error {
return nil
}
func (s *Server) IsOwnLink(ctx context.Context) bool {
inbound := session.InboundFromContext(ctx)
return inbound != nil && inbound.Tag == s.tag
}
// Match check dns ip match geoip
func (s *Server) Match(idx int, client Client, domain string, ips []net.IP) ([]net.IP, error) {
var matcher *MultiGeoIPMatcher
if idx < len(s.ipIndexMap) {
matcher = s.ipIndexMap[idx]
}
if matcher == nil {
return ips, nil
}
if !matcher.HasMatcher() {
newError("domain ", domain, " server has no valid matcher: ", client.Name(), " idx:", idx).AtDebug().WriteToLog()
return ips, nil
}
newIps := []net.IP{}
for _, ip := range ips {
if matcher.Match(ip) {
newIps = append(newIps, ip)
}
}
if len(newIps) == 0 {
return nil, errExpectedIPNonMatch
}
newError("domain ", domain, " expectIPs ", newIps, " matched at server ", client.Name(), " idx:", idx).AtDebug().WriteToLog()
return newIps, nil
}
func (s *Server) queryIPTimeout(idx int, client Client, domain string, option IPOption) ([]net.IP, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*4)
if len(s.tag) > 0 {
ctx = session.ContextWithInbound(ctx, &session.Inbound{
Tag: s.tag,
})
}
ips, err := client.QueryIP(ctx, domain, option)
cancel()
if err != nil {
return ips, err
}
ips, err = s.Match(idx, client, domain, ips)
return ips, err
}
// LookupIP implements dns.Client.
func (s *Server) LookupIP(domain string) ([]net.IP, error) {
return s.lookupIPInternal(domain, IPOption{
IPv4Enable: true,
IPv6Enable: true,
})
}
// LookupIPv4 implements dns.IPv4Lookup.
func (s *Server) LookupIPv4(domain string) ([]net.IP, error) {
return s.lookupIPInternal(domain, IPOption{
IPv4Enable: true,
IPv6Enable: false,
})
}
// LookupIPv6 implements dns.IPv6Lookup.
func (s *Server) LookupIPv6(domain string) ([]net.IP, error) {
return s.lookupIPInternal(domain, IPOption{
IPv4Enable: false,
IPv6Enable: true,
})
}
func (s *Server) lookupStatic(domain string, option IPOption, depth int32) []net.Address {
ips := s.hosts.LookupIP(domain, option)
if ips == nil {
return nil
}
if ips[0].Family().IsDomain() && depth < 5 {
if newIPs := s.lookupStatic(ips[0].Domain(), option, depth+1); newIPs != nil {
return newIPs
}
}
return ips
}
func toNetIP(ips []net.Address) []net.IP {
if len(ips) == 0 {
return nil
}
netips := make([]net.IP, 0, len(ips))
for _, ip := range ips {
netips = append(netips, ip.IP())
}
return netips
}
func (s *Server) lookupIPInternal(domain string, option IPOption) ([]net.IP, error) {
if domain == "" {
return nil, newError("empty domain name")
}
// normalize the FQDN form query
if domain[len(domain)-1] == '.' {
domain = domain[:len(domain)-1]
}
ips := s.lookupStatic(domain, option, 0)
if ips != nil && ips[0].Family().IsIP() {
newError("returning ", len(ips), " IPs for domain ", domain).WriteToLog()
return toNetIP(ips), nil
}
if ips != nil && ips[0].Family().IsDomain() {
newdomain := ips[0].Domain()
newError("domain replaced: ", domain, " -> ", newdomain).WriteToLog()
domain = newdomain
}
var lastErr error
var matchedClient Client
if s.domainMatcher != nil {
indices := s.domainMatcher.Match(domain)
domainRules := []string{}
matchingDNS := []string{}
for _, idx := range indices {
info := s.matcherInfos[idx]
rule := s.domainRules[info.clientIdx][info.domainRuleIdx]
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", rule, info.clientIdx))
matchingDNS = append(matchingDNS, s.clients[info.clientIdx].Name())
}
if len(domainRules) > 0 {
newError("domain ", domain, " matches following rules: ", domainRules).AtDebug().WriteToLog()
}
if len(matchingDNS) > 0 {
newError("domain ", domain, " uses following DNS first: ", matchingDNS).AtDebug().WriteToLog()
}
for _, idx := range indices {
clientIdx := int(s.matcherInfos[idx].clientIdx)
matchedClient = s.clients[clientIdx]
ips, err := s.queryIPTimeout(clientIdx, matchedClient, domain, option)
if len(ips) > 0 {
return ips, nil
}
if err == dns.ErrEmptyResponse {
return nil, err
}
if err != nil {
newError("failed to lookup ip for domain ", domain, " at server ", matchedClient.Name()).Base(err).WriteToLog()
lastErr = err
}
}
}
for idx, client := range s.clients {
if client == matchedClient {
newError("domain ", domain, " at server ", client.Name(), " idx:", idx, " already lookup failed, just ignore").AtDebug().WriteToLog()
continue
}
ips, err := s.queryIPTimeout(idx, client, domain, option)
if len(ips) > 0 {
return ips, nil
}
if err != nil {
newError("failed to lookup ip for domain ", domain, " at server ", client.Name()).Base(err).WriteToLog()
lastErr = err
}
if err != context.Canceled && err != context.DeadlineExceeded && err != errExpectedIPNonMatch {
return nil, err
}
}
return nil, newError("returning nil for domain ", domain).Base(lastErr)
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return New(ctx, config.(*Config))
}))
}

972
app/dns/server_test.go Normal file
View file

@ -0,0 +1,972 @@
package dns_test
import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/miekg/dns"
"github.com/xtls/xray-core/v1/app/dispatcher"
. "github.com/xtls/xray-core/v1/app/dns"
"github.com/xtls/xray-core/v1/app/policy"
"github.com/xtls/xray-core/v1/app/proxyman"
_ "github.com/xtls/xray-core/v1/app/proxyman/outbound"
"github.com/xtls/xray-core/v1/app/router"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/serial"
"github.com/xtls/xray-core/v1/core"
feature_dns "github.com/xtls/xray-core/v1/features/dns"
"github.com/xtls/xray-core/v1/proxy/freedom"
"github.com/xtls/xray-core/v1/testing/servers/udp"
)
type staticHandler struct {
}
func (*staticHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
ans := new(dns.Msg)
ans.Id = r.Id
var clientIP net.IP
opt := r.IsEdns0()
if opt != nil {
for _, o := range opt.Option {
if o.Option() == dns.EDNS0SUBNET {
subnet := o.(*dns.EDNS0_SUBNET)
clientIP = subnet.Address
}
}
}
for _, q := range r.Question {
switch {
case q.Name == "google.com." && q.Qtype == dns.TypeA:
if clientIP == nil {
rr, _ := dns.NewRR("google.com. IN A 8.8.8.8")
ans.Answer = append(ans.Answer, rr)
} else {
rr, _ := dns.NewRR("google.com. IN A 8.8.4.4")
ans.Answer = append(ans.Answer, rr)
}
case q.Name == "api.google.com." && q.Qtype == dns.TypeA:
rr, _ := dns.NewRR("api.google.com. IN A 8.8.7.7")
ans.Answer = append(ans.Answer, rr)
case q.Name == "v2.api.google.com." && q.Qtype == dns.TypeA:
rr, _ := dns.NewRR("v2.api.google.com. IN A 8.8.7.8")
ans.Answer = append(ans.Answer, rr)
case q.Name == "facebook.com." && q.Qtype == dns.TypeA:
rr, _ := dns.NewRR("facebook.com. IN A 9.9.9.9")
ans.Answer = append(ans.Answer, rr)
case q.Name == "ipv6.google.com." && q.Qtype == dns.TypeA:
rr, err := dns.NewRR("ipv6.google.com. IN A 8.8.8.7")
common.Must(err)
ans.Answer = append(ans.Answer, rr)
case q.Name == "ipv6.google.com." && q.Qtype == dns.TypeAAAA:
rr, err := dns.NewRR("ipv6.google.com. IN AAAA 2001:4860:4860::8888")
common.Must(err)
ans.Answer = append(ans.Answer, rr)
case q.Name == "notexist.google.com." && q.Qtype == dns.TypeAAAA:
ans.MsgHdr.Rcode = dns.RcodeNameError
case q.Name == "hostname." && q.Qtype == dns.TypeA:
rr, _ := dns.NewRR("hostname. IN A 127.0.0.1")
ans.Answer = append(ans.Answer, rr)
case q.Name == "hostname.local." && q.Qtype == dns.TypeA:
rr, _ := dns.NewRR("hostname.local. IN A 127.0.0.1")
ans.Answer = append(ans.Answer, rr)
case q.Name == "hostname.localdomain." && q.Qtype == dns.TypeA:
rr, _ := dns.NewRR("hostname.localdomain. IN A 127.0.0.1")
ans.Answer = append(ans.Answer, rr)
case q.Name == "localhost." && q.Qtype == dns.TypeA:
rr, _ := dns.NewRR("localhost. IN A 127.0.0.2")
ans.Answer = append(ans.Answer, rr)
case q.Name == "localhost-a." && q.Qtype == dns.TypeA:
rr, _ := dns.NewRR("localhost-a. IN A 127.0.0.3")
ans.Answer = append(ans.Answer, rr)
case q.Name == "localhost-b." && q.Qtype == dns.TypeA:
rr, _ := dns.NewRR("localhost-b. IN A 127.0.0.4")
ans.Answer = append(ans.Answer, rr)
case q.Name == "Mijia\\ Cloud." && q.Qtype == dns.TypeA:
rr, _ := dns.NewRR("Mijia\\ Cloud. IN A 127.0.0.1")
ans.Answer = append(ans.Answer, rr)
}
}
w.WriteMsg(ans)
}
func TestUDPServerSubnet(t *testing.T) {
port := udp.PickPort()
dnsServer := dns.Server{
Addr: "127.0.0.1:" + port.String(),
Net: "udp",
Handler: &staticHandler{},
UDPSize: 1200,
}
go dnsServer.ListenAndServe()
time.Sleep(time.Second)
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&Config{
NameServers: []*net.Endpoint{
{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
},
ClientIp: []byte{7, 8, 9, 10},
}),
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&policy.Config{}),
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
},
},
}
v, err := core.New(config)
common.Must(err)
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
ips, err := client.LookupIP("google.com")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{8, 8, 4, 4}}); r != "" {
t.Fatal(r)
}
}
func TestUDPServer(t *testing.T) {
port := udp.PickPort()
dnsServer := dns.Server{
Addr: "127.0.0.1:" + port.String(),
Net: "udp",
Handler: &staticHandler{},
UDPSize: 1200,
}
go dnsServer.ListenAndServe()
time.Sleep(time.Second)
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&Config{
NameServers: []*net.Endpoint{
{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
},
}),
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&policy.Config{}),
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
},
},
}
v, err := core.New(config)
common.Must(err)
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
{
ips, err := client.LookupIP("google.com")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{8, 8, 8, 8}}); r != "" {
t.Fatal(r)
}
}
{
ips, err := client.LookupIP("facebook.com")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{9, 9, 9, 9}}); r != "" {
t.Fatal(r)
}
}
{
_, err := client.LookupIP("notexist.google.com")
if err == nil {
t.Fatal("nil error")
}
if r := feature_dns.RCodeFromError(err); r != uint16(dns.RcodeNameError) {
t.Fatal("expected NameError, but got ", r)
}
}
{
clientv6 := client.(feature_dns.IPv6Lookup)
ips, err := clientv6.LookupIPv6("ipv4only.google.com")
if err != feature_dns.ErrEmptyResponse {
t.Fatal("error: ", err)
}
if len(ips) != 0 {
t.Fatal("ips: ", ips)
}
}
dnsServer.Shutdown()
{
ips, err := client.LookupIP("google.com")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{8, 8, 8, 8}}); r != "" {
t.Fatal(r)
}
}
}
func TestPrioritizedDomain(t *testing.T) {
port := udp.PickPort()
dnsServer := dns.Server{
Addr: "127.0.0.1:" + port.String(),
Net: "udp",
Handler: &staticHandler{},
UDPSize: 1200,
}
go dnsServer.ListenAndServe()
time.Sleep(time.Second)
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&Config{
NameServers: []*net.Endpoint{
{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: 9999, /* unreachable */
},
},
NameServer: []*NameServer{
{
Address: &net.Endpoint{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
PrioritizedDomain: []*NameServer_PriorityDomain{
{
Type: DomainMatchingType_Full,
Domain: "google.com",
},
},
},
},
}),
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&policy.Config{}),
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
},
},
}
v, err := core.New(config)
common.Must(err)
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
startTime := time.Now()
{
ips, err := client.LookupIP("google.com")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{8, 8, 8, 8}}); r != "" {
t.Fatal(r)
}
}
endTime := time.Now()
if startTime.After(endTime.Add(time.Second * 2)) {
t.Error("DNS query doesn't finish in 2 seconds.")
}
}
func TestUDPServerIPv6(t *testing.T) {
port := udp.PickPort()
dnsServer := dns.Server{
Addr: "127.0.0.1:" + port.String(),
Net: "udp",
Handler: &staticHandler{},
UDPSize: 1200,
}
go dnsServer.ListenAndServe()
time.Sleep(time.Second)
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&Config{
NameServers: []*net.Endpoint{
{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
},
}),
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&policy.Config{}),
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
},
},
}
v, err := core.New(config)
common.Must(err)
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
client6 := client.(feature_dns.IPv6Lookup)
{
ips, err := client6.LookupIPv6("ipv6.google.com")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{32, 1, 72, 96, 72, 96, 0, 0, 0, 0, 0, 0, 0, 0, 136, 136}}); r != "" {
t.Fatal(r)
}
}
}
func TestStaticHostDomain(t *testing.T) {
port := udp.PickPort()
dnsServer := dns.Server{
Addr: "127.0.0.1:" + port.String(),
Net: "udp",
Handler: &staticHandler{},
UDPSize: 1200,
}
go dnsServer.ListenAndServe()
time.Sleep(time.Second)
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&Config{
NameServers: []*net.Endpoint{
{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
},
StaticHosts: []*Config_HostMapping{
{
Type: DomainMatchingType_Full,
Domain: "example.com",
ProxiedDomain: "google.com",
},
},
}),
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&policy.Config{}),
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
},
},
}
v, err := core.New(config)
common.Must(err)
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
{
ips, err := client.LookupIP("example.com")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{8, 8, 8, 8}}); r != "" {
t.Fatal(r)
}
}
dnsServer.Shutdown()
}
func TestIPMatch(t *testing.T) {
port := udp.PickPort()
dnsServer := dns.Server{
Addr: "127.0.0.1:" + port.String(),
Net: "udp",
Handler: &staticHandler{},
UDPSize: 1200,
}
go dnsServer.ListenAndServe()
time.Sleep(time.Second)
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&Config{
NameServer: []*NameServer{
// private dns, not match
{
Address: &net.Endpoint{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
Geoip: []*router.GeoIP{
{
CountryCode: "local",
Cidr: []*router.CIDR{
{
// inner ip, will not match
Ip: []byte{192, 168, 11, 1},
Prefix: 32,
},
},
},
},
},
// second dns, match ip
{
Address: &net.Endpoint{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
Geoip: []*router.GeoIP{
{
CountryCode: "test",
Cidr: []*router.CIDR{
{
Ip: []byte{8, 8, 8, 8},
Prefix: 32,
},
},
},
{
CountryCode: "test",
Cidr: []*router.CIDR{
{
Ip: []byte{8, 8, 8, 4},
Prefix: 32,
},
},
},
},
},
},
}),
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&policy.Config{}),
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
},
},
}
v, err := core.New(config)
common.Must(err)
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
startTime := time.Now()
{
ips, err := client.LookupIP("google.com")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{8, 8, 8, 8}}); r != "" {
t.Fatal(r)
}
}
endTime := time.Now()
if startTime.After(endTime.Add(time.Second * 2)) {
t.Error("DNS query doesn't finish in 2 seconds.")
}
}
func TestLocalDomain(t *testing.T) {
port := udp.PickPort()
dnsServer := dns.Server{
Addr: "127.0.0.1:" + port.String(),
Net: "udp",
Handler: &staticHandler{},
UDPSize: 1200,
}
go dnsServer.ListenAndServe()
time.Sleep(time.Second)
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&Config{
NameServers: []*net.Endpoint{
{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: 9999, /* unreachable */
},
},
NameServer: []*NameServer{
{
Address: &net.Endpoint{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
PrioritizedDomain: []*NameServer_PriorityDomain{
// Equivalent of dotless:localhost
{Type: DomainMatchingType_Regex, Domain: "^[^.]*localhost[^.]*$"},
},
Geoip: []*router.GeoIP{
{ // Will match localhost, localhost-a and localhost-b,
CountryCode: "local",
Cidr: []*router.CIDR{
{Ip: []byte{127, 0, 0, 2}, Prefix: 32},
{Ip: []byte{127, 0, 0, 3}, Prefix: 32},
{Ip: []byte{127, 0, 0, 4}, Prefix: 32},
},
},
},
},
{
Address: &net.Endpoint{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
PrioritizedDomain: []*NameServer_PriorityDomain{
// Equivalent of dotless: and domain:local
{Type: DomainMatchingType_Regex, Domain: "^[^.]*$"},
{Type: DomainMatchingType_Subdomain, Domain: "local"},
{Type: DomainMatchingType_Subdomain, Domain: "localdomain"},
},
},
},
StaticHosts: []*Config_HostMapping{
{
Type: DomainMatchingType_Full,
Domain: "hostnamestatic",
Ip: [][]byte{{127, 0, 0, 53}},
},
{
Type: DomainMatchingType_Full,
Domain: "hostnamealias",
ProxiedDomain: "hostname.localdomain",
},
},
}),
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&policy.Config{}),
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
},
},
}
v, err := core.New(config)
common.Must(err)
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
startTime := time.Now()
{ // Will match dotless:
ips, err := client.LookupIP("hostname")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 1}}); r != "" {
t.Fatal(r)
}
}
{ // Will match domain:local
ips, err := client.LookupIP("hostname.local")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 1}}); r != "" {
t.Fatal(r)
}
}
{ // Will match static ip
ips, err := client.LookupIP("hostnamestatic")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 53}}); r != "" {
t.Fatal(r)
}
}
{ // Will match domain replacing
ips, err := client.LookupIP("hostnamealias")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 1}}); r != "" {
t.Fatal(r)
}
}
{ // Will match dotless:localhost, but not expectIPs: 127.0.0.2, 127.0.0.3, then matches at dotless:
ips, err := client.LookupIP("localhost")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 2}}); r != "" {
t.Fatal(r)
}
}
{ // Will match dotless:localhost, and expectIPs: 127.0.0.2, 127.0.0.3
ips, err := client.LookupIP("localhost-a")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 3}}); r != "" {
t.Fatal(r)
}
}
{ // Will match dotless:localhost, and expectIPs: 127.0.0.2, 127.0.0.3
ips, err := client.LookupIP("localhost-b")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 4}}); r != "" {
t.Fatal(r)
}
}
{ // Will match dotless:
ips, err := client.LookupIP("Mijia Cloud")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 1}}); r != "" {
t.Fatal(r)
}
}
endTime := time.Now()
if startTime.After(endTime.Add(time.Second * 2)) {
t.Error("DNS query doesn't finish in 2 seconds.")
}
}
func TestMultiMatchPrioritizedDomain(t *testing.T) {
port := udp.PickPort()
dnsServer := dns.Server{
Addr: "127.0.0.1:" + port.String(),
Net: "udp",
Handler: &staticHandler{},
UDPSize: 1200,
}
go dnsServer.ListenAndServe()
time.Sleep(time.Second)
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&Config{
NameServers: []*net.Endpoint{
{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: 9999, /* unreachable */
},
},
NameServer: []*NameServer{
{
Address: &net.Endpoint{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
PrioritizedDomain: []*NameServer_PriorityDomain{
{
Type: DomainMatchingType_Subdomain,
Domain: "google.com",
},
},
Geoip: []*router.GeoIP{
{ // Will only match 8.8.8.8 and 8.8.4.4
Cidr: []*router.CIDR{
{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
{Ip: []byte{8, 8, 4, 4}, Prefix: 32},
},
},
},
},
{
Address: &net.Endpoint{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
PrioritizedDomain: []*NameServer_PriorityDomain{
{
Type: DomainMatchingType_Subdomain,
Domain: "google.com",
},
},
Geoip: []*router.GeoIP{
{ // Will match 8.8.8.8 and 8.8.8.7, etc
Cidr: []*router.CIDR{
{Ip: []byte{8, 8, 8, 7}, Prefix: 24},
},
},
},
},
{
Address: &net.Endpoint{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
PrioritizedDomain: []*NameServer_PriorityDomain{
{
Type: DomainMatchingType_Subdomain,
Domain: "api.google.com",
},
},
Geoip: []*router.GeoIP{
{ // Will only match 8.8.7.7 (api.google.com)
Cidr: []*router.CIDR{
{Ip: []byte{8, 8, 7, 7}, Prefix: 32},
},
},
},
},
{
Address: &net.Endpoint{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
PrioritizedDomain: []*NameServer_PriorityDomain{
{
Type: DomainMatchingType_Full,
Domain: "v2.api.google.com",
},
},
Geoip: []*router.GeoIP{
{ // Will only match 8.8.7.8 (v2.api.google.com)
Cidr: []*router.CIDR{
{Ip: []byte{8, 8, 7, 8}, Prefix: 32},
},
},
},
},
},
}),
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&policy.Config{}),
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
},
},
}
v, err := core.New(config)
common.Must(err)
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
startTime := time.Now()
{ // Will match server 1,2 and server 1 returns expected ip
ips, err := client.LookupIP("google.com")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{8, 8, 8, 8}}); r != "" {
t.Fatal(r)
}
}
{ // Will match server 1,2 and server 1 returns unexpected ip, then server 2 returns expected one
clientv4 := client.(feature_dns.IPv4Lookup)
ips, err := clientv4.LookupIPv4("ipv6.google.com")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{8, 8, 8, 7}}); r != "" {
t.Fatal(r)
}
}
{ // Will match server 3,1,2 and server 3 returns expected one
ips, err := client.LookupIP("api.google.com")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{8, 8, 7, 7}}); r != "" {
t.Fatal(r)
}
}
{ // Will match server 4,3,1,2 and server 4 returns expected one
ips, err := client.LookupIP("v2.api.google.com")
if err != nil {
t.Fatal("unexpected error: ", err)
}
if r := cmp.Diff(ips, []net.IP{{8, 8, 7, 8}}); r != "" {
t.Fatal(r)
}
}
endTime := time.Now()
if startTime.After(endTime.Add(time.Second * 2)) {
t.Error("DNS query doesn't finish in 2 seconds.")
}
}

289
app/dns/udpns.go Normal file
View file

@ -0,0 +1,289 @@
// +build !confonly
package dns
import (
"context"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/protocol/dns"
udp_proto "github.com/xtls/xray-core/v1/common/protocol/udp"
"github.com/xtls/xray-core/v1/common/session"
"github.com/xtls/xray-core/v1/common/signal/pubsub"
"github.com/xtls/xray-core/v1/common/task"
dns_feature "github.com/xtls/xray-core/v1/features/dns"
"github.com/xtls/xray-core/v1/features/routing"
"github.com/xtls/xray-core/v1/transport/internet/udp"
"golang.org/x/net/dns/dnsmessage"
)
type ClassicNameServer struct {
sync.RWMutex
name string
address net.Destination
ips map[string]record
requests map[uint16]dnsRequest
pub *pubsub.Service
udpServer *udp.Dispatcher
cleanup *task.Periodic
reqID uint32
clientIP net.IP
}
func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher, clientIP net.IP) *ClassicNameServer {
// default to 53 if unspecific
if address.Port == 0 {
address.Port = net.Port(53)
}
s := &ClassicNameServer{
address: address,
ips: make(map[string]record),
requests: make(map[uint16]dnsRequest),
clientIP: clientIP,
pub: pubsub.NewService(),
name: strings.ToUpper(address.String()),
}
s.cleanup = &task.Periodic{
Interval: time.Minute,
Execute: s.Cleanup,
}
s.udpServer = udp.NewDispatcher(dispatcher, s.HandleResponse)
newError("DNS: created udp client inited for ", address.NetAddr()).AtInfo().WriteToLog()
return s
}
func (s *ClassicNameServer) Name() string {
return s.name
}
func (s *ClassicNameServer) Cleanup() error {
now := time.Now()
s.Lock()
defer s.Unlock()
if len(s.ips) == 0 && len(s.requests) == 0 {
return newError(s.name, " nothing to do. stopping...")
}
for domain, record := range s.ips {
if record.A != nil && record.A.Expire.Before(now) {
record.A = nil
}
if record.AAAA != nil && record.AAAA.Expire.Before(now) {
record.AAAA = nil
}
if record.A == nil && record.AAAA == nil {
delete(s.ips, domain)
} else {
s.ips[domain] = record
}
}
if len(s.ips) == 0 {
s.ips = make(map[string]record)
}
for id, req := range s.requests {
if req.expire.Before(now) {
delete(s.requests, id)
}
}
if len(s.requests) == 0 {
s.requests = make(map[uint16]dnsRequest)
}
return nil
}
func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_proto.Packet) {
ipRec, err := parseResponse(packet.Payload.Bytes())
if err != nil {
newError(s.name, " fail to parse responded DNS udp").AtError().WriteToLog()
return
}
s.Lock()
id := ipRec.ReqID
req, ok := s.requests[id]
if ok {
// remove the pending request
delete(s.requests, id)
}
s.Unlock()
if !ok {
newError(s.name, " cannot find the pending request").AtError().WriteToLog()
return
}
var rec record
switch req.reqType {
case dnsmessage.TypeA:
rec.A = ipRec
case dnsmessage.TypeAAAA:
rec.AAAA = ipRec
}
elapsed := time.Since(req.start)
newError(s.name, " got answer: ", req.domain, " ", req.reqType, " -> ", ipRec.IP, " ", elapsed).AtInfo().WriteToLog()
if len(req.domain) > 0 && (rec.A != nil || rec.AAAA != nil) {
s.updateIP(req.domain, rec)
}
}
func (s *ClassicNameServer) updateIP(domain string, newRec record) {
s.Lock()
newError(s.name, " updating IP records for domain:", domain).AtDebug().WriteToLog()
rec := s.ips[domain]
updated := false
if isNewer(rec.A, newRec.A) {
rec.A = newRec.A
updated = true
}
if isNewer(rec.AAAA, newRec.AAAA) {
rec.AAAA = newRec.AAAA
updated = true
}
if updated {
s.ips[domain] = rec
}
if newRec.A != nil {
s.pub.Publish(domain+"4", nil)
}
if newRec.AAAA != nil {
s.pub.Publish(domain+"6", nil)
}
s.Unlock()
common.Must(s.cleanup.Start())
}
func (s *ClassicNameServer) newReqID() uint16 {
return uint16(atomic.AddUint32(&s.reqID, 1))
}
func (s *ClassicNameServer) addPendingRequest(req *dnsRequest) {
s.Lock()
defer s.Unlock()
id := req.msg.ID
req.expire = time.Now().Add(time.Second * 8)
s.requests[id] = *req
}
func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, option IPOption) {
newError(s.name, " querying DNS for: ", domain).AtDebug().WriteToLog(session.ExportIDToError(ctx))
reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP))
for _, req := range reqs {
s.addPendingRequest(req)
b, _ := dns.PackMessage(req.msg)
udpCtx := context.Background()
if inbound := session.InboundFromContext(ctx); inbound != nil {
udpCtx = session.ContextWithInbound(udpCtx, inbound)
}
udpCtx = session.ContextWithContent(udpCtx, &session.Content{
Protocol: "dns",
})
s.udpServer.Dispatch(udpCtx, s.address, b)
}
}
func (s *ClassicNameServer) findIPsForDomain(domain string, option IPOption) ([]net.IP, error) {
s.RLock()
record, found := s.ips[domain]
s.RUnlock()
if !found {
return nil, errRecordNotFound
}
var ips []net.Address
var lastErr error
if option.IPv4Enable {
a, err := record.A.getIPs()
if err != nil {
lastErr = err
}
ips = append(ips, a...)
}
if option.IPv6Enable {
aaaa, err := record.AAAA.getIPs()
if err != nil {
lastErr = err
}
ips = append(ips, aaaa...)
}
if len(ips) > 0 {
return toNetIP(ips), nil
}
if lastErr != nil {
return nil, lastErr
}
return nil, dns_feature.ErrEmptyResponse
}
func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) {
fqdn := Fqdn(domain)
ips, err := s.findIPsForDomain(fqdn, option)
if err != errRecordNotFound {
newError(s.name, " cache HIT ", domain, " -> ", ips).Base(err).AtDebug().WriteToLog()
return ips, err
}
// ipv4 and ipv6 belong to different subscription groups
var sub4, sub6 *pubsub.Subscriber
if option.IPv4Enable {
sub4 = s.pub.Subscribe(fqdn + "4")
defer sub4.Close()
}
if option.IPv6Enable {
sub6 = s.pub.Subscribe(fqdn + "6")
defer sub6.Close()
}
done := make(chan interface{})
go func() {
if sub4 != nil {
select {
case <-sub4.Wait():
case <-ctx.Done():
}
}
if sub6 != nil {
select {
case <-sub6.Wait():
case <-ctx.Done():
}
}
close(done)
}()
s.sendQuery(ctx, fqdn, option)
for {
ips, err := s.findIPsForDomain(fqdn, option)
if err != errRecordNotFound {
return ips, err
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-done:
}
}
}

View file

@ -0,0 +1,53 @@
// +build !confonly
package command
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
import (
"context"
grpc "google.golang.org/grpc"
"github.com/xtls/xray-core/v1/app/log"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/core"
)
type LoggerServer struct {
V *core.Instance
}
// RestartLogger implements LoggerService.
func (s *LoggerServer) RestartLogger(ctx context.Context, request *RestartLoggerRequest) (*RestartLoggerResponse, error) {
logger := s.V.GetFeature((*log.Instance)(nil))
if logger == nil {
return nil, newError("unable to get logger instance")
}
if err := logger.Close(); err != nil {
return nil, newError("failed to close logger").Base(err)
}
if err := logger.Start(); err != nil {
return nil, newError("failed to start logger").Base(err)
}
return &RestartLoggerResponse{}, nil
}
func (s *LoggerServer) mustEmbedUnimplementedLoggerServiceServer() {}
type service struct {
v *core.Instance
}
func (s *service) Register(server *grpc.Server) {
RegisterLoggerServiceServer(server, &LoggerServer{
V: s.v,
})
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) {
s := core.MustFromContext(ctx)
return &service{v: s}, nil
}))
}

View file

@ -0,0 +1,34 @@
package command_test
import (
"context"
"testing"
"github.com/xtls/xray-core/v1/app/dispatcher"
"github.com/xtls/xray-core/v1/app/log"
. "github.com/xtls/xray-core/v1/app/log/command"
"github.com/xtls/xray-core/v1/app/proxyman"
_ "github.com/xtls/xray-core/v1/app/proxyman/inbound"
_ "github.com/xtls/xray-core/v1/app/proxyman/outbound"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/serial"
"github.com/xtls/xray-core/v1/core"
)
func TestLoggerRestart(t *testing.T) {
v, err := core.New(&core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&log.Config{}),
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
},
})
common.Must(err)
common.Must(v.Start())
server := &LoggerServer{
V: v,
}
common.Must2(server.RestartLogger(context.Background(), &RestartLoggerRequest{}))
}

View file

@ -0,0 +1,258 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.14.0
// source: app/log/command/config.proto
package command
import (
proto "github.com/golang/protobuf/proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
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)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type Config struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *Config) Reset() {
*x = Config{}
if protoimpl.UnsafeEnabled {
mi := &file_app_log_command_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_log_command_config_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && 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_log_command_config_proto_rawDescGZIP(), []int{0}
}
type RestartLoggerRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *RestartLoggerRequest) Reset() {
*x = RestartLoggerRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_app_log_command_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RestartLoggerRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RestartLoggerRequest) ProtoMessage() {}
func (x *RestartLoggerRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_log_command_config_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RestartLoggerRequest.ProtoReflect.Descriptor instead.
func (*RestartLoggerRequest) Descriptor() ([]byte, []int) {
return file_app_log_command_config_proto_rawDescGZIP(), []int{1}
}
type RestartLoggerResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *RestartLoggerResponse) Reset() {
*x = RestartLoggerResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_app_log_command_config_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RestartLoggerResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RestartLoggerResponse) ProtoMessage() {}
func (x *RestartLoggerResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_log_command_config_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RestartLoggerResponse.ProtoReflect.Descriptor instead.
func (*RestartLoggerResponse) Descriptor() ([]byte, []int) {
return file_app_log_command_config_proto_rawDescGZIP(), []int{2}
}
var File_app_log_command_config_proto protoreflect.FileDescriptor
var file_app_log_command_config_proto_rawDesc = []byte{
0x0a, 0x1c, 0x61, 0x70, 0x70, 0x2f, 0x6c, 0x6f, 0x67, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
0x64, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x14,
0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x67, 0x2e, 0x63, 0x6f, 0x6d,
0x6d, 0x61, 0x6e, 0x64, 0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16,
0x0a, 0x14, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4c, 0x6f, 0x67, 0x67, 0x65, 0x72, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x17, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72,
0x74, 0x4c, 0x6f, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32,
0x7b, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x67, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x12, 0x6a, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4c, 0x6f, 0x67, 0x67, 0x65,
0x72, 0x12, 0x2a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x67,
0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74,
0x4c, 0x6f, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e,
0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x67, 0x2e, 0x63, 0x6f, 0x6d,
0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4c, 0x6f, 0x67, 0x67,
0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x61, 0x0a, 0x18,
0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x67,
0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79,
0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x6c, 0x6f, 0x67,
0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x14, 0x58, 0x72, 0x61, 0x79, 0x2e,
0x41, 0x70, 0x70, 0x2e, 0x4c, 0x6f, 0x67, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_app_log_command_config_proto_rawDescOnce sync.Once
file_app_log_command_config_proto_rawDescData = file_app_log_command_config_proto_rawDesc
)
func file_app_log_command_config_proto_rawDescGZIP() []byte {
file_app_log_command_config_proto_rawDescOnce.Do(func() {
file_app_log_command_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_log_command_config_proto_rawDescData)
})
return file_app_log_command_config_proto_rawDescData
}
var file_app_log_command_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_app_log_command_config_proto_goTypes = []interface{}{
(*Config)(nil), // 0: xray.app.log.command.Config
(*RestartLoggerRequest)(nil), // 1: xray.app.log.command.RestartLoggerRequest
(*RestartLoggerResponse)(nil), // 2: xray.app.log.command.RestartLoggerResponse
}
var file_app_log_command_config_proto_depIdxs = []int32{
1, // 0: xray.app.log.command.LoggerService.RestartLogger:input_type -> xray.app.log.command.RestartLoggerRequest
2, // 1: xray.app.log.command.LoggerService.RestartLogger:output_type -> xray.app.log.command.RestartLoggerResponse
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_app_log_command_config_proto_init() }
func file_app_log_command_config_proto_init() {
if File_app_log_command_config_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_app_log_command_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_log_command_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RestartLoggerRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_log_command_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RestartLoggerResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_log_command_config_proto_rawDesc,
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_app_log_command_config_proto_goTypes,
DependencyIndexes: file_app_log_command_config_proto_depIdxs,
MessageInfos: file_app_log_command_config_proto_msgTypes,
}.Build()
File_app_log_command_config_proto = out.File
file_app_log_command_config_proto_rawDesc = nil
file_app_log_command_config_proto_goTypes = nil
file_app_log_command_config_proto_depIdxs = nil
}

View file

@ -0,0 +1,17 @@
syntax = "proto3";
package xray.app.log.command;
option csharp_namespace = "Xray.App.Log.Command";
option go_package = "github.com/xtls/xray-core/v1/app/log/command";
option java_package = "com.xray.app.log.command";
option java_multiple_files = true;
message Config {}
message RestartLoggerRequest {}
message RestartLoggerResponse {}
service LoggerService {
rpc RestartLogger(RestartLoggerRequest) returns (RestartLoggerResponse) {}
}

View file

@ -0,0 +1,97 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package command
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion7
// LoggerServiceClient is the client API for LoggerService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type LoggerServiceClient interface {
RestartLogger(ctx context.Context, in *RestartLoggerRequest, opts ...grpc.CallOption) (*RestartLoggerResponse, error)
}
type loggerServiceClient struct {
cc grpc.ClientConnInterface
}
func NewLoggerServiceClient(cc grpc.ClientConnInterface) LoggerServiceClient {
return &loggerServiceClient{cc}
}
func (c *loggerServiceClient) RestartLogger(ctx context.Context, in *RestartLoggerRequest, opts ...grpc.CallOption) (*RestartLoggerResponse, error) {
out := new(RestartLoggerResponse)
err := c.cc.Invoke(ctx, "/xray.app.log.command.LoggerService/RestartLogger", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// LoggerServiceServer is the server API for LoggerService service.
// All implementations must embed UnimplementedLoggerServiceServer
// for forward compatibility
type LoggerServiceServer interface {
RestartLogger(context.Context, *RestartLoggerRequest) (*RestartLoggerResponse, error)
mustEmbedUnimplementedLoggerServiceServer()
}
// UnimplementedLoggerServiceServer must be embedded to have forward compatible implementations.
type UnimplementedLoggerServiceServer struct {
}
func (UnimplementedLoggerServiceServer) RestartLogger(context.Context, *RestartLoggerRequest) (*RestartLoggerResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RestartLogger not implemented")
}
func (UnimplementedLoggerServiceServer) mustEmbedUnimplementedLoggerServiceServer() {}
// UnsafeLoggerServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to LoggerServiceServer will
// result in compilation errors.
type UnsafeLoggerServiceServer interface {
mustEmbedUnimplementedLoggerServiceServer()
}
func RegisterLoggerServiceServer(s grpc.ServiceRegistrar, srv LoggerServiceServer) {
s.RegisterService(&_LoggerService_serviceDesc, srv)
}
func _LoggerService_RestartLogger_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RestartLoggerRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(LoggerServiceServer).RestartLogger(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/xray.app.log.command.LoggerService/RestartLogger",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(LoggerServiceServer).RestartLogger(ctx, req.(*RestartLoggerRequest))
}
return interceptor(ctx, in, info, handler)
}
var _LoggerService_serviceDesc = grpc.ServiceDesc{
ServiceName: "xray.app.log.command.LoggerService",
HandlerType: (*LoggerServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "RestartLogger",
Handler: _LoggerService_RestartLogger_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "app/log/command/config.proto",
}

View file

@ -0,0 +1,9 @@
package command
import "github.com/xtls/xray-core/v1/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

263
app/log/config.pb.go Normal file
View file

@ -0,0 +1,263 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.14.0
// source: app/log/config.proto
package log
import (
proto "github.com/golang/protobuf/proto"
log "github.com/xtls/xray-core/v1/common/log"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
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)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type LogType int32
const (
LogType_None LogType = 0
LogType_Console LogType = 1
LogType_File LogType = 2
LogType_Event LogType = 3
)
// Enum value maps for LogType.
var (
LogType_name = map[int32]string{
0: "None",
1: "Console",
2: "File",
3: "Event",
}
LogType_value = map[string]int32{
"None": 0,
"Console": 1,
"File": 2,
"Event": 3,
}
)
func (x LogType) Enum() *LogType {
p := new(LogType)
*p = x
return p
}
func (x LogType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (LogType) Descriptor() protoreflect.EnumDescriptor {
return file_app_log_config_proto_enumTypes[0].Descriptor()
}
func (LogType) Type() protoreflect.EnumType {
return &file_app_log_config_proto_enumTypes[0]
}
func (x LogType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use LogType.Descriptor instead.
func (LogType) EnumDescriptor() ([]byte, []int) {
return file_app_log_config_proto_rawDescGZIP(), []int{0}
}
type Config struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ErrorLogType LogType `protobuf:"varint,1,opt,name=error_log_type,json=errorLogType,proto3,enum=xray.app.log.LogType" json:"error_log_type,omitempty"`
ErrorLogLevel log.Severity `protobuf:"varint,2,opt,name=error_log_level,json=errorLogLevel,proto3,enum=xray.common.log.Severity" json:"error_log_level,omitempty"`
ErrorLogPath string `protobuf:"bytes,3,opt,name=error_log_path,json=errorLogPath,proto3" json:"error_log_path,omitempty"`
AccessLogType LogType `protobuf:"varint,4,opt,name=access_log_type,json=accessLogType,proto3,enum=xray.app.log.LogType" json:"access_log_type,omitempty"`
AccessLogPath string `protobuf:"bytes,5,opt,name=access_log_path,json=accessLogPath,proto3" json:"access_log_path,omitempty"`
}
func (x *Config) Reset() {
*x = Config{}
if protoimpl.UnsafeEnabled {
mi := &file_app_log_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_log_config_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && 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_log_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetErrorLogType() LogType {
if x != nil {
return x.ErrorLogType
}
return LogType_None
}
func (x *Config) GetErrorLogLevel() log.Severity {
if x != nil {
return x.ErrorLogLevel
}
return log.Severity_Unknown
}
func (x *Config) GetErrorLogPath() string {
if x != nil {
return x.ErrorLogPath
}
return ""
}
func (x *Config) GetAccessLogType() LogType {
if x != nil {
return x.AccessLogType
}
return LogType_None
}
func (x *Config) GetAccessLogPath() string {
if x != nil {
return x.AccessLogPath
}
return ""
}
var File_app_log_config_proto protoreflect.FileDescriptor
var file_app_log_config_proto_rawDesc = []byte{
0x0a, 0x14, 0x61, 0x70, 0x70, 0x2f, 0x6c, 0x6f, 0x67, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
0x2e, 0x6c, 0x6f, 0x67, 0x1a, 0x14, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6c, 0x6f, 0x67,
0x2f, 0x6c, 0x6f, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x95, 0x02, 0x0a, 0x06, 0x43,
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3b, 0x0a, 0x0e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6c,
0x6f, 0x67, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e,
0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x67, 0x2e, 0x4c, 0x6f, 0x67,
0x54, 0x79, 0x70, 0x65, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4c, 0x6f, 0x67, 0x54, 0x79,
0x70, 0x65, 0x12, 0x41, 0x0a, 0x0f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x5f,
0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x78, 0x72,
0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6c, 0x6f, 0x67, 0x2e, 0x53, 0x65,
0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4c, 0x6f, 0x67,
0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x24, 0x0a, 0x0e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6c,
0x6f, 0x67, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65,
0x72, 0x72, 0x6f, 0x72, 0x4c, 0x6f, 0x67, 0x50, 0x61, 0x74, 0x68, 0x12, 0x3d, 0x0a, 0x0f, 0x61,
0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04,
0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e,
0x6c, 0x6f, 0x67, 0x2e, 0x4c, 0x6f, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0d, 0x61, 0x63, 0x63,
0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x61, 0x63,
0x63, 0x65, 0x73, 0x73, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x50, 0x61,
0x74, 0x68, 0x2a, 0x35, 0x0a, 0x07, 0x4c, 0x6f, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a,
0x04, 0x4e, 0x6f, 0x6e, 0x65, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x73, 0x6f,
0x6c, 0x65, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x10, 0x02, 0x12, 0x09,
0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x10, 0x03, 0x42, 0x49, 0x0a, 0x10, 0x63, 0x6f, 0x6d,
0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x67, 0x50, 0x01, 0x5a,
0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73,
0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70,
0x70, 0x2f, 0x6c, 0x6f, 0x67, 0xaa, 0x02, 0x0c, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70,
0x2e, 0x4c, 0x6f, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_app_log_config_proto_rawDescOnce sync.Once
file_app_log_config_proto_rawDescData = file_app_log_config_proto_rawDesc
)
func file_app_log_config_proto_rawDescGZIP() []byte {
file_app_log_config_proto_rawDescOnce.Do(func() {
file_app_log_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_log_config_proto_rawDescData)
})
return file_app_log_config_proto_rawDescData
}
var file_app_log_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_app_log_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_app_log_config_proto_goTypes = []interface{}{
(LogType)(0), // 0: xray.app.log.LogType
(*Config)(nil), // 1: xray.app.log.Config
(log.Severity)(0), // 2: xray.common.log.Severity
}
var file_app_log_config_proto_depIdxs = []int32{
0, // 0: xray.app.log.Config.error_log_type:type_name -> xray.app.log.LogType
2, // 1: xray.app.log.Config.error_log_level:type_name -> xray.common.log.Severity
0, // 2: xray.app.log.Config.access_log_type:type_name -> xray.app.log.LogType
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_app_log_config_proto_init() }
func file_app_log_config_proto_init() {
if File_app_log_config_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_app_log_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_log_config_proto_rawDesc,
NumEnums: 1,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_app_log_config_proto_goTypes,
DependencyIndexes: file_app_log_config_proto_depIdxs,
EnumInfos: file_app_log_config_proto_enumTypes,
MessageInfos: file_app_log_config_proto_msgTypes,
}.Build()
File_app_log_config_proto = out.File
file_app_log_config_proto_rawDesc = nil
file_app_log_config_proto_goTypes = nil
file_app_log_config_proto_depIdxs = nil
}

25
app/log/config.proto Normal file
View file

@ -0,0 +1,25 @@
syntax = "proto3";
package xray.app.log;
option csharp_namespace = "Xray.App.Log";
option go_package = "github.com/xtls/xray-core/v1/app/log";
option java_package = "com.xray.app.log";
option java_multiple_files = true;
import "common/log/log.proto";
enum LogType {
None = 0;
Console = 1;
File = 2;
Event = 3;
}
message Config {
LogType error_log_type = 1;
xray.common.log.Severity error_log_level = 2;
string error_log_path = 3;
LogType access_log_type = 4;
string access_log_path = 5;
}

View file

@ -0,0 +1,9 @@
package log
import "github.com/xtls/xray-core/v1/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

143
app/log/log.go Normal file
View file

@ -0,0 +1,143 @@
// +build !confonly
package log
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
import (
"context"
"sync"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/log"
)
// Instance is a log.Handler that handles logs.
type Instance struct {
sync.RWMutex
config *Config
accessLogger log.Handler
errorLogger log.Handler
active bool
}
// New creates a new log.Instance based on the given config.
func New(ctx context.Context, config *Config) (*Instance, error) {
g := &Instance{
config: config,
active: false,
}
log.RegisterHandler(g)
// start logger instantly on inited
// other modules would log during init
if err := g.startInternal(); err != nil {
return nil, err
}
newError("Logger started").AtDebug().WriteToLog()
return g, nil
}
func (g *Instance) initAccessLogger() error {
handler, err := createHandler(g.config.AccessLogType, HandlerCreatorOptions{
Path: g.config.AccessLogPath,
})
if err != nil {
return err
}
g.accessLogger = handler
return nil
}
func (g *Instance) initErrorLogger() error {
handler, err := createHandler(g.config.ErrorLogType, HandlerCreatorOptions{
Path: g.config.ErrorLogPath,
})
if err != nil {
return err
}
g.errorLogger = handler
return nil
}
// Type implements common.HasType.
func (*Instance) Type() interface{} {
return (*Instance)(nil)
}
func (g *Instance) startInternal() error {
g.Lock()
defer g.Unlock()
if g.active {
return nil
}
g.active = true
if err := g.initAccessLogger(); err != nil {
return newError("failed to initialize access logger").Base(err).AtWarning()
}
if err := g.initErrorLogger(); err != nil {
return newError("failed to initialize error logger").Base(err).AtWarning()
}
return nil
}
// Start implements common.Runnable.Start().
func (g *Instance) Start() error {
return g.startInternal()
}
// Handle implements log.Handler.
func (g *Instance) Handle(msg log.Message) {
g.RLock()
defer g.RUnlock()
if !g.active {
return
}
switch msg := msg.(type) {
case *log.AccessMessage:
if g.accessLogger != nil {
g.accessLogger.Handle(msg)
}
case *log.GeneralMessage:
if g.errorLogger != nil && msg.Severity <= g.config.ErrorLogLevel {
g.errorLogger.Handle(msg)
}
default:
// Swallow
}
}
// Close implements common.Closable.Close().
func (g *Instance) Close() error {
newError("Logger closing").AtDebug().WriteToLog()
g.Lock()
defer g.Unlock()
if !g.active {
return nil
}
g.active = false
common.Close(g.accessLogger)
g.accessLogger = nil
common.Close(g.errorLogger)
g.errorLogger = nil
return nil
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return New(ctx, config.(*Config))
}))
}

53
app/log/log_creator.go Normal file
View file

@ -0,0 +1,53 @@
// +build !confonly
package log
import (
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/log"
)
type HandlerCreatorOptions struct {
Path string
}
type HandlerCreator func(LogType, HandlerCreatorOptions) (log.Handler, error)
var (
handlerCreatorMap = make(map[LogType]HandlerCreator)
)
func RegisterHandlerCreator(logType LogType, f HandlerCreator) error {
if f == nil {
return newError("nil HandlerCreator")
}
handlerCreatorMap[logType] = f
return nil
}
func createHandler(logType LogType, options HandlerCreatorOptions) (log.Handler, error) {
creator, found := handlerCreatorMap[logType]
if !found {
return nil, newError("unable to create log handler for ", logType)
}
return creator(logType, options)
}
func init() {
common.Must(RegisterHandlerCreator(LogType_Console, func(lt LogType, options HandlerCreatorOptions) (log.Handler, error) {
return log.NewLogger(log.CreateStdoutLogWriter()), nil
}))
common.Must(RegisterHandlerCreator(LogType_File, func(lt LogType, options HandlerCreatorOptions) (log.Handler, error) {
creator, err := log.CreateFileLogWriter(options.Path)
if err != nil {
return nil, err
}
return log.NewLogger(creator), nil
}))
common.Must(RegisterHandlerCreator(LogType_None, func(lt LogType, options HandlerCreatorOptions) (log.Handler, error) {
return nil, nil
}))
}

52
app/log/log_test.go Normal file
View file

@ -0,0 +1,52 @@
package log_test
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/xtls/xray-core/v1/app/log"
"github.com/xtls/xray-core/v1/common"
clog "github.com/xtls/xray-core/v1/common/log"
"github.com/xtls/xray-core/v1/testing/mocks"
)
func TestCustomLogHandler(t *testing.T) {
mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
var loggedValue []string
mockHandler := mocks.NewLogHandler(mockCtl)
mockHandler.EXPECT().Handle(gomock.Any()).AnyTimes().DoAndReturn(func(msg clog.Message) {
loggedValue = append(loggedValue, msg.String())
})
log.RegisterHandlerCreator(log.LogType_Console, func(lt log.LogType, options log.HandlerCreatorOptions) (clog.Handler, error) {
return mockHandler, nil
})
logger, err := log.New(context.Background(), &log.Config{
ErrorLogLevel: clog.Severity_Debug,
ErrorLogType: log.LogType_Console,
AccessLogType: log.LogType_None,
})
common.Must(err)
common.Must(logger.Start())
clog.Record(&clog.GeneralMessage{
Severity: clog.Severity_Debug,
Content: "test",
})
if len(loggedValue) < 2 {
t.Fatal("expected 2 log messages, but actually ", loggedValue)
}
if loggedValue[1] != "[Debug] test" {
t.Fatal("expected '[Debug] test', but actually ", loggedValue[1])
}
common.Must(logger.Close())
}

93
app/policy/config.go Normal file
View file

@ -0,0 +1,93 @@
package policy
import (
"time"
"github.com/xtls/xray-core/v1/features/policy"
)
// Duration converts Second to time.Duration.
func (s *Second) Duration() time.Duration {
if s == nil {
return 0
}
return time.Second * time.Duration(s.Value)
}
func defaultPolicy() *Policy {
p := policy.SessionDefault()
return &Policy{
Timeout: &Policy_Timeout{
Handshake: &Second{Value: uint32(p.Timeouts.Handshake / time.Second)},
ConnectionIdle: &Second{Value: uint32(p.Timeouts.ConnectionIdle / time.Second)},
UplinkOnly: &Second{Value: uint32(p.Timeouts.UplinkOnly / time.Second)},
DownlinkOnly: &Second{Value: uint32(p.Timeouts.DownlinkOnly / time.Second)},
},
Buffer: &Policy_Buffer{
Connection: p.Buffer.PerConnection,
},
}
}
func (p *Policy_Timeout) overrideWith(another *Policy_Timeout) {
if another.Handshake != nil {
p.Handshake = &Second{Value: another.Handshake.Value}
}
if another.ConnectionIdle != nil {
p.ConnectionIdle = &Second{Value: another.ConnectionIdle.Value}
}
if another.UplinkOnly != nil {
p.UplinkOnly = &Second{Value: another.UplinkOnly.Value}
}
if another.DownlinkOnly != nil {
p.DownlinkOnly = &Second{Value: another.DownlinkOnly.Value}
}
}
func (p *Policy) overrideWith(another *Policy) {
if another.Timeout != nil {
p.Timeout.overrideWith(another.Timeout)
}
if another.Stats != nil && p.Stats == nil {
p.Stats = &Policy_Stats{}
p.Stats = another.Stats
}
if another.Buffer != nil {
p.Buffer = &Policy_Buffer{
Connection: another.Buffer.Connection,
}
}
}
// ToCorePolicy converts this Policy to policy.Session.
func (p *Policy) ToCorePolicy() policy.Session {
cp := policy.SessionDefault()
if p.Timeout != nil {
cp.Timeouts.ConnectionIdle = p.Timeout.ConnectionIdle.Duration()
cp.Timeouts.Handshake = p.Timeout.Handshake.Duration()
cp.Timeouts.DownlinkOnly = p.Timeout.DownlinkOnly.Duration()
cp.Timeouts.UplinkOnly = p.Timeout.UplinkOnly.Duration()
}
if p.Stats != nil {
cp.Stats.UserUplink = p.Stats.UserUplink
cp.Stats.UserDownlink = p.Stats.UserDownlink
}
if p.Buffer != nil {
cp.Buffer.PerConnection = p.Buffer.Connection
}
return cp
}
// ToCorePolicy converts this SystemPolicy to policy.System.
func (p *SystemPolicy) ToCorePolicy() policy.System {
return policy.System{
Stats: policy.SystemStats{
InboundUplink: p.Stats.InboundUplink,
InboundDownlink: p.Stats.InboundDownlink,
OutboundUplink: p.Stats.OutboundUplink,
OutboundDownlink: p.Stats.OutboundDownlink,
},
}
}

729
app/policy/config.pb.go Normal file
View file

@ -0,0 +1,729 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.14.0
// source: app/policy/config.proto
package policy
import (
proto "github.com/golang/protobuf/proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
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)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type Second struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"`
}
func (x *Second) Reset() {
*x = Second{}
if protoimpl.UnsafeEnabled {
mi := &file_app_policy_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Second) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Second) ProtoMessage() {}
func (x *Second) ProtoReflect() protoreflect.Message {
mi := &file_app_policy_config_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Second.ProtoReflect.Descriptor instead.
func (*Second) Descriptor() ([]byte, []int) {
return file_app_policy_config_proto_rawDescGZIP(), []int{0}
}
func (x *Second) GetValue() uint32 {
if x != nil {
return x.Value
}
return 0
}
type Policy struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Timeout *Policy_Timeout `protobuf:"bytes,1,opt,name=timeout,proto3" json:"timeout,omitempty"`
Stats *Policy_Stats `protobuf:"bytes,2,opt,name=stats,proto3" json:"stats,omitempty"`
Buffer *Policy_Buffer `protobuf:"bytes,3,opt,name=buffer,proto3" json:"buffer,omitempty"`
}
func (x *Policy) Reset() {
*x = Policy{}
if protoimpl.UnsafeEnabled {
mi := &file_app_policy_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Policy) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Policy) ProtoMessage() {}
func (x *Policy) ProtoReflect() protoreflect.Message {
mi := &file_app_policy_config_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Policy.ProtoReflect.Descriptor instead.
func (*Policy) Descriptor() ([]byte, []int) {
return file_app_policy_config_proto_rawDescGZIP(), []int{1}
}
func (x *Policy) GetTimeout() *Policy_Timeout {
if x != nil {
return x.Timeout
}
return nil
}
func (x *Policy) GetStats() *Policy_Stats {
if x != nil {
return x.Stats
}
return nil
}
func (x *Policy) GetBuffer() *Policy_Buffer {
if x != nil {
return x.Buffer
}
return nil
}
type SystemPolicy struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Stats *SystemPolicy_Stats `protobuf:"bytes,1,opt,name=stats,proto3" json:"stats,omitempty"`
}
func (x *SystemPolicy) Reset() {
*x = SystemPolicy{}
if protoimpl.UnsafeEnabled {
mi := &file_app_policy_config_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SystemPolicy) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SystemPolicy) ProtoMessage() {}
func (x *SystemPolicy) ProtoReflect() protoreflect.Message {
mi := &file_app_policy_config_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SystemPolicy.ProtoReflect.Descriptor instead.
func (*SystemPolicy) Descriptor() ([]byte, []int) {
return file_app_policy_config_proto_rawDescGZIP(), []int{2}
}
func (x *SystemPolicy) GetStats() *SystemPolicy_Stats {
if x != nil {
return x.Stats
}
return nil
}
type Config struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Level map[uint32]*Policy `protobuf:"bytes,1,rep,name=level,proto3" json:"level,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
System *SystemPolicy `protobuf:"bytes,2,opt,name=system,proto3" json:"system,omitempty"`
}
func (x *Config) Reset() {
*x = Config{}
if protoimpl.UnsafeEnabled {
mi := &file_app_policy_config_proto_msgTypes[3]
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_policy_config_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && 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_policy_config_proto_rawDescGZIP(), []int{3}
}
func (x *Config) GetLevel() map[uint32]*Policy {
if x != nil {
return x.Level
}
return nil
}
func (x *Config) GetSystem() *SystemPolicy {
if x != nil {
return x.System
}
return nil
}
// Timeout is a message for timeout settings in various stages, in seconds.
type Policy_Timeout struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Handshake *Second `protobuf:"bytes,1,opt,name=handshake,proto3" json:"handshake,omitempty"`
ConnectionIdle *Second `protobuf:"bytes,2,opt,name=connection_idle,json=connectionIdle,proto3" json:"connection_idle,omitempty"`
UplinkOnly *Second `protobuf:"bytes,3,opt,name=uplink_only,json=uplinkOnly,proto3" json:"uplink_only,omitempty"`
DownlinkOnly *Second `protobuf:"bytes,4,opt,name=downlink_only,json=downlinkOnly,proto3" json:"downlink_only,omitempty"`
}
func (x *Policy_Timeout) Reset() {
*x = Policy_Timeout{}
if protoimpl.UnsafeEnabled {
mi := &file_app_policy_config_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Policy_Timeout) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Policy_Timeout) ProtoMessage() {}
func (x *Policy_Timeout) ProtoReflect() protoreflect.Message {
mi := &file_app_policy_config_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Policy_Timeout.ProtoReflect.Descriptor instead.
func (*Policy_Timeout) Descriptor() ([]byte, []int) {
return file_app_policy_config_proto_rawDescGZIP(), []int{1, 0}
}
func (x *Policy_Timeout) GetHandshake() *Second {
if x != nil {
return x.Handshake
}
return nil
}
func (x *Policy_Timeout) GetConnectionIdle() *Second {
if x != nil {
return x.ConnectionIdle
}
return nil
}
func (x *Policy_Timeout) GetUplinkOnly() *Second {
if x != nil {
return x.UplinkOnly
}
return nil
}
func (x *Policy_Timeout) GetDownlinkOnly() *Second {
if x != nil {
return x.DownlinkOnly
}
return nil
}
type Policy_Stats struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserUplink bool `protobuf:"varint,1,opt,name=user_uplink,json=userUplink,proto3" json:"user_uplink,omitempty"`
UserDownlink bool `protobuf:"varint,2,opt,name=user_downlink,json=userDownlink,proto3" json:"user_downlink,omitempty"`
}
func (x *Policy_Stats) Reset() {
*x = Policy_Stats{}
if protoimpl.UnsafeEnabled {
mi := &file_app_policy_config_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Policy_Stats) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Policy_Stats) ProtoMessage() {}
func (x *Policy_Stats) ProtoReflect() protoreflect.Message {
mi := &file_app_policy_config_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Policy_Stats.ProtoReflect.Descriptor instead.
func (*Policy_Stats) Descriptor() ([]byte, []int) {
return file_app_policy_config_proto_rawDescGZIP(), []int{1, 1}
}
func (x *Policy_Stats) GetUserUplink() bool {
if x != nil {
return x.UserUplink
}
return false
}
func (x *Policy_Stats) GetUserDownlink() bool {
if x != nil {
return x.UserDownlink
}
return false
}
type Policy_Buffer struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Buffer size per connection, in bytes. -1 for unlimited buffer.
Connection int32 `protobuf:"varint,1,opt,name=connection,proto3" json:"connection,omitempty"`
}
func (x *Policy_Buffer) Reset() {
*x = Policy_Buffer{}
if protoimpl.UnsafeEnabled {
mi := &file_app_policy_config_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Policy_Buffer) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Policy_Buffer) ProtoMessage() {}
func (x *Policy_Buffer) ProtoReflect() protoreflect.Message {
mi := &file_app_policy_config_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Policy_Buffer.ProtoReflect.Descriptor instead.
func (*Policy_Buffer) Descriptor() ([]byte, []int) {
return file_app_policy_config_proto_rawDescGZIP(), []int{1, 2}
}
func (x *Policy_Buffer) GetConnection() int32 {
if x != nil {
return x.Connection
}
return 0
}
type SystemPolicy_Stats struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
InboundUplink bool `protobuf:"varint,1,opt,name=inbound_uplink,json=inboundUplink,proto3" json:"inbound_uplink,omitempty"`
InboundDownlink bool `protobuf:"varint,2,opt,name=inbound_downlink,json=inboundDownlink,proto3" json:"inbound_downlink,omitempty"`
OutboundUplink bool `protobuf:"varint,3,opt,name=outbound_uplink,json=outboundUplink,proto3" json:"outbound_uplink,omitempty"`
OutboundDownlink bool `protobuf:"varint,4,opt,name=outbound_downlink,json=outboundDownlink,proto3" json:"outbound_downlink,omitempty"`
}
func (x *SystemPolicy_Stats) Reset() {
*x = SystemPolicy_Stats{}
if protoimpl.UnsafeEnabled {
mi := &file_app_policy_config_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SystemPolicy_Stats) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SystemPolicy_Stats) ProtoMessage() {}
func (x *SystemPolicy_Stats) ProtoReflect() protoreflect.Message {
mi := &file_app_policy_config_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SystemPolicy_Stats.ProtoReflect.Descriptor instead.
func (*SystemPolicy_Stats) Descriptor() ([]byte, []int) {
return file_app_policy_config_proto_rawDescGZIP(), []int{2, 0}
}
func (x *SystemPolicy_Stats) GetInboundUplink() bool {
if x != nil {
return x.InboundUplink
}
return false
}
func (x *SystemPolicy_Stats) GetInboundDownlink() bool {
if x != nil {
return x.InboundDownlink
}
return false
}
func (x *SystemPolicy_Stats) GetOutboundUplink() bool {
if x != nil {
return x.OutboundUplink
}
return false
}
func (x *SystemPolicy_Stats) GetOutboundDownlink() bool {
if x != nil {
return x.OutboundDownlink
}
return false
}
var File_app_policy_config_proto protoreflect.FileDescriptor
var file_app_policy_config_proto_rawDesc = []byte{
0x0a, 0x17, 0x61, 0x70, 0x70, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x63, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x78, 0x72, 0x61, 0x79, 0x2e,
0x61, 0x70, 0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x1e, 0x0a, 0x06, 0x53, 0x65,
0x63, 0x6f, 0x6e, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xa6, 0x04, 0x0a, 0x06, 0x50,
0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x39, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70,
0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e,
0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74,
0x12, 0x33, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x1d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63,
0x79, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05,
0x73, 0x74, 0x61, 0x74, 0x73, 0x12, 0x36, 0x0a, 0x06, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x18,
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x42,
0x75, 0x66, 0x66, 0x65, 0x72, 0x52, 0x06, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x1a, 0xfa, 0x01,
0x0a, 0x07, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x35, 0x0a, 0x09, 0x68, 0x61, 0x6e,
0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x78,
0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53,
0x65, 0x63, 0x6f, 0x6e, 0x64, 0x52, 0x09, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65,
0x12, 0x40, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69,
0x64, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x78, 0x72, 0x61, 0x79,
0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x65, 0x63, 0x6f,
0x6e, 0x64, 0x52, 0x0e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64,
0x6c, 0x65, 0x12, 0x38, 0x0a, 0x0b, 0x75, 0x70, 0x6c, 0x69, 0x6e, 0x6b, 0x5f, 0x6f, 0x6e, 0x6c,
0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61,
0x70, 0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64,
0x52, 0x0a, 0x75, 0x70, 0x6c, 0x69, 0x6e, 0x6b, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x3c, 0x0a, 0x0d,
0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x04, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70,
0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x52, 0x0c, 0x64, 0x6f,
0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x4f, 0x6e, 0x6c, 0x79, 0x1a, 0x4d, 0x0a, 0x05, 0x53, 0x74,
0x61, 0x74, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x75, 0x70, 0x6c, 0x69,
0x6e, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x55, 0x70,
0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x23, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x64, 0x6f, 0x77,
0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x73, 0x65,
0x72, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x1a, 0x28, 0x0a, 0x06, 0x42, 0x75, 0x66,
0x66, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x22, 0xfb, 0x01, 0x0a, 0x0c, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50, 0x6f,
0x6c, 0x69, 0x63, 0x79, 0x12, 0x39, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70,
0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50, 0x6f, 0x6c, 0x69,
0x63, 0x79, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x1a,
0xaf, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x69, 0x6e, 0x62,
0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x75, 0x70, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28,
0x08, 0x52, 0x0d, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x55, 0x70, 0x6c, 0x69, 0x6e, 0x6b,
0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x64, 0x6f, 0x77, 0x6e,
0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x69, 0x6e, 0x62, 0x6f,
0x75, 0x6e, 0x64, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x27, 0x0a, 0x0f, 0x6f,
0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x75, 0x70, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x03,
0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x55, 0x70,
0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x2b, 0x0a, 0x11, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64,
0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52,
0x10, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e,
0x6b, 0x22, 0xcc, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x38, 0x0a, 0x05,
0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x78, 0x72,
0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x43, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52,
0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x35, 0x0a, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70,
0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50,
0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x1a, 0x51, 0x0a,
0x0a, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b,
0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a,
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x78,
0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50,
0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
0x42, 0x52, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x50, 0x01, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d,
0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x70, 0x6f, 0x6c, 0x69,
0x63, 0x79, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x50, 0x6f,
0x6c, 0x69, 0x63, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_app_policy_config_proto_rawDescOnce sync.Once
file_app_policy_config_proto_rawDescData = file_app_policy_config_proto_rawDesc
)
func file_app_policy_config_proto_rawDescGZIP() []byte {
file_app_policy_config_proto_rawDescOnce.Do(func() {
file_app_policy_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_policy_config_proto_rawDescData)
})
return file_app_policy_config_proto_rawDescData
}
var file_app_policy_config_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_app_policy_config_proto_goTypes = []interface{}{
(*Second)(nil), // 0: xray.app.policy.Second
(*Policy)(nil), // 1: xray.app.policy.Policy
(*SystemPolicy)(nil), // 2: xray.app.policy.SystemPolicy
(*Config)(nil), // 3: xray.app.policy.Config
(*Policy_Timeout)(nil), // 4: xray.app.policy.Policy.Timeout
(*Policy_Stats)(nil), // 5: xray.app.policy.Policy.Stats
(*Policy_Buffer)(nil), // 6: xray.app.policy.Policy.Buffer
(*SystemPolicy_Stats)(nil), // 7: xray.app.policy.SystemPolicy.Stats
nil, // 8: xray.app.policy.Config.LevelEntry
}
var file_app_policy_config_proto_depIdxs = []int32{
4, // 0: xray.app.policy.Policy.timeout:type_name -> xray.app.policy.Policy.Timeout
5, // 1: xray.app.policy.Policy.stats:type_name -> xray.app.policy.Policy.Stats
6, // 2: xray.app.policy.Policy.buffer:type_name -> xray.app.policy.Policy.Buffer
7, // 3: xray.app.policy.SystemPolicy.stats:type_name -> xray.app.policy.SystemPolicy.Stats
8, // 4: xray.app.policy.Config.level:type_name -> xray.app.policy.Config.LevelEntry
2, // 5: xray.app.policy.Config.system:type_name -> xray.app.policy.SystemPolicy
0, // 6: xray.app.policy.Policy.Timeout.handshake:type_name -> xray.app.policy.Second
0, // 7: xray.app.policy.Policy.Timeout.connection_idle:type_name -> xray.app.policy.Second
0, // 8: xray.app.policy.Policy.Timeout.uplink_only:type_name -> xray.app.policy.Second
0, // 9: xray.app.policy.Policy.Timeout.downlink_only:type_name -> xray.app.policy.Second
1, // 10: xray.app.policy.Config.LevelEntry.value:type_name -> xray.app.policy.Policy
11, // [11:11] is the sub-list for method output_type
11, // [11:11] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
}
func init() { file_app_policy_config_proto_init() }
func file_app_policy_config_proto_init() {
if File_app_policy_config_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_app_policy_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Second); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_policy_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Policy); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_policy_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SystemPolicy); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_policy_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_policy_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Policy_Timeout); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_policy_config_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Policy_Stats); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_policy_config_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Policy_Buffer); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_policy_config_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SystemPolicy_Stats); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_policy_config_proto_rawDesc,
NumEnums: 0,
NumMessages: 9,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_app_policy_config_proto_goTypes,
DependencyIndexes: file_app_policy_config_proto_depIdxs,
MessageInfos: file_app_policy_config_proto_msgTypes,
}.Build()
File_app_policy_config_proto = out.File
file_app_policy_config_proto_rawDesc = nil
file_app_policy_config_proto_goTypes = nil
file_app_policy_config_proto_depIdxs = nil
}

51
app/policy/config.proto Normal file
View file

@ -0,0 +1,51 @@
syntax = "proto3";
package xray.app.policy;
option csharp_namespace = "Xray.App.Policy";
option go_package = "github.com/xtls/xray-core/v1/app/policy";
option java_package = "com.xray.app.policy";
option java_multiple_files = true;
message Second {
uint32 value = 1;
}
message Policy {
// Timeout is a message for timeout settings in various stages, in seconds.
message Timeout {
Second handshake = 1;
Second connection_idle = 2;
Second uplink_only = 3;
Second downlink_only = 4;
}
message Stats {
bool user_uplink = 1;
bool user_downlink = 2;
}
message Buffer {
// Buffer size per connection, in bytes. -1 for unlimited buffer.
int32 connection = 1;
}
Timeout timeout = 1;
Stats stats = 2;
Buffer buffer = 3;
}
message SystemPolicy {
message Stats {
bool inbound_uplink = 1;
bool inbound_downlink = 2;
bool outbound_uplink = 3;
bool outbound_downlink = 4;
}
Stats stats = 1;
}
message Config {
map<uint32, Policy> level = 1;
SystemPolicy system = 2;
}

View file

@ -0,0 +1,9 @@
package policy
import "github.com/xtls/xray-core/v1/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

68
app/policy/manager.go Normal file
View file

@ -0,0 +1,68 @@
package policy
import (
"context"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/features/policy"
)
// Instance is an instance of Policy manager.
type Instance struct {
levels map[uint32]*Policy
system *SystemPolicy
}
// New creates new Policy manager instance.
func New(ctx context.Context, config *Config) (*Instance, error) {
m := &Instance{
levels: make(map[uint32]*Policy),
system: config.System,
}
if len(config.Level) > 0 {
for lv, p := range config.Level {
pp := defaultPolicy()
pp.overrideWith(p)
m.levels[lv] = pp
}
}
return m, nil
}
// Type implements common.HasType.
func (*Instance) Type() interface{} {
return policy.ManagerType()
}
// ForLevel implements policy.Manager.
func (m *Instance) ForLevel(level uint32) policy.Session {
if p, ok := m.levels[level]; ok {
return p.ToCorePolicy()
}
return policy.SessionDefault()
}
// ForSystem implements policy.Manager.
func (m *Instance) ForSystem() policy.System {
if m.system == nil {
return policy.System{}
}
return m.system.ToCorePolicy()
}
// Start implements common.Runnable.Start().
func (m *Instance) Start() error {
return nil
}
// Close implements common.Closable.Close().
func (m *Instance) Close() error {
return nil
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return New(ctx, config.(*Config))
}))
}

View file

@ -0,0 +1,45 @@
package policy_test
import (
"context"
"testing"
"time"
. "github.com/xtls/xray-core/v1/app/policy"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/features/policy"
)
func TestPolicy(t *testing.T) {
manager, err := New(context.Background(), &Config{
Level: map[uint32]*Policy{
0: {
Timeout: &Policy_Timeout{
Handshake: &Second{
Value: 2,
},
},
},
},
})
common.Must(err)
pDefault := policy.SessionDefault()
{
p := manager.ForLevel(0)
if p.Timeouts.Handshake != 2*time.Second {
t.Error("expect 2 sec timeout, but got ", p.Timeouts.Handshake)
}
if p.Timeouts.ConnectionIdle != pDefault.Timeouts.ConnectionIdle {
t.Error("expect ", pDefault.Timeouts.ConnectionIdle, " sec timeout, but got ", p.Timeouts.ConnectionIdle)
}
}
{
p := manager.ForLevel(1)
if p.Timeouts.Handshake != pDefault.Timeouts.Handshake {
t.Error("expect ", pDefault.Timeouts.Handshake, " sec timeout, but got ", p.Timeouts.Handshake)
}
}
}

4
app/policy/policy.go Normal file
View file

@ -0,0 +1,4 @@
// Package policy is an implementation of policy.Manager feature.
package policy
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen

View file

@ -0,0 +1,150 @@
// +build !confonly
package command
import (
"context"
grpc "google.golang.org/grpc"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/core"
"github.com/xtls/xray-core/v1/features/inbound"
"github.com/xtls/xray-core/v1/features/outbound"
"github.com/xtls/xray-core/v1/proxy"
)
// InboundOperation is the interface for operations that applies to inbound handlers.
type InboundOperation interface {
// ApplyInbound applies this operation to the given inbound handler.
ApplyInbound(context.Context, inbound.Handler) error
}
// OutboundOperation is the interface for operations that applies to outbound handlers.
type OutboundOperation interface {
// ApplyOutbound applies this operation to the given outbound handler.
ApplyOutbound(context.Context, outbound.Handler) error
}
func getInbound(handler inbound.Handler) (proxy.Inbound, error) {
gi, ok := handler.(proxy.GetInbound)
if !ok {
return nil, newError("can't get inbound proxy from handler.")
}
return gi.GetInbound(), nil
}
// ApplyInbound implements InboundOperation.
func (op *AddUserOperation) ApplyInbound(ctx context.Context, handler inbound.Handler) error {
p, err := getInbound(handler)
if err != nil {
return err
}
um, ok := p.(proxy.UserManager)
if !ok {
return newError("proxy is not a UserManager")
}
mUser, err := op.User.ToMemoryUser()
if err != nil {
return newError("failed to parse user").Base(err)
}
return um.AddUser(ctx, mUser)
}
// ApplyInbound implements InboundOperation.
func (op *RemoveUserOperation) ApplyInbound(ctx context.Context, handler inbound.Handler) error {
p, err := getInbound(handler)
if err != nil {
return err
}
um, ok := p.(proxy.UserManager)
if !ok {
return newError("proxy is not a UserManager")
}
return um.RemoveUser(ctx, op.Email)
}
type handlerServer struct {
s *core.Instance
ihm inbound.Manager
ohm outbound.Manager
}
func (s *handlerServer) AddInbound(ctx context.Context, request *AddInboundRequest) (*AddInboundResponse, error) {
if err := core.AddInboundHandler(s.s, request.Inbound); err != nil {
return nil, err
}
return &AddInboundResponse{}, nil
}
func (s *handlerServer) RemoveInbound(ctx context.Context, request *RemoveInboundRequest) (*RemoveInboundResponse, error) {
return &RemoveInboundResponse{}, s.ihm.RemoveHandler(ctx, request.Tag)
}
func (s *handlerServer) AlterInbound(ctx context.Context, request *AlterInboundRequest) (*AlterInboundResponse, error) {
rawOperation, err := request.Operation.GetInstance()
if err != nil {
return nil, newError("unknown operation").Base(err)
}
operation, ok := rawOperation.(InboundOperation)
if !ok {
return nil, newError("not an inbound operation")
}
handler, err := s.ihm.GetHandler(ctx, request.Tag)
if err != nil {
return nil, newError("failed to get handler: ", request.Tag).Base(err)
}
return &AlterInboundResponse{}, operation.ApplyInbound(ctx, handler)
}
func (s *handlerServer) AddOutbound(ctx context.Context, request *AddOutboundRequest) (*AddOutboundResponse, error) {
if err := core.AddOutboundHandler(s.s, request.Outbound); err != nil {
return nil, err
}
return &AddOutboundResponse{}, nil
}
func (s *handlerServer) RemoveOutbound(ctx context.Context, request *RemoveOutboundRequest) (*RemoveOutboundResponse, error) {
return &RemoveOutboundResponse{}, s.ohm.RemoveHandler(ctx, request.Tag)
}
func (s *handlerServer) AlterOutbound(ctx context.Context, request *AlterOutboundRequest) (*AlterOutboundResponse, error) {
rawOperation, err := request.Operation.GetInstance()
if err != nil {
return nil, newError("unknown operation").Base(err)
}
operation, ok := rawOperation.(OutboundOperation)
if !ok {
return nil, newError("not an outbound operation")
}
handler := s.ohm.GetHandler(request.Tag)
return &AlterOutboundResponse{}, operation.ApplyOutbound(ctx, handler)
}
func (s *handlerServer) mustEmbedUnimplementedHandlerServiceServer() {}
type service struct {
v *core.Instance
}
func (s *service) Register(server *grpc.Server) {
hs := &handlerServer{
s: s.v,
}
common.Must(s.v.RequireFeatures(func(im inbound.Manager, om outbound.Manager) {
hs.ihm = im
hs.ohm = om
}))
RegisterHandlerServiceServer(server, hs)
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) {
s := core.MustFromContext(ctx)
return &service{v: s}, nil
}))
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,73 @@
syntax = "proto3";
package xray.app.proxyman.command;
option csharp_namespace = "Xray.App.Proxyman.Command";
option go_package = "github.com/xtls/xray-core/v1/app/proxyman/command";
option java_package = "com.xray.app.proxyman.command";
option java_multiple_files = true;
import "common/protocol/user.proto";
import "common/serial/typed_message.proto";
import "core/config.proto";
message AddUserOperation {
xray.common.protocol.User user = 1;
}
message RemoveUserOperation {
string email = 1;
}
message AddInboundRequest {
core.InboundHandlerConfig inbound = 1;
}
message AddInboundResponse {}
message RemoveInboundRequest {
string tag = 1;
}
message RemoveInboundResponse {}
message AlterInboundRequest {
string tag = 1;
xray.common.serial.TypedMessage operation = 2;
}
message AlterInboundResponse {}
message AddOutboundRequest {
core.OutboundHandlerConfig outbound = 1;
}
message AddOutboundResponse {}
message RemoveOutboundRequest {
string tag = 1;
}
message RemoveOutboundResponse {}
message AlterOutboundRequest {
string tag = 1;
xray.common.serial.TypedMessage operation = 2;
}
message AlterOutboundResponse {}
service HandlerService {
rpc AddInbound(AddInboundRequest) returns (AddInboundResponse) {}
rpc RemoveInbound(RemoveInboundRequest) returns (RemoveInboundResponse) {}
rpc AlterInbound(AlterInboundRequest) returns (AlterInboundResponse) {}
rpc AddOutbound(AddOutboundRequest) returns (AddOutboundResponse) {}
rpc RemoveOutbound(RemoveOutboundRequest) returns (RemoveOutboundResponse) {}
rpc AlterOutbound(AlterOutboundRequest) returns (AlterOutboundResponse) {}
}
message Config {}

View file

@ -0,0 +1,277 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package command
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion7
// HandlerServiceClient is the client API for HandlerService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type HandlerServiceClient interface {
AddInbound(ctx context.Context, in *AddInboundRequest, opts ...grpc.CallOption) (*AddInboundResponse, error)
RemoveInbound(ctx context.Context, in *RemoveInboundRequest, opts ...grpc.CallOption) (*RemoveInboundResponse, error)
AlterInbound(ctx context.Context, in *AlterInboundRequest, opts ...grpc.CallOption) (*AlterInboundResponse, error)
AddOutbound(ctx context.Context, in *AddOutboundRequest, opts ...grpc.CallOption) (*AddOutboundResponse, error)
RemoveOutbound(ctx context.Context, in *RemoveOutboundRequest, opts ...grpc.CallOption) (*RemoveOutboundResponse, error)
AlterOutbound(ctx context.Context, in *AlterOutboundRequest, opts ...grpc.CallOption) (*AlterOutboundResponse, error)
}
type handlerServiceClient struct {
cc grpc.ClientConnInterface
}
func NewHandlerServiceClient(cc grpc.ClientConnInterface) HandlerServiceClient {
return &handlerServiceClient{cc}
}
func (c *handlerServiceClient) AddInbound(ctx context.Context, in *AddInboundRequest, opts ...grpc.CallOption) (*AddInboundResponse, error) {
out := new(AddInboundResponse)
err := c.cc.Invoke(ctx, "/xray.app.proxyman.command.HandlerService/AddInbound", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *handlerServiceClient) RemoveInbound(ctx context.Context, in *RemoveInboundRequest, opts ...grpc.CallOption) (*RemoveInboundResponse, error) {
out := new(RemoveInboundResponse)
err := c.cc.Invoke(ctx, "/xray.app.proxyman.command.HandlerService/RemoveInbound", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *handlerServiceClient) AlterInbound(ctx context.Context, in *AlterInboundRequest, opts ...grpc.CallOption) (*AlterInboundResponse, error) {
out := new(AlterInboundResponse)
err := c.cc.Invoke(ctx, "/xray.app.proxyman.command.HandlerService/AlterInbound", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *handlerServiceClient) AddOutbound(ctx context.Context, in *AddOutboundRequest, opts ...grpc.CallOption) (*AddOutboundResponse, error) {
out := new(AddOutboundResponse)
err := c.cc.Invoke(ctx, "/xray.app.proxyman.command.HandlerService/AddOutbound", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *handlerServiceClient) RemoveOutbound(ctx context.Context, in *RemoveOutboundRequest, opts ...grpc.CallOption) (*RemoveOutboundResponse, error) {
out := new(RemoveOutboundResponse)
err := c.cc.Invoke(ctx, "/xray.app.proxyman.command.HandlerService/RemoveOutbound", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *handlerServiceClient) AlterOutbound(ctx context.Context, in *AlterOutboundRequest, opts ...grpc.CallOption) (*AlterOutboundResponse, error) {
out := new(AlterOutboundResponse)
err := c.cc.Invoke(ctx, "/xray.app.proxyman.command.HandlerService/AlterOutbound", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// HandlerServiceServer is the server API for HandlerService service.
// All implementations must embed UnimplementedHandlerServiceServer
// for forward compatibility
type HandlerServiceServer interface {
AddInbound(context.Context, *AddInboundRequest) (*AddInboundResponse, error)
RemoveInbound(context.Context, *RemoveInboundRequest) (*RemoveInboundResponse, error)
AlterInbound(context.Context, *AlterInboundRequest) (*AlterInboundResponse, error)
AddOutbound(context.Context, *AddOutboundRequest) (*AddOutboundResponse, error)
RemoveOutbound(context.Context, *RemoveOutboundRequest) (*RemoveOutboundResponse, error)
AlterOutbound(context.Context, *AlterOutboundRequest) (*AlterOutboundResponse, error)
mustEmbedUnimplementedHandlerServiceServer()
}
// UnimplementedHandlerServiceServer must be embedded to have forward compatible implementations.
type UnimplementedHandlerServiceServer struct {
}
func (UnimplementedHandlerServiceServer) AddInbound(context.Context, *AddInboundRequest) (*AddInboundResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AddInbound not implemented")
}
func (UnimplementedHandlerServiceServer) RemoveInbound(context.Context, *RemoveInboundRequest) (*RemoveInboundResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveInbound not implemented")
}
func (UnimplementedHandlerServiceServer) AlterInbound(context.Context, *AlterInboundRequest) (*AlterInboundResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AlterInbound not implemented")
}
func (UnimplementedHandlerServiceServer) AddOutbound(context.Context, *AddOutboundRequest) (*AddOutboundResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AddOutbound not implemented")
}
func (UnimplementedHandlerServiceServer) RemoveOutbound(context.Context, *RemoveOutboundRequest) (*RemoveOutboundResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveOutbound not implemented")
}
func (UnimplementedHandlerServiceServer) AlterOutbound(context.Context, *AlterOutboundRequest) (*AlterOutboundResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AlterOutbound not implemented")
}
func (UnimplementedHandlerServiceServer) mustEmbedUnimplementedHandlerServiceServer() {}
// UnsafeHandlerServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to HandlerServiceServer will
// result in compilation errors.
type UnsafeHandlerServiceServer interface {
mustEmbedUnimplementedHandlerServiceServer()
}
func RegisterHandlerServiceServer(s grpc.ServiceRegistrar, srv HandlerServiceServer) {
s.RegisterService(&_HandlerService_serviceDesc, srv)
}
func _HandlerService_AddInbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AddInboundRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HandlerServiceServer).AddInbound(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/xray.app.proxyman.command.HandlerService/AddInbound",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HandlerServiceServer).AddInbound(ctx, req.(*AddInboundRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HandlerService_RemoveInbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RemoveInboundRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HandlerServiceServer).RemoveInbound(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/xray.app.proxyman.command.HandlerService/RemoveInbound",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HandlerServiceServer).RemoveInbound(ctx, req.(*RemoveInboundRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HandlerService_AlterInbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AlterInboundRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HandlerServiceServer).AlterInbound(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/xray.app.proxyman.command.HandlerService/AlterInbound",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HandlerServiceServer).AlterInbound(ctx, req.(*AlterInboundRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HandlerService_AddOutbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AddOutboundRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HandlerServiceServer).AddOutbound(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/xray.app.proxyman.command.HandlerService/AddOutbound",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HandlerServiceServer).AddOutbound(ctx, req.(*AddOutboundRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HandlerService_RemoveOutbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RemoveOutboundRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HandlerServiceServer).RemoveOutbound(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/xray.app.proxyman.command.HandlerService/RemoveOutbound",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HandlerServiceServer).RemoveOutbound(ctx, req.(*RemoveOutboundRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HandlerService_AlterOutbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AlterOutboundRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HandlerServiceServer).AlterOutbound(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/xray.app.proxyman.command.HandlerService/AlterOutbound",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HandlerServiceServer).AlterOutbound(ctx, req.(*AlterOutboundRequest))
}
return interceptor(ctx, in, info, handler)
}
var _HandlerService_serviceDesc = grpc.ServiceDesc{
ServiceName: "xray.app.proxyman.command.HandlerService",
HandlerType: (*HandlerServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "AddInbound",
Handler: _HandlerService_AddInbound_Handler,
},
{
MethodName: "RemoveInbound",
Handler: _HandlerService_RemoveInbound_Handler,
},
{
MethodName: "AlterInbound",
Handler: _HandlerService_AlterInbound_Handler,
},
{
MethodName: "AddOutbound",
Handler: _HandlerService_AddOutbound_Handler,
},
{
MethodName: "RemoveOutbound",
Handler: _HandlerService_RemoveOutbound_Handler,
},
{
MethodName: "AlterOutbound",
Handler: _HandlerService_AlterOutbound_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "app/proxyman/command/command.proto",
}

View file

@ -0,0 +1,3 @@
package command
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen

View file

@ -0,0 +1,9 @@
package command
import "github.com/xtls/xray-core/v1/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

39
app/proxyman/config.go Normal file
View file

@ -0,0 +1,39 @@
package proxyman
func (s *AllocationStrategy) GetConcurrencyValue() uint32 {
if s == nil || s.Concurrency == nil {
return 3
}
return s.Concurrency.Value
}
func (s *AllocationStrategy) GetRefreshValue() uint32 {
if s == nil || s.Refresh == nil {
return 5
}
return s.Refresh.Value
}
func (c *ReceiverConfig) GetEffectiveSniffingSettings() *SniffingConfig {
if c.SniffingSettings != nil {
return c.SniffingSettings
}
if len(c.DomainOverride) > 0 {
var p []string
for _, kd := range c.DomainOverride {
switch kd {
case KnownProtocols_HTTP:
p = append(p, "http")
case KnownProtocols_TLS:
p = append(p, "tls")
}
}
return &SniffingConfig{
Enabled: true,
DestinationOverride: p,
}
}
return nil
}

1049
app/proxyman/config.pb.go Normal file

File diff suppressed because it is too large Load diff

97
app/proxyman/config.proto Normal file
View file

@ -0,0 +1,97 @@
syntax = "proto3";
package xray.app.proxyman;
option csharp_namespace = "Xray.App.Proxyman";
option go_package = "github.com/xtls/xray-core/v1/app/proxyman";
option java_package = "com.xray.app.proxyman";
option java_multiple_files = true;
import "common/net/address.proto";
import "common/net/port.proto";
import "transport/internet/config.proto";
import "common/serial/typed_message.proto";
message InboundConfig {}
message AllocationStrategy {
enum Type {
// Always allocate all connection handlers.
Always = 0;
// Randomly allocate specific range of handlers.
Random = 1;
// External. Not supported yet.
External = 2;
}
Type type = 1;
message AllocationStrategyConcurrency {
uint32 value = 1;
}
// Number of handlers (ports) running in parallel.
// Default value is 3 if unset.
AllocationStrategyConcurrency concurrency = 2;
message AllocationStrategyRefresh {
uint32 value = 1;
}
// Number of minutes before a handler is regenerated.
// Default value is 5 if unset.
AllocationStrategyRefresh refresh = 3;
}
enum KnownProtocols {
HTTP = 0;
TLS = 1;
}
message SniffingConfig {
// Whether or not to enable content sniffing on an inbound connection.
bool enabled = 1;
// Override target destination if sniff'ed protocol is in the given list.
// Supported values are "http", "tls".
repeated string destination_override = 2;
}
message ReceiverConfig {
// PortRange specifies the ports which the Receiver should listen on.
xray.common.net.PortRange port_range = 1;
// Listen specifies the IP address that the Receiver should listen on.
xray.common.net.IPOrDomain listen = 2;
AllocationStrategy allocation_strategy = 3;
xray.transport.internet.StreamConfig stream_settings = 4;
bool receive_original_destination = 5;
reserved 6;
// Override domains for the given protocol.
// Deprecated. Use sniffing_settings.
repeated KnownProtocols domain_override = 7 [deprecated = true];
SniffingConfig sniffing_settings = 8;
}
message InboundHandlerConfig {
string tag = 1;
xray.common.serial.TypedMessage receiver_settings = 2;
xray.common.serial.TypedMessage proxy_settings = 3;
}
message OutboundConfig {}
message SenderConfig {
// Send traffic through the given IP. Only IP is allowed.
xray.common.net.IPOrDomain via = 1;
xray.transport.internet.StreamConfig stream_settings = 2;
xray.transport.internet.ProxyConfig proxy_settings = 3;
MultiplexingConfig multiplex_settings = 4;
}
message MultiplexingConfig {
// Whether or not Mux is enabled.
bool enabled = 1;
// Max number of concurrent connections that one Mux connection can handle.
uint32 concurrency = 2;
}

View file

@ -0,0 +1,185 @@
package inbound
import (
"context"
"github.com/xtls/xray-core/v1/app/proxyman"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/dice"
"github.com/xtls/xray-core/v1/common/errors"
"github.com/xtls/xray-core/v1/common/mux"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/core"
"github.com/xtls/xray-core/v1/features/policy"
"github.com/xtls/xray-core/v1/features/stats"
"github.com/xtls/xray-core/v1/proxy"
"github.com/xtls/xray-core/v1/transport/internet"
)
func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter) {
var uplinkCounter stats.Counter
var downlinkCounter stats.Counter
policy := v.GetFeature(policy.ManagerType()).(policy.Manager)
if len(tag) > 0 && policy.ForSystem().Stats.InboundUplink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + tag + ">>>traffic>>>uplink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
uplinkCounter = c
}
}
if len(tag) > 0 && policy.ForSystem().Stats.InboundDownlink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + tag + ">>>traffic>>>downlink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
downlinkCounter = c
}
}
return uplinkCounter, downlinkCounter
}
type AlwaysOnInboundHandler struct {
proxy proxy.Inbound
workers []worker
mux *mux.Server
tag string
}
func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *proxyman.ReceiverConfig, proxyConfig interface{}) (*AlwaysOnInboundHandler, error) {
rawProxy, err := common.CreateObject(ctx, proxyConfig)
if err != nil {
return nil, err
}
p, ok := rawProxy.(proxy.Inbound)
if !ok {
return nil, newError("not an inbound proxy.")
}
h := &AlwaysOnInboundHandler{
proxy: p,
mux: mux.NewServer(ctx),
tag: tag,
}
uplinkCounter, downlinkCounter := getStatCounter(core.MustFromContext(ctx), tag)
nl := p.Network()
pr := receiverConfig.PortRange
address := receiverConfig.Listen.AsAddress()
if address == nil {
address = net.AnyIP
}
mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings)
if err != nil {
return nil, newError("failed to parse stream config").Base(err).AtWarning()
}
if receiverConfig.ReceiveOriginalDestination {
if mss.SocketSettings == nil {
mss.SocketSettings = &internet.SocketConfig{}
}
if mss.SocketSettings.Tproxy == internet.SocketConfig_Off {
mss.SocketSettings.Tproxy = internet.SocketConfig_Redirect
}
mss.SocketSettings.ReceiveOriginalDestAddress = true
}
if pr == nil {
if net.HasNetwork(nl, net.Network_UNIX) {
newError("creating unix domain socket worker on ", address).AtDebug().WriteToLog()
worker := &dsWorker{
address: address,
proxy: p,
stream: mss,
tag: tag,
dispatcher: h.mux,
sniffingConfig: receiverConfig.GetEffectiveSniffingSettings(),
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
ctx: ctx,
}
h.workers = append(h.workers, worker)
}
}
if pr != nil {
for port := pr.From; port <= pr.To; port++ {
if net.HasNetwork(nl, net.Network_TCP) {
newError("creating stream worker on ", address, ":", port).AtDebug().WriteToLog()
worker := &tcpWorker{
address: address,
port: net.Port(port),
proxy: p,
stream: mss,
recvOrigDest: receiverConfig.ReceiveOriginalDestination,
tag: tag,
dispatcher: h.mux,
sniffingConfig: receiverConfig.GetEffectiveSniffingSettings(),
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
ctx: ctx,
}
h.workers = append(h.workers, worker)
}
if net.HasNetwork(nl, net.Network_UDP) {
worker := &udpWorker{
tag: tag,
proxy: p,
address: address,
port: net.Port(port),
dispatcher: h.mux,
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
stream: mss,
}
h.workers = append(h.workers, worker)
}
}
}
return h, nil
}
// Start implements common.Runnable.
func (h *AlwaysOnInboundHandler) Start() error {
for _, worker := range h.workers {
if err := worker.Start(); err != nil {
return err
}
}
return nil
}
// Close implements common.Closable.
func (h *AlwaysOnInboundHandler) Close() error {
var errs []error
for _, worker := range h.workers {
errs = append(errs, worker.Close())
}
errs = append(errs, h.mux.Close())
if err := errors.Combine(errs...); err != nil {
return newError("failed to close all resources").Base(err)
}
return nil
}
func (h *AlwaysOnInboundHandler) GetRandomInboundProxy() (interface{}, net.Port, int) {
if len(h.workers) == 0 {
return nil, 0, 0
}
w := h.workers[dice.Roll(len(h.workers))]
return w.Proxy(), w.Port(), 9999
}
func (h *AlwaysOnInboundHandler) Tag() string {
return h.tag
}
func (h *AlwaysOnInboundHandler) GetInbound() proxy.Inbound {
return h.proxy
}

View file

@ -0,0 +1,201 @@
package inbound
import (
"context"
"sync"
"time"
"github.com/xtls/xray-core/v1/app/proxyman"
"github.com/xtls/xray-core/v1/common/dice"
"github.com/xtls/xray-core/v1/common/mux"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/task"
"github.com/xtls/xray-core/v1/core"
"github.com/xtls/xray-core/v1/proxy"
"github.com/xtls/xray-core/v1/transport/internet"
)
type DynamicInboundHandler struct {
tag string
v *core.Instance
proxyConfig interface{}
receiverConfig *proxyman.ReceiverConfig
streamSettings *internet.MemoryStreamConfig
portMutex sync.Mutex
portsInUse map[net.Port]bool
workerMutex sync.RWMutex
worker []worker
lastRefresh time.Time
mux *mux.Server
task *task.Periodic
ctx context.Context
}
func NewDynamicInboundHandler(ctx context.Context, tag string, receiverConfig *proxyman.ReceiverConfig, proxyConfig interface{}) (*DynamicInboundHandler, error) {
v := core.MustFromContext(ctx)
h := &DynamicInboundHandler{
tag: tag,
proxyConfig: proxyConfig,
receiverConfig: receiverConfig,
portsInUse: make(map[net.Port]bool),
mux: mux.NewServer(ctx),
v: v,
ctx: ctx,
}
mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings)
if err != nil {
return nil, newError("failed to parse stream settings").Base(err).AtWarning()
}
if receiverConfig.ReceiveOriginalDestination {
if mss.SocketSettings == nil {
mss.SocketSettings = &internet.SocketConfig{}
}
if mss.SocketSettings.Tproxy == internet.SocketConfig_Off {
mss.SocketSettings.Tproxy = internet.SocketConfig_Redirect
}
mss.SocketSettings.ReceiveOriginalDestAddress = true
}
h.streamSettings = mss
h.task = &task.Periodic{
Interval: time.Minute * time.Duration(h.receiverConfig.AllocationStrategy.GetRefreshValue()),
Execute: h.refresh,
}
return h, nil
}
func (h *DynamicInboundHandler) allocatePort() net.Port {
from := int(h.receiverConfig.PortRange.From)
delta := int(h.receiverConfig.PortRange.To) - from + 1
h.portMutex.Lock()
defer h.portMutex.Unlock()
for {
r := dice.Roll(delta)
port := net.Port(from + r)
_, used := h.portsInUse[port]
if !used {
h.portsInUse[port] = true
return port
}
}
}
func (h *DynamicInboundHandler) closeWorkers(workers []worker) {
ports2Del := make([]net.Port, len(workers))
for idx, worker := range workers {
ports2Del[idx] = worker.Port()
if err := worker.Close(); err != nil {
newError("failed to close worker").Base(err).WriteToLog()
}
}
h.portMutex.Lock()
for _, port := range ports2Del {
delete(h.portsInUse, port)
}
h.portMutex.Unlock()
}
func (h *DynamicInboundHandler) refresh() error {
h.lastRefresh = time.Now()
timeout := time.Minute * time.Duration(h.receiverConfig.AllocationStrategy.GetRefreshValue()) * 2
concurrency := h.receiverConfig.AllocationStrategy.GetConcurrencyValue()
workers := make([]worker, 0, concurrency)
address := h.receiverConfig.Listen.AsAddress()
if address == nil {
address = net.AnyIP
}
uplinkCounter, downlinkCounter := getStatCounter(h.v, h.tag)
for i := uint32(0); i < concurrency; i++ {
port := h.allocatePort()
rawProxy, err := core.CreateObject(h.v, h.proxyConfig)
if err != nil {
newError("failed to create proxy instance").Base(err).AtWarning().WriteToLog()
continue
}
p := rawProxy.(proxy.Inbound)
nl := p.Network()
if net.HasNetwork(nl, net.Network_TCP) {
worker := &tcpWorker{
tag: h.tag,
address: address,
port: port,
proxy: p,
stream: h.streamSettings,
recvOrigDest: h.receiverConfig.ReceiveOriginalDestination,
dispatcher: h.mux,
sniffingConfig: h.receiverConfig.GetEffectiveSniffingSettings(),
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
ctx: h.ctx,
}
if err := worker.Start(); err != nil {
newError("failed to create TCP worker").Base(err).AtWarning().WriteToLog()
continue
}
workers = append(workers, worker)
}
if net.HasNetwork(nl, net.Network_UDP) {
worker := &udpWorker{
tag: h.tag,
proxy: p,
address: address,
port: port,
dispatcher: h.mux,
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
stream: h.streamSettings,
}
if err := worker.Start(); err != nil {
newError("failed to create UDP worker").Base(err).AtWarning().WriteToLog()
continue
}
workers = append(workers, worker)
}
}
h.workerMutex.Lock()
h.worker = workers
h.workerMutex.Unlock()
time.AfterFunc(timeout, func() {
h.closeWorkers(workers)
})
return nil
}
func (h *DynamicInboundHandler) Start() error {
return h.task.Start()
}
func (h *DynamicInboundHandler) Close() error {
return h.task.Close()
}
func (h *DynamicInboundHandler) GetRandomInboundProxy() (interface{}, net.Port, int) {
h.workerMutex.RLock()
defer h.workerMutex.RUnlock()
if len(h.worker) == 0 {
return nil, 0, 0
}
w := h.worker[dice.Roll(len(h.worker))]
expire := h.receiverConfig.AllocationStrategy.GetRefreshValue() - uint32(time.Since(h.lastRefresh)/time.Minute)
return w.Proxy(), w.Port(), int(expire)
}
func (h *DynamicInboundHandler) Tag() string {
return h.tag
}

View file

@ -0,0 +1,9 @@
package inbound
import "github.com/xtls/xray-core/v1/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

View file

@ -0,0 +1,178 @@
package inbound
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
import (
"context"
"sync"
"github.com/xtls/xray-core/v1/app/proxyman"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/serial"
"github.com/xtls/xray-core/v1/common/session"
"github.com/xtls/xray-core/v1/core"
"github.com/xtls/xray-core/v1/features/inbound"
)
// Manager is to manage all inbound handlers.
type Manager struct {
access sync.RWMutex
untaggedHandler []inbound.Handler
taggedHandlers map[string]inbound.Handler
running bool
}
// New returns a new Manager for inbound handlers.
func New(ctx context.Context, config *proxyman.InboundConfig) (*Manager, error) {
m := &Manager{
taggedHandlers: make(map[string]inbound.Handler),
}
return m, nil
}
// Type implements common.HasType.
func (*Manager) Type() interface{} {
return inbound.ManagerType()
}
// AddHandler implements inbound.Manager.
func (m *Manager) AddHandler(ctx context.Context, handler inbound.Handler) error {
m.access.Lock()
defer m.access.Unlock()
tag := handler.Tag()
if len(tag) > 0 {
m.taggedHandlers[tag] = handler
} else {
m.untaggedHandler = append(m.untaggedHandler, handler)
}
if m.running {
return handler.Start()
}
return nil
}
// GetHandler implements inbound.Manager.
func (m *Manager) GetHandler(ctx context.Context, tag string) (inbound.Handler, error) {
m.access.RLock()
defer m.access.RUnlock()
handler, found := m.taggedHandlers[tag]
if !found {
return nil, newError("handler not found: ", tag)
}
return handler, nil
}
// RemoveHandler implements inbound.Manager.
func (m *Manager) RemoveHandler(ctx context.Context, tag string) error {
if tag == "" {
return common.ErrNoClue
}
m.access.Lock()
defer m.access.Unlock()
if handler, found := m.taggedHandlers[tag]; found {
if err := handler.Close(); err != nil {
newError("failed to close handler ", tag).Base(err).AtWarning().WriteToLog(session.ExportIDToError(ctx))
}
delete(m.taggedHandlers, tag)
return nil
}
return common.ErrNoClue
}
// Start implements common.Runnable.
func (m *Manager) Start() error {
m.access.Lock()
defer m.access.Unlock()
m.running = true
for _, handler := range m.taggedHandlers {
if err := handler.Start(); err != nil {
return err
}
}
for _, handler := range m.untaggedHandler {
if err := handler.Start(); err != nil {
return err
}
}
return nil
}
// Close implements common.Closable.
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
m.running = false
var errors []interface{}
for _, handler := range m.taggedHandlers {
if err := handler.Close(); err != nil {
errors = append(errors, err)
}
}
for _, handler := range m.untaggedHandler {
if err := handler.Close(); err != nil {
errors = append(errors, err)
}
}
if len(errors) > 0 {
return newError("failed to close all handlers").Base(newError(serial.Concat(errors...)))
}
return nil
}
// NewHandler creates a new inbound.Handler based on the given config.
func NewHandler(ctx context.Context, config *core.InboundHandlerConfig) (inbound.Handler, error) {
rawReceiverSettings, err := config.ReceiverSettings.GetInstance()
if err != nil {
return nil, err
}
proxySettings, err := config.ProxySettings.GetInstance()
if err != nil {
return nil, err
}
tag := config.Tag
receiverSettings, ok := rawReceiverSettings.(*proxyman.ReceiverConfig)
if !ok {
return nil, newError("not a ReceiverConfig").AtError()
}
streamSettings := receiverSettings.StreamSettings
if streamSettings != nil && streamSettings.SocketSettings != nil {
ctx = session.ContextWithSockopt(ctx, &session.Sockopt{
Mark: streamSettings.SocketSettings.Mark,
})
}
allocStrategy := receiverSettings.AllocationStrategy
if allocStrategy == nil || allocStrategy.Type == proxyman.AllocationStrategy_Always {
return NewAlwaysOnInboundHandler(ctx, tag, receiverSettings, proxySettings)
}
if allocStrategy.Type == proxyman.AllocationStrategy_Random {
return NewDynamicInboundHandler(ctx, tag, receiverSettings, proxySettings)
}
return nil, newError("unknown allocation strategy: ", receiverSettings.AllocationStrategy.Type).AtError()
}
func init() {
common.Must(common.RegisterConfig((*proxyman.InboundConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return New(ctx, config.(*proxyman.InboundConfig))
}))
common.Must(common.RegisterConfig((*core.InboundHandlerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return NewHandler(ctx, config.(*core.InboundHandlerConfig))
}))
}

View file

@ -0,0 +1,483 @@
package inbound
import (
"context"
"sync"
"sync/atomic"
"time"
"github.com/xtls/xray-core/v1/app/proxyman"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/buf"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/serial"
"github.com/xtls/xray-core/v1/common/session"
"github.com/xtls/xray-core/v1/common/signal/done"
"github.com/xtls/xray-core/v1/common/task"
"github.com/xtls/xray-core/v1/features/routing"
"github.com/xtls/xray-core/v1/features/stats"
"github.com/xtls/xray-core/v1/proxy"
"github.com/xtls/xray-core/v1/transport/internet"
"github.com/xtls/xray-core/v1/transport/internet/tcp"
"github.com/xtls/xray-core/v1/transport/internet/udp"
"github.com/xtls/xray-core/v1/transport/pipe"
)
type worker interface {
Start() error
Close() error
Port() net.Port
Proxy() proxy.Inbound
}
type tcpWorker struct {
address net.Address
port net.Port
proxy proxy.Inbound
stream *internet.MemoryStreamConfig
recvOrigDest bool
tag string
dispatcher routing.Dispatcher
sniffingConfig *proxyman.SniffingConfig
uplinkCounter stats.Counter
downlinkCounter stats.Counter
hub internet.Listener
ctx context.Context
}
func getTProxyType(s *internet.MemoryStreamConfig) internet.SocketConfig_TProxyMode {
if s == nil || s.SocketSettings == nil {
return internet.SocketConfig_Off
}
return s.SocketSettings.Tproxy
}
func (w *tcpWorker) callback(conn internet.Connection) {
ctx, cancel := context.WithCancel(w.ctx)
sid := session.NewID()
ctx = session.ContextWithID(ctx, sid)
if w.recvOrigDest {
var dest net.Destination
switch getTProxyType(w.stream) {
case internet.SocketConfig_Redirect:
d, err := tcp.GetOriginalDestination(conn)
if err != nil {
newError("failed to get original destination").Base(err).WriteToLog(session.ExportIDToError(ctx))
} else {
dest = d
}
case internet.SocketConfig_TProxy:
dest = net.DestinationFromAddr(conn.LocalAddr())
}
if dest.IsValid() {
ctx = session.ContextWithOutbound(ctx, &session.Outbound{
Target: dest,
})
}
}
ctx = session.ContextWithInbound(ctx, &session.Inbound{
Source: net.DestinationFromAddr(conn.RemoteAddr()),
Gateway: net.TCPDestination(w.address, w.port),
Tag: w.tag,
})
content := new(session.Content)
if w.sniffingConfig != nil {
content.SniffingRequest.Enabled = w.sniffingConfig.Enabled
content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride
}
ctx = session.ContextWithContent(ctx, content)
if w.uplinkCounter != nil || w.downlinkCounter != nil {
conn = &internet.StatCouterConnection{
Connection: conn,
ReadCounter: w.uplinkCounter,
WriteCounter: w.downlinkCounter,
}
}
if err := w.proxy.Process(ctx, net.Network_TCP, conn, w.dispatcher); err != nil {
newError("connection ends").Base(err).WriteToLog(session.ExportIDToError(ctx))
}
cancel()
if err := conn.Close(); err != nil {
newError("failed to close connection").Base(err).WriteToLog(session.ExportIDToError(ctx))
}
}
func (w *tcpWorker) Proxy() proxy.Inbound {
return w.proxy
}
func (w *tcpWorker) Start() error {
ctx := context.Background()
hub, err := internet.ListenTCP(ctx, w.address, w.port, w.stream, func(conn internet.Connection) {
go w.callback(conn)
})
if err != nil {
return newError("failed to listen TCP on ", w.port).AtWarning().Base(err)
}
w.hub = hub
return nil
}
func (w *tcpWorker) Close() error {
var errors []interface{}
if w.hub != nil {
if err := common.Close(w.hub); err != nil {
errors = append(errors, err)
}
if err := common.Close(w.proxy); err != nil {
errors = append(errors, err)
}
}
if len(errors) > 0 {
return newError("failed to close all resources").Base(newError(serial.Concat(errors...)))
}
return nil
}
func (w *tcpWorker) Port() net.Port {
return w.port
}
type udpConn struct {
lastActivityTime int64 // in seconds
reader buf.Reader
writer buf.Writer
output func([]byte) (int, error)
remote net.Addr
local net.Addr
done *done.Instance
uplink stats.Counter
downlink stats.Counter
}
func (c *udpConn) updateActivity() {
atomic.StoreInt64(&c.lastActivityTime, time.Now().Unix())
}
// ReadMultiBuffer implements buf.Reader
func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) {
mb, err := c.reader.ReadMultiBuffer()
if err != nil {
return nil, err
}
c.updateActivity()
if c.uplink != nil {
c.uplink.Add(int64(mb.Len()))
}
return mb, nil
}
func (c *udpConn) Read(buf []byte) (int, error) {
panic("not implemented")
}
// Write implements io.Writer.
func (c *udpConn) Write(buf []byte) (int, error) {
n, err := c.output(buf)
if c.downlink != nil {
c.downlink.Add(int64(n))
}
if err == nil {
c.updateActivity()
}
return n, err
}
func (c *udpConn) Close() error {
common.Must(c.done.Close())
common.Must(common.Close(c.writer))
return nil
}
func (c *udpConn) RemoteAddr() net.Addr {
return c.remote
}
func (c *udpConn) LocalAddr() net.Addr {
return c.local
}
func (*udpConn) SetDeadline(time.Time) error {
return nil
}
func (*udpConn) SetReadDeadline(time.Time) error {
return nil
}
func (*udpConn) SetWriteDeadline(time.Time) error {
return nil
}
type connID struct {
src net.Destination
dest net.Destination
}
type udpWorker struct {
sync.RWMutex
proxy proxy.Inbound
hub *udp.Hub
address net.Address
port net.Port
tag string
stream *internet.MemoryStreamConfig
dispatcher routing.Dispatcher
uplinkCounter stats.Counter
downlinkCounter stats.Counter
checker *task.Periodic
activeConn map[connID]*udpConn
}
func (w *udpWorker) getConnection(id connID) (*udpConn, bool) {
w.Lock()
defer w.Unlock()
if conn, found := w.activeConn[id]; found && !conn.done.Done() {
return conn, true
}
pReader, pWriter := pipe.New(pipe.DiscardOverflow(), pipe.WithSizeLimit(16*1024))
conn := &udpConn{
reader: pReader,
writer: pWriter,
output: func(b []byte) (int, error) {
return w.hub.WriteTo(b, id.src)
},
remote: &net.UDPAddr{
IP: id.src.Address.IP(),
Port: int(id.src.Port),
},
local: &net.UDPAddr{
IP: w.address.IP(),
Port: int(w.port),
},
done: done.New(),
uplink: w.uplinkCounter,
downlink: w.downlinkCounter,
}
w.activeConn[id] = conn
conn.updateActivity()
return conn, false
}
func (w *udpWorker) callback(b *buf.Buffer, source net.Destination, originalDest net.Destination) {
id := connID{
src: source,
}
if originalDest.IsValid() {
id.dest = originalDest
}
conn, existing := w.getConnection(id)
// payload will be discarded in pipe is full.
conn.writer.WriteMultiBuffer(buf.MultiBuffer{b})
if !existing {
common.Must(w.checker.Start())
go func() {
ctx := context.Background()
sid := session.NewID()
ctx = session.ContextWithID(ctx, sid)
if originalDest.IsValid() {
ctx = session.ContextWithOutbound(ctx, &session.Outbound{
Target: originalDest,
})
}
ctx = session.ContextWithInbound(ctx, &session.Inbound{
Source: source,
Gateway: net.UDPDestination(w.address, w.port),
Tag: w.tag,
})
if err := w.proxy.Process(ctx, net.Network_UDP, conn, w.dispatcher); err != nil {
newError("connection ends").Base(err).WriteToLog(session.ExportIDToError(ctx))
}
conn.Close()
w.removeConn(id)
}()
}
}
func (w *udpWorker) removeConn(id connID) {
w.Lock()
delete(w.activeConn, id)
w.Unlock()
}
func (w *udpWorker) handlePackets() {
receive := w.hub.Receive()
for payload := range receive {
w.callback(payload.Payload, payload.Source, payload.Target)
}
}
func (w *udpWorker) clean() error {
nowSec := time.Now().Unix()
w.Lock()
defer w.Unlock()
if len(w.activeConn) == 0 {
return newError("no more connections. stopping...")
}
for addr, conn := range w.activeConn {
if nowSec-atomic.LoadInt64(&conn.lastActivityTime) > 8 { // TODO Timeout too small
delete(w.activeConn, addr)
conn.Close()
}
}
if len(w.activeConn) == 0 {
w.activeConn = make(map[connID]*udpConn, 16)
}
return nil
}
func (w *udpWorker) Start() error {
w.activeConn = make(map[connID]*udpConn, 16)
ctx := context.Background()
h, err := udp.ListenUDP(ctx, w.address, w.port, w.stream, udp.HubCapacity(256))
if err != nil {
return err
}
w.checker = &task.Periodic{
Interval: time.Second * 16,
Execute: w.clean,
}
w.hub = h
go w.handlePackets()
return nil
}
func (w *udpWorker) Close() error {
w.Lock()
defer w.Unlock()
var errors []interface{}
if w.hub != nil {
if err := w.hub.Close(); err != nil {
errors = append(errors, err)
}
}
if w.checker != nil {
if err := w.checker.Close(); err != nil {
errors = append(errors, err)
}
}
if err := common.Close(w.proxy); err != nil {
errors = append(errors, err)
}
if len(errors) > 0 {
return newError("failed to close all resources").Base(newError(serial.Concat(errors...)))
}
return nil
}
func (w *udpWorker) Port() net.Port {
return w.port
}
func (w *udpWorker) Proxy() proxy.Inbound {
return w.proxy
}
type dsWorker struct {
address net.Address
proxy proxy.Inbound
stream *internet.MemoryStreamConfig
tag string
dispatcher routing.Dispatcher
sniffingConfig *proxyman.SniffingConfig
uplinkCounter stats.Counter
downlinkCounter stats.Counter
hub internet.Listener
ctx context.Context
}
func (w *dsWorker) callback(conn internet.Connection) {
ctx, cancel := context.WithCancel(w.ctx)
sid := session.NewID()
ctx = session.ContextWithID(ctx, sid)
ctx = session.ContextWithInbound(ctx, &session.Inbound{
Source: net.DestinationFromAddr(conn.RemoteAddr()),
Gateway: net.UnixDestination(w.address),
Tag: w.tag,
})
content := new(session.Content)
if w.sniffingConfig != nil {
content.SniffingRequest.Enabled = w.sniffingConfig.Enabled
content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride
}
ctx = session.ContextWithContent(ctx, content)
if w.uplinkCounter != nil || w.downlinkCounter != nil {
conn = &internet.StatCouterConnection{
Connection: conn,
ReadCounter: w.uplinkCounter,
WriteCounter: w.downlinkCounter,
}
}
if err := w.proxy.Process(ctx, net.Network_UNIX, conn, w.dispatcher); err != nil {
newError("connection ends").Base(err).WriteToLog(session.ExportIDToError(ctx))
}
cancel()
if err := conn.Close(); err != nil {
newError("failed to close connection").Base(err).WriteToLog(session.ExportIDToError(ctx))
}
}
func (w *dsWorker) Proxy() proxy.Inbound {
return w.proxy
}
func (w *dsWorker) Port() net.Port {
return net.Port(0)
}
func (w *dsWorker) Start() error {
ctx := context.Background()
hub, err := internet.ListenUnix(ctx, w.address, w.stream, func(conn internet.Connection) {
go w.callback(conn)
})
if err != nil {
return newError("failed to listen Unix Domain Socket on ", w.address).AtWarning().Base(err)
}
w.hub = hub
return nil
}
func (w *dsWorker) Close() error {
var errors []interface{}
if w.hub != nil {
if err := common.Close(w.hub); err != nil {
errors = append(errors, err)
}
if err := common.Close(w.proxy); err != nil {
errors = append(errors, err)
}
}
if len(errors) > 0 {
return newError("failed to close all resources").Base(newError(serial.Concat(errors...)))
}
return nil
}

View file

@ -0,0 +1,9 @@
package outbound
import "github.com/xtls/xray-core/v1/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

View file

@ -0,0 +1,228 @@
package outbound
import (
"context"
"github.com/xtls/xray-core/v1/app/proxyman"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/mux"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/session"
"github.com/xtls/xray-core/v1/core"
"github.com/xtls/xray-core/v1/features/outbound"
"github.com/xtls/xray-core/v1/features/policy"
"github.com/xtls/xray-core/v1/features/stats"
"github.com/xtls/xray-core/v1/proxy"
"github.com/xtls/xray-core/v1/transport"
"github.com/xtls/xray-core/v1/transport/internet"
"github.com/xtls/xray-core/v1/transport/internet/tls"
"github.com/xtls/xray-core/v1/transport/pipe"
)
func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter) {
var uplinkCounter stats.Counter
var downlinkCounter stats.Counter
policy := v.GetFeature(policy.ManagerType()).(policy.Manager)
if len(tag) > 0 && policy.ForSystem().Stats.OutboundUplink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "outbound>>>" + tag + ">>>traffic>>>uplink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
uplinkCounter = c
}
}
if len(tag) > 0 && policy.ForSystem().Stats.OutboundDownlink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "outbound>>>" + tag + ">>>traffic>>>downlink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
downlinkCounter = c
}
}
return uplinkCounter, downlinkCounter
}
// Handler is an implements of outbound.Handler.
type Handler struct {
tag string
senderSettings *proxyman.SenderConfig
streamSettings *internet.MemoryStreamConfig
proxy proxy.Outbound
outboundManager outbound.Manager
mux *mux.ClientManager
uplinkCounter stats.Counter
downlinkCounter stats.Counter
}
// NewHandler create a new Handler based on the given configuration.
func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbound.Handler, error) {
v := core.MustFromContext(ctx)
uplinkCounter, downlinkCounter := getStatCounter(v, config.Tag)
h := &Handler{
tag: config.Tag,
outboundManager: v.GetFeature(outbound.ManagerType()).(outbound.Manager),
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
}
if config.SenderSettings != nil {
senderSettings, err := config.SenderSettings.GetInstance()
if err != nil {
return nil, err
}
switch s := senderSettings.(type) {
case *proxyman.SenderConfig:
h.senderSettings = s
mss, err := internet.ToMemoryStreamConfig(s.StreamSettings)
if err != nil {
return nil, newError("failed to parse stream settings").Base(err).AtWarning()
}
h.streamSettings = mss
default:
return nil, newError("settings is not SenderConfig")
}
}
proxyConfig, err := config.ProxySettings.GetInstance()
if err != nil {
return nil, err
}
rawProxyHandler, err := common.CreateObject(ctx, proxyConfig)
if err != nil {
return nil, err
}
proxyHandler, ok := rawProxyHandler.(proxy.Outbound)
if !ok {
return nil, newError("not an outbound handler")
}
if h.senderSettings != nil && h.senderSettings.MultiplexSettings != nil {
config := h.senderSettings.MultiplexSettings
if config.Concurrency < 1 || config.Concurrency > 1024 {
return nil, newError("invalid mux concurrency: ", config.Concurrency).AtWarning()
}
h.mux = &mux.ClientManager{
Enabled: h.senderSettings.MultiplexSettings.Enabled,
Picker: &mux.IncrementalWorkerPicker{
Factory: &mux.DialingWorkerFactory{
Proxy: proxyHandler,
Dialer: h,
Strategy: mux.ClientStrategy{
MaxConcurrency: config.Concurrency,
MaxConnection: 128,
},
},
},
}
}
h.proxy = proxyHandler
return h, nil
}
// Tag implements outbound.Handler.
func (h *Handler) Tag() string {
return h.tag
}
// Dispatch implements proxy.Outbound.Dispatch.
func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) {
if h.mux != nil && (h.mux.Enabled || session.MuxPreferedFromContext(ctx)) {
if err := h.mux.Dispatch(ctx, link); err != nil {
newError("failed to process mux outbound traffic").Base(err).WriteToLog(session.ExportIDToError(ctx))
common.Interrupt(link.Writer)
}
} else {
if err := h.proxy.Process(ctx, link, h); err != nil {
// Ensure outbound ray is properly closed.
newError("failed to process outbound traffic").Base(err).WriteToLog(session.ExportIDToError(ctx))
common.Interrupt(link.Writer)
} else {
common.Must(common.Close(link.Writer))
}
common.Interrupt(link.Reader)
}
}
// Address implements internet.Dialer.
func (h *Handler) Address() net.Address {
if h.senderSettings == nil || h.senderSettings.Via == nil {
return nil
}
return h.senderSettings.Via.AsAddress()
}
// Dial implements internet.Dialer.
func (h *Handler) Dial(ctx context.Context, dest net.Destination) (internet.Connection, error) {
if h.senderSettings != nil {
if h.senderSettings.ProxySettings.HasTag() {
tag := h.senderSettings.ProxySettings.Tag
handler := h.outboundManager.GetHandler(tag)
if handler != nil {
newError("proxying to ", tag, " for dest ", dest).AtDebug().WriteToLog(session.ExportIDToError(ctx))
ctx = session.ContextWithOutbound(ctx, &session.Outbound{
Target: dest,
})
opts := pipe.OptionsFromContext(ctx)
uplinkReader, uplinkWriter := pipe.New(opts...)
downlinkReader, downlinkWriter := pipe.New(opts...)
go handler.Dispatch(ctx, &transport.Link{Reader: uplinkReader, Writer: downlinkWriter})
conn := net.NewConnection(net.ConnectionInputMulti(uplinkWriter), net.ConnectionOutputMulti(downlinkReader))
if config := tls.ConfigFromStreamSettings(h.streamSettings); config != nil {
tlsConfig := config.GetTLSConfig(tls.WithDestination(dest))
conn = tls.Client(conn, tlsConfig)
}
return h.getStatCouterConnection(conn), nil
}
newError("failed to get outbound handler with tag: ", tag).AtWarning().WriteToLog(session.ExportIDToError(ctx))
}
if h.senderSettings.Via != nil {
outbound := session.OutboundFromContext(ctx)
if outbound == nil {
outbound = new(session.Outbound)
ctx = session.ContextWithOutbound(ctx, outbound)
}
outbound.Gateway = h.senderSettings.Via.AsAddress()
}
}
conn, err := internet.Dial(ctx, dest, h.streamSettings)
return h.getStatCouterConnection(conn), err
}
func (h *Handler) getStatCouterConnection(conn internet.Connection) internet.Connection {
if h.uplinkCounter != nil || h.downlinkCounter != nil {
return &internet.StatCouterConnection{
Connection: conn,
ReadCounter: h.downlinkCounter,
WriteCounter: h.uplinkCounter,
}
}
return conn
}
// GetOutbound implements proxy.GetOutbound.
func (h *Handler) GetOutbound() proxy.Outbound {
return h.proxy
}
// Start implements common.Runnable.
func (h *Handler) Start() error {
return nil
}
// Close implements common.Closable.
func (h *Handler) Close() error {
common.Close(h.mux)
return nil
}

View file

@ -0,0 +1,80 @@
package outbound_test
import (
"context"
"testing"
"github.com/xtls/xray-core/v1/app/policy"
. "github.com/xtls/xray-core/v1/app/proxyman/outbound"
"github.com/xtls/xray-core/v1/app/stats"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/serial"
core "github.com/xtls/xray-core/v1/core"
"github.com/xtls/xray-core/v1/features/outbound"
"github.com/xtls/xray-core/v1/proxy/freedom"
"github.com/xtls/xray-core/v1/transport/internet"
)
func TestInterfaces(t *testing.T) {
_ = (outbound.Handler)(new(Handler))
_ = (outbound.Manager)(new(Manager))
}
const xrayKey core.XrayKey = 1
func TestOutboundWithoutStatCounter(t *testing.T) {
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&stats.Config{}),
serial.ToTypedMessage(&policy.Config{
System: &policy.SystemPolicy{
Stats: &policy.SystemPolicy_Stats{
InboundUplink: true,
},
},
}),
},
}
v, _ := core.New(config)
v.AddFeature((outbound.Manager)(new(Manager)))
ctx := context.WithValue(context.Background(), xrayKey, v)
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
Tag: "tag",
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
})
conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146))
_, ok := conn.(*internet.StatCouterConnection)
if ok {
t.Errorf("Expected conn to not be StatCouterConnection")
}
}
func TestOutboundWithStatCounter(t *testing.T) {
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&stats.Config{}),
serial.ToTypedMessage(&policy.Config{
System: &policy.SystemPolicy{
Stats: &policy.SystemPolicy_Stats{
OutboundUplink: true,
OutboundDownlink: true,
},
},
}),
},
}
v, _ := core.New(config)
v.AddFeature((outbound.Manager)(new(Manager)))
ctx := context.WithValue(context.Background(), xrayKey, v)
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
Tag: "tag",
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
})
conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146))
_, ok := conn.(*internet.StatCouterConnection)
if !ok {
t.Errorf("Expected conn to be StatCouterConnection")
}
}

View file

@ -0,0 +1,170 @@
package outbound
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
import (
"context"
"strings"
"sync"
"github.com/xtls/xray-core/v1/app/proxyman"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/errors"
"github.com/xtls/xray-core/v1/core"
"github.com/xtls/xray-core/v1/features/outbound"
)
// Manager is to manage all outbound handlers.
type Manager struct {
access sync.RWMutex
defaultHandler outbound.Handler
taggedHandler map[string]outbound.Handler
untaggedHandlers []outbound.Handler
running bool
}
// New creates a new Manager.
func New(ctx context.Context, config *proxyman.OutboundConfig) (*Manager, error) {
m := &Manager{
taggedHandler: make(map[string]outbound.Handler),
}
return m, nil
}
// Type implements common.HasType.
func (m *Manager) Type() interface{} {
return outbound.ManagerType()
}
// Start implements core.Feature
func (m *Manager) Start() error {
m.access.Lock()
defer m.access.Unlock()
m.running = true
for _, h := range m.taggedHandler {
if err := h.Start(); err != nil {
return err
}
}
for _, h := range m.untaggedHandlers {
if err := h.Start(); err != nil {
return err
}
}
return nil
}
// Close implements core.Feature
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
m.running = false
var errs []error
for _, h := range m.taggedHandler {
errs = append(errs, h.Close())
}
for _, h := range m.untaggedHandlers {
errs = append(errs, h.Close())
}
return errors.Combine(errs...)
}
// GetDefaultHandler implements outbound.Manager.
func (m *Manager) GetDefaultHandler() outbound.Handler {
m.access.RLock()
defer m.access.RUnlock()
if m.defaultHandler == nil {
return nil
}
return m.defaultHandler
}
// GetHandler implements outbound.Manager.
func (m *Manager) GetHandler(tag string) outbound.Handler {
m.access.RLock()
defer m.access.RUnlock()
if handler, found := m.taggedHandler[tag]; found {
return handler
}
return nil
}
// AddHandler implements outbound.Manager.
func (m *Manager) AddHandler(ctx context.Context, handler outbound.Handler) error {
m.access.Lock()
defer m.access.Unlock()
if m.defaultHandler == nil {
m.defaultHandler = handler
}
tag := handler.Tag()
if len(tag) > 0 {
m.taggedHandler[tag] = handler
} else {
m.untaggedHandlers = append(m.untaggedHandlers, handler)
}
if m.running {
return handler.Start()
}
return nil
}
// RemoveHandler implements outbound.Manager.
func (m *Manager) RemoveHandler(ctx context.Context, tag string) error {
if tag == "" {
return common.ErrNoClue
}
m.access.Lock()
defer m.access.Unlock()
delete(m.taggedHandler, tag)
if m.defaultHandler != nil && m.defaultHandler.Tag() == tag {
m.defaultHandler = nil
}
return nil
}
// Select implements outbound.HandlerSelector.
func (m *Manager) Select(selectors []string) []string {
m.access.RLock()
defer m.access.RUnlock()
tags := make([]string, 0, len(selectors))
for tag := range m.taggedHandler {
match := false
for _, selector := range selectors {
if strings.HasPrefix(tag, selector) {
match = true
break
}
}
if match {
tags = append(tags, tag)
}
}
return tags
}
func init() {
common.Must(common.RegisterConfig((*proxyman.OutboundConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return New(ctx, config.(*proxyman.OutboundConfig))
}))
common.Must(common.RegisterConfig((*core.OutboundHandlerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return NewHandler(ctx, config.(*core.OutboundHandlerConfig))
}))
}

194
app/reverse/bridge.go Normal file
View file

@ -0,0 +1,194 @@
// +build !confonly
package reverse
import (
"context"
"time"
"github.com/golang/protobuf/proto"
"github.com/xtls/xray-core/v1/common/mux"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/session"
"github.com/xtls/xray-core/v1/common/task"
"github.com/xtls/xray-core/v1/features/routing"
"github.com/xtls/xray-core/v1/transport"
"github.com/xtls/xray-core/v1/transport/pipe"
)
// Bridge is a component in reverse proxy, that relays connections from Portal to local address.
type Bridge struct {
dispatcher routing.Dispatcher
tag string
domain string
workers []*BridgeWorker
monitorTask *task.Periodic
}
// NewBridge creates a new Bridge instance.
func NewBridge(config *BridgeConfig, dispatcher routing.Dispatcher) (*Bridge, error) {
if config.Tag == "" {
return nil, newError("bridge tag is empty")
}
if config.Domain == "" {
return nil, newError("bridge domain is empty")
}
b := &Bridge{
dispatcher: dispatcher,
tag: config.Tag,
domain: config.Domain,
}
b.monitorTask = &task.Periodic{
Execute: b.monitor,
Interval: time.Second * 2,
}
return b, nil
}
func (b *Bridge) cleanup() {
var activeWorkers []*BridgeWorker
for _, w := range b.workers {
if w.IsActive() {
activeWorkers = append(activeWorkers, w)
}
}
if len(activeWorkers) != len(b.workers) {
b.workers = activeWorkers
}
}
func (b *Bridge) monitor() error {
b.cleanup()
var numConnections uint32
var numWorker uint32
for _, w := range b.workers {
if w.IsActive() {
numConnections += w.Connections()
numWorker++
}
}
if numWorker == 0 || numConnections/numWorker > 16 {
worker, err := NewBridgeWorker(b.domain, b.tag, b.dispatcher)
if err != nil {
newError("failed to create bridge worker").Base(err).AtWarning().WriteToLog()
return nil
}
b.workers = append(b.workers, worker)
}
return nil
}
func (b *Bridge) Start() error {
return b.monitorTask.Start()
}
func (b *Bridge) Close() error {
return b.monitorTask.Close()
}
type BridgeWorker struct {
tag string
worker *mux.ServerWorker
dispatcher routing.Dispatcher
state Control_State
}
func NewBridgeWorker(domain string, tag string, d routing.Dispatcher) (*BridgeWorker, error) {
ctx := context.Background()
ctx = session.ContextWithInbound(ctx, &session.Inbound{
Tag: tag,
})
link, err := d.Dispatch(ctx, net.Destination{
Network: net.Network_TCP,
Address: net.DomainAddress(domain),
Port: 0,
})
if err != nil {
return nil, err
}
w := &BridgeWorker{
dispatcher: d,
tag: tag,
}
worker, err := mux.NewServerWorker(context.Background(), w, link)
if err != nil {
return nil, err
}
w.worker = worker
return w, nil
}
func (w *BridgeWorker) Type() interface{} {
return routing.DispatcherType()
}
func (w *BridgeWorker) Start() error {
return nil
}
func (w *BridgeWorker) Close() error {
return nil
}
func (w *BridgeWorker) IsActive() bool {
return w.state == Control_ACTIVE && !w.worker.Closed()
}
func (w *BridgeWorker) Connections() uint32 {
return w.worker.ActiveConnections()
}
func (w *BridgeWorker) handleInternalConn(link transport.Link) {
go func() {
reader := link.Reader
for {
mb, err := reader.ReadMultiBuffer()
if err != nil {
break
}
for _, b := range mb {
var ctl Control
if err := proto.Unmarshal(b.Bytes(), &ctl); err != nil {
newError("failed to parse proto message").Base(err).WriteToLog()
break
}
if ctl.State != w.state {
w.state = ctl.State
}
}
}
}()
}
func (w *BridgeWorker) Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) {
if !isInternalDomain(dest) {
ctx = session.ContextWithInbound(ctx, &session.Inbound{
Tag: w.tag,
})
return w.dispatcher.Dispatch(ctx, dest)
}
opt := []pipe.Option{pipe.WithSizeLimit(16 * 1024)}
uplinkReader, uplinkWriter := pipe.New(opt...)
downlinkReader, downlinkWriter := pipe.New(opt...)
w.handleInternalConn(transport.Link{
Reader: downlinkReader,
Writer: uplinkWriter,
})
return &transport.Link{
Reader: uplinkReader,
Writer: downlinkWriter,
}, nil
}

16
app/reverse/config.go Normal file
View file

@ -0,0 +1,16 @@
// +build !confonly
package reverse
import (
"crypto/rand"
"io"
"github.com/xtls/xray-core/v1/common/dice"
)
func (c *Control) FillInRandom() {
randomLength := dice.Roll(64)
c.Random = make([]byte, randomLength)
io.ReadFull(rand.Reader, c.Random)
}

439
app/reverse/config.pb.go Normal file
View file

@ -0,0 +1,439 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.14.0
// source: app/reverse/config.proto
package reverse
import (
proto "github.com/golang/protobuf/proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
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)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type Control_State int32
const (
Control_ACTIVE Control_State = 0
Control_DRAIN Control_State = 1
)
// Enum value maps for Control_State.
var (
Control_State_name = map[int32]string{
0: "ACTIVE",
1: "DRAIN",
}
Control_State_value = map[string]int32{
"ACTIVE": 0,
"DRAIN": 1,
}
)
func (x Control_State) Enum() *Control_State {
p := new(Control_State)
*p = x
return p
}
func (x Control_State) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Control_State) Descriptor() protoreflect.EnumDescriptor {
return file_app_reverse_config_proto_enumTypes[0].Descriptor()
}
func (Control_State) Type() protoreflect.EnumType {
return &file_app_reverse_config_proto_enumTypes[0]
}
func (x Control_State) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Control_State.Descriptor instead.
func (Control_State) EnumDescriptor() ([]byte, []int) {
return file_app_reverse_config_proto_rawDescGZIP(), []int{0, 0}
}
type Control struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
State Control_State `protobuf:"varint,1,opt,name=state,proto3,enum=xray.app.reverse.Control_State" json:"state,omitempty"`
Random []byte `protobuf:"bytes,99,opt,name=random,proto3" json:"random,omitempty"`
}
func (x *Control) Reset() {
*x = Control{}
if protoimpl.UnsafeEnabled {
mi := &file_app_reverse_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Control) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Control) ProtoMessage() {}
func (x *Control) ProtoReflect() protoreflect.Message {
mi := &file_app_reverse_config_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Control.ProtoReflect.Descriptor instead.
func (*Control) Descriptor() ([]byte, []int) {
return file_app_reverse_config_proto_rawDescGZIP(), []int{0}
}
func (x *Control) GetState() Control_State {
if x != nil {
return x.State
}
return Control_ACTIVE
}
func (x *Control) GetRandom() []byte {
if x != nil {
return x.Random
}
return nil
}
type BridgeConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
}
func (x *BridgeConfig) Reset() {
*x = BridgeConfig{}
if protoimpl.UnsafeEnabled {
mi := &file_app_reverse_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *BridgeConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BridgeConfig) ProtoMessage() {}
func (x *BridgeConfig) ProtoReflect() protoreflect.Message {
mi := &file_app_reverse_config_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BridgeConfig.ProtoReflect.Descriptor instead.
func (*BridgeConfig) Descriptor() ([]byte, []int) {
return file_app_reverse_config_proto_rawDescGZIP(), []int{1}
}
func (x *BridgeConfig) GetTag() string {
if x != nil {
return x.Tag
}
return ""
}
func (x *BridgeConfig) GetDomain() string {
if x != nil {
return x.Domain
}
return ""
}
type PortalConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
}
func (x *PortalConfig) Reset() {
*x = PortalConfig{}
if protoimpl.UnsafeEnabled {
mi := &file_app_reverse_config_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *PortalConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PortalConfig) ProtoMessage() {}
func (x *PortalConfig) ProtoReflect() protoreflect.Message {
mi := &file_app_reverse_config_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PortalConfig.ProtoReflect.Descriptor instead.
func (*PortalConfig) Descriptor() ([]byte, []int) {
return file_app_reverse_config_proto_rawDescGZIP(), []int{2}
}
func (x *PortalConfig) GetTag() string {
if x != nil {
return x.Tag
}
return ""
}
func (x *PortalConfig) GetDomain() string {
if x != nil {
return x.Domain
}
return ""
}
type Config struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
BridgeConfig []*BridgeConfig `protobuf:"bytes,1,rep,name=bridge_config,json=bridgeConfig,proto3" json:"bridge_config,omitempty"`
PortalConfig []*PortalConfig `protobuf:"bytes,2,rep,name=portal_config,json=portalConfig,proto3" json:"portal_config,omitempty"`
}
func (x *Config) Reset() {
*x = Config{}
if protoimpl.UnsafeEnabled {
mi := &file_app_reverse_config_proto_msgTypes[3]
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_reverse_config_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && 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_reverse_config_proto_rawDescGZIP(), []int{3}
}
func (x *Config) GetBridgeConfig() []*BridgeConfig {
if x != nil {
return x.BridgeConfig
}
return nil
}
func (x *Config) GetPortalConfig() []*PortalConfig {
if x != nil {
return x.PortalConfig
}
return nil
}
var File_app_reverse_config_proto protoreflect.FileDescriptor
var file_app_reverse_config_proto_rawDesc = []byte{
0x0a, 0x18, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x2f, 0x63, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x78, 0x72, 0x61, 0x79,
0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x22, 0x78, 0x0a, 0x07,
0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x12, 0x35, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70,
0x70, 0x2e, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f,
0x6c, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x16,
0x0a, 0x06, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x18, 0x63, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06,
0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x22, 0x1e, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12,
0x0a, 0x0a, 0x06, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x44,
0x52, 0x41, 0x49, 0x4e, 0x10, 0x01, 0x22, 0x38, 0x0a, 0x0c, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65,
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61,
0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
0x22, 0x38, 0x0a, 0x0c, 0x50, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74,
0x61, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x92, 0x01, 0x0a, 0x06, 0x43,
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x43, 0x0a, 0x0d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x5f,
0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x78,
0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x2e,
0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x62, 0x72,
0x69, 0x64, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x43, 0x0a, 0x0d, 0x70, 0x6f,
0x72, 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x03, 0x28,
0x0b, 0x32, 0x1e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x65, 0x76,
0x65, 0x72, 0x73, 0x65, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x52, 0x0c, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42,
0x59, 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78,
0x79, 0x2e, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x50, 0x01, 0x5a, 0x28, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61,
0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x65,
0x76, 0x65, 0x72, 0x73, 0x65, 0xaa, 0x02, 0x12, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f,
0x78, 0x79, 0x2e, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
}
var (
file_app_reverse_config_proto_rawDescOnce sync.Once
file_app_reverse_config_proto_rawDescData = file_app_reverse_config_proto_rawDesc
)
func file_app_reverse_config_proto_rawDescGZIP() []byte {
file_app_reverse_config_proto_rawDescOnce.Do(func() {
file_app_reverse_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_reverse_config_proto_rawDescData)
})
return file_app_reverse_config_proto_rawDescData
}
var file_app_reverse_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_app_reverse_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_app_reverse_config_proto_goTypes = []interface{}{
(Control_State)(0), // 0: xray.app.reverse.Control.State
(*Control)(nil), // 1: xray.app.reverse.Control
(*BridgeConfig)(nil), // 2: xray.app.reverse.BridgeConfig
(*PortalConfig)(nil), // 3: xray.app.reverse.PortalConfig
(*Config)(nil), // 4: xray.app.reverse.Config
}
var file_app_reverse_config_proto_depIdxs = []int32{
0, // 0: xray.app.reverse.Control.state:type_name -> xray.app.reverse.Control.State
2, // 1: xray.app.reverse.Config.bridge_config:type_name -> xray.app.reverse.BridgeConfig
3, // 2: xray.app.reverse.Config.portal_config:type_name -> xray.app.reverse.PortalConfig
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_app_reverse_config_proto_init() }
func file_app_reverse_config_proto_init() {
if File_app_reverse_config_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_app_reverse_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Control); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_reverse_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*BridgeConfig); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_reverse_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*PortalConfig); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_reverse_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_reverse_config_proto_rawDesc,
NumEnums: 1,
NumMessages: 4,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_app_reverse_config_proto_goTypes,
DependencyIndexes: file_app_reverse_config_proto_depIdxs,
EnumInfos: file_app_reverse_config_proto_enumTypes,
MessageInfos: file_app_reverse_config_proto_msgTypes,
}.Build()
File_app_reverse_config_proto = out.File
file_app_reverse_config_proto_rawDesc = nil
file_app_reverse_config_proto_goTypes = nil
file_app_reverse_config_proto_depIdxs = nil
}

32
app/reverse/config.proto Normal file
View file

@ -0,0 +1,32 @@
syntax = "proto3";
package xray.app.reverse;
option csharp_namespace = "Xray.Proxy.Reverse";
option go_package = "github.com/xtls/xray-core/v1/app/reverse";
option java_package = "com.xray.proxy.reverse";
option java_multiple_files = true;
message Control {
enum State {
ACTIVE = 0;
DRAIN = 1;
}
State state = 1;
bytes random = 99;
}
message BridgeConfig {
string tag = 1;
string domain = 2;
}
message PortalConfig {
string tag = 1;
string domain = 2;
}
message Config {
repeated BridgeConfig bridge_config = 1;
repeated PortalConfig portal_config = 2;
}

View file

@ -0,0 +1,9 @@
package reverse
import "github.com/xtls/xray-core/v1/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

266
app/reverse/portal.go Normal file
View file

@ -0,0 +1,266 @@
// +build !confonly
package reverse
import (
"context"
"sync"
"time"
"github.com/golang/protobuf/proto"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/buf"
"github.com/xtls/xray-core/v1/common/mux"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/session"
"github.com/xtls/xray-core/v1/common/task"
"github.com/xtls/xray-core/v1/features/outbound"
"github.com/xtls/xray-core/v1/transport"
"github.com/xtls/xray-core/v1/transport/pipe"
)
type Portal struct {
ohm outbound.Manager
tag string
domain string
picker *StaticMuxPicker
client *mux.ClientManager
}
func NewPortal(config *PortalConfig, ohm outbound.Manager) (*Portal, error) {
if config.Tag == "" {
return nil, newError("portal tag is empty")
}
if config.Domain == "" {
return nil, newError("portal domain is empty")
}
picker, err := NewStaticMuxPicker()
if err != nil {
return nil, err
}
return &Portal{
ohm: ohm,
tag: config.Tag,
domain: config.Domain,
picker: picker,
client: &mux.ClientManager{
Picker: picker,
},
}, nil
}
func (p *Portal) Start() error {
return p.ohm.AddHandler(context.Background(), &Outbound{
portal: p,
tag: p.tag,
})
}
func (p *Portal) Close() error {
return p.ohm.RemoveHandler(context.Background(), p.tag)
}
func (p *Portal) HandleConnection(ctx context.Context, link *transport.Link) error {
outboundMeta := session.OutboundFromContext(ctx)
if outboundMeta == nil {
return newError("outbound metadata not found").AtError()
}
if isDomain(outboundMeta.Target, p.domain) {
muxClient, err := mux.NewClientWorker(*link, mux.ClientStrategy{})
if err != nil {
return newError("failed to create mux client worker").Base(err).AtWarning()
}
worker, err := NewPortalWorker(muxClient)
if err != nil {
return newError("failed to create portal worker").Base(err)
}
p.picker.AddWorker(worker)
return nil
}
return p.client.Dispatch(ctx, link)
}
type Outbound struct {
portal *Portal
tag string
}
func (o *Outbound) Tag() string {
return o.tag
}
func (o *Outbound) Dispatch(ctx context.Context, link *transport.Link) {
if err := o.portal.HandleConnection(ctx, link); err != nil {
newError("failed to process reverse connection").Base(err).WriteToLog(session.ExportIDToError(ctx))
common.Interrupt(link.Writer)
}
}
func (o *Outbound) Start() error {
return nil
}
func (o *Outbound) Close() error {
return nil
}
type StaticMuxPicker struct {
access sync.Mutex
workers []*PortalWorker
cTask *task.Periodic
}
func NewStaticMuxPicker() (*StaticMuxPicker, error) {
p := &StaticMuxPicker{}
p.cTask = &task.Periodic{
Execute: p.cleanup,
Interval: time.Second * 30,
}
p.cTask.Start()
return p, nil
}
func (p *StaticMuxPicker) cleanup() error {
p.access.Lock()
defer p.access.Unlock()
var activeWorkers []*PortalWorker
for _, w := range p.workers {
if !w.Closed() {
activeWorkers = append(activeWorkers, w)
}
}
if len(activeWorkers) != len(p.workers) {
p.workers = activeWorkers
}
return nil
}
func (p *StaticMuxPicker) PickAvailable() (*mux.ClientWorker, error) {
p.access.Lock()
defer p.access.Unlock()
if len(p.workers) == 0 {
return nil, newError("empty worker list")
}
var minIdx int = -1
var minConn uint32 = 9999
for i, w := range p.workers {
if w.draining {
continue
}
if w.client.ActiveConnections() < minConn {
minConn = w.client.ActiveConnections()
minIdx = i
}
}
if minIdx == -1 {
for i, w := range p.workers {
if w.IsFull() {
continue
}
if w.client.ActiveConnections() < minConn {
minConn = w.client.ActiveConnections()
minIdx = i
}
}
}
if minIdx != -1 {
return p.workers[minIdx].client, nil
}
return nil, newError("no mux client worker available")
}
func (p *StaticMuxPicker) AddWorker(worker *PortalWorker) {
p.access.Lock()
defer p.access.Unlock()
p.workers = append(p.workers, worker)
}
type PortalWorker struct {
client *mux.ClientWorker
control *task.Periodic
writer buf.Writer
reader buf.Reader
draining bool
}
func NewPortalWorker(client *mux.ClientWorker) (*PortalWorker, error) {
opt := []pipe.Option{pipe.WithSizeLimit(16 * 1024)}
uplinkReader, uplinkWriter := pipe.New(opt...)
downlinkReader, downlinkWriter := pipe.New(opt...)
ctx := context.Background()
ctx = session.ContextWithOutbound(ctx, &session.Outbound{
Target: net.UDPDestination(net.DomainAddress(internalDomain), 0),
})
f := client.Dispatch(ctx, &transport.Link{
Reader: uplinkReader,
Writer: downlinkWriter,
})
if !f {
return nil, newError("unable to dispatch control connection")
}
w := &PortalWorker{
client: client,
reader: downlinkReader,
writer: uplinkWriter,
}
w.control = &task.Periodic{
Execute: w.heartbeat,
Interval: time.Second * 2,
}
w.control.Start()
return w, nil
}
func (w *PortalWorker) heartbeat() error {
if w.client.Closed() {
return newError("client worker stopped")
}
if w.draining || w.writer == nil {
return newError("already disposed")
}
msg := &Control{}
msg.FillInRandom()
if w.client.TotalConnections() > 256 {
w.draining = true
msg.State = Control_DRAIN
defer func() {
common.Close(w.writer)
common.Interrupt(w.reader)
w.writer = nil
}()
}
b, err := proto.Marshal(msg)
common.Must(err)
mb := buf.MergeBytes(nil, b)
return w.writer.WriteMultiBuffer(mb)
}
func (w *PortalWorker) IsFull() bool {
return w.client.IsFull()
}
func (w *PortalWorker) Closed() bool {
return w.client.Closed()
}

View file

@ -0,0 +1,20 @@
package reverse_test
import (
"testing"
"github.com/xtls/xray-core/v1/app/reverse"
"github.com/xtls/xray-core/v1/common"
)
func TestStaticPickerEmpty(t *testing.T) {
picker, err := reverse.NewStaticMuxPicker()
common.Must(err)
worker, err := picker.PickAvailable()
if err == nil {
t.Error("expected error, but nil")
}
if worker != nil {
t.Error("expected nil worker, but not nil")
}
}

98
app/reverse/reverse.go Normal file
View file

@ -0,0 +1,98 @@
// +build !confonly
package reverse
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
import (
"context"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/errors"
"github.com/xtls/xray-core/v1/common/net"
core "github.com/xtls/xray-core/v1/core"
"github.com/xtls/xray-core/v1/features/outbound"
"github.com/xtls/xray-core/v1/features/routing"
)
const (
internalDomain = "reverse.internal.example.com"
)
func isDomain(dest net.Destination, domain string) bool {
return dest.Address.Family().IsDomain() && dest.Address.Domain() == domain
}
func isInternalDomain(dest net.Destination) bool {
return isDomain(dest, internalDomain)
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
r := new(Reverse)
if err := core.RequireFeatures(ctx, func(d routing.Dispatcher, om outbound.Manager) error {
return r.Init(config.(*Config), d, om)
}); err != nil {
return nil, err
}
return r, nil
}))
}
type Reverse struct {
bridges []*Bridge
portals []*Portal
}
func (r *Reverse) Init(config *Config, d routing.Dispatcher, ohm outbound.Manager) error {
for _, bConfig := range config.BridgeConfig {
b, err := NewBridge(bConfig, d)
if err != nil {
return err
}
r.bridges = append(r.bridges, b)
}
for _, pConfig := range config.PortalConfig {
p, err := NewPortal(pConfig, ohm)
if err != nil {
return err
}
r.portals = append(r.portals, p)
}
return nil
}
func (r *Reverse) Type() interface{} {
return (*Reverse)(nil)
}
func (r *Reverse) Start() error {
for _, b := range r.bridges {
if err := b.Start(); err != nil {
return err
}
}
for _, p := range r.portals {
if err := p.Start(); err != nil {
return err
}
}
return nil
}
func (r *Reverse) Close() error {
var errs []error
for _, b := range r.bridges {
errs = append(errs, b.Close())
}
for _, p := range r.portals {
errs = append(errs, p.Close())
}
return errors.Combine(errs...)
}

46
app/router/balancing.go Normal file
View file

@ -0,0 +1,46 @@
// +build !confonly
package router
import (
"github.com/xtls/xray-core/v1/common/dice"
"github.com/xtls/xray-core/v1/features/outbound"
)
type BalancingStrategy interface {
PickOutbound([]string) string
}
type RandomStrategy struct {
}
func (s *RandomStrategy) PickOutbound(tags []string) string {
n := len(tags)
if n == 0 {
panic("0 tags")
}
return tags[dice.Roll(n)]
}
type Balancer struct {
selectors []string
strategy BalancingStrategy
ohm outbound.Manager
}
func (b *Balancer) PickOutbound() (string, error) {
hs, ok := b.ohm.(outbound.HandlerSelector)
if !ok {
return "", newError("outbound.Manager is not a HandlerSelector")
}
tags := hs.Select(b.selectors)
if len(tags) == 0 {
return "", newError("no available outbounds selected")
}
tag := b.strategy.PickOutbound(tags)
if tag == "" {
return "", newError("balancing strategy returns empty tag")
}
return tag, nil
}

View file

@ -0,0 +1,95 @@
// +build !confonly
package command
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
import (
"context"
"time"
"google.golang.org/grpc"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/core"
"github.com/xtls/xray-core/v1/features/routing"
"github.com/xtls/xray-core/v1/features/stats"
)
// routingServer is an implementation of RoutingService.
type routingServer struct {
router routing.Router
routingStats stats.Channel
}
// NewRoutingServer creates a statistics service with statistics manager.
func NewRoutingServer(router routing.Router, routingStats stats.Channel) RoutingServiceServer {
return &routingServer{
router: router,
routingStats: routingStats,
}
}
func (s *routingServer) TestRoute(ctx context.Context, request *TestRouteRequest) (*RoutingContext, error) {
if request.RoutingContext == nil {
return nil, newError("Invalid routing request.")
}
route, err := s.router.PickRoute(AsRoutingContext(request.RoutingContext))
if err != nil {
return nil, err
}
if request.PublishResult && s.routingStats != nil {
ctx, _ := context.WithTimeout(context.Background(), 4*time.Second)
s.routingStats.Publish(ctx, route)
}
return AsProtobufMessage(request.FieldSelectors)(route), nil
}
func (s *routingServer) SubscribeRoutingStats(request *SubscribeRoutingStatsRequest, stream RoutingService_SubscribeRoutingStatsServer) error {
if s.routingStats == nil {
return newError("Routing statistics not enabled.")
}
genMessage := AsProtobufMessage(request.FieldSelectors)
subscriber, err := stats.SubscribeRunnableChannel(s.routingStats)
if err != nil {
return err
}
defer stats.UnsubscribeClosableChannel(s.routingStats, subscriber)
for {
select {
case value, ok := <-subscriber:
if !ok {
return newError("Upstream closed the subscriber channel.")
}
route, ok := value.(routing.Route)
if !ok {
return newError("Upstream sent malformed statistics.")
}
err := stream.Send(genMessage(route))
if err != nil {
return err
}
case <-stream.Context().Done():
return stream.Context().Err()
}
}
}
func (s *routingServer) mustEmbedUnimplementedRoutingServiceServer() {}
type service struct {
v *core.Instance
}
func (s *service) Register(server *grpc.Server) {
common.Must(s.v.RequireFeatures(func(router routing.Router, stats stats.Manager) {
RegisterRoutingServiceServer(server, NewRoutingServer(router, nil))
}))
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) {
s := core.MustFromContext(ctx)
return &service{v: s}, nil
}))
}

View file

@ -0,0 +1,532 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.14.0
// source: app/router/command/command.proto
package command
import (
proto "github.com/golang/protobuf/proto"
net "github.com/xtls/xray-core/v1/common/net"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
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)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
// RoutingContext is the context with information relative to routing process.
// It conforms to the structure of xray.features.routing.Context and
// xray.features.routing.Route.
type RoutingContext struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
InboundTag string `protobuf:"bytes,1,opt,name=InboundTag,proto3" json:"InboundTag,omitempty"`
Network net.Network `protobuf:"varint,2,opt,name=Network,proto3,enum=xray.common.net.Network" json:"Network,omitempty"`
SourceIPs [][]byte `protobuf:"bytes,3,rep,name=SourceIPs,proto3" json:"SourceIPs,omitempty"`
TargetIPs [][]byte `protobuf:"bytes,4,rep,name=TargetIPs,proto3" json:"TargetIPs,omitempty"`
SourcePort uint32 `protobuf:"varint,5,opt,name=SourcePort,proto3" json:"SourcePort,omitempty"`
TargetPort uint32 `protobuf:"varint,6,opt,name=TargetPort,proto3" json:"TargetPort,omitempty"`
TargetDomain string `protobuf:"bytes,7,opt,name=TargetDomain,proto3" json:"TargetDomain,omitempty"`
Protocol string `protobuf:"bytes,8,opt,name=Protocol,proto3" json:"Protocol,omitempty"`
User string `protobuf:"bytes,9,opt,name=User,proto3" json:"User,omitempty"`
Attributes map[string]string `protobuf:"bytes,10,rep,name=Attributes,proto3" json:"Attributes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
OutboundGroupTags []string `protobuf:"bytes,11,rep,name=OutboundGroupTags,proto3" json:"OutboundGroupTags,omitempty"`
OutboundTag string `protobuf:"bytes,12,opt,name=OutboundTag,proto3" json:"OutboundTag,omitempty"`
}
func (x *RoutingContext) Reset() {
*x = RoutingContext{}
if protoimpl.UnsafeEnabled {
mi := &file_app_router_command_command_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RoutingContext) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RoutingContext) ProtoMessage() {}
func (x *RoutingContext) ProtoReflect() protoreflect.Message {
mi := &file_app_router_command_command_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RoutingContext.ProtoReflect.Descriptor instead.
func (*RoutingContext) Descriptor() ([]byte, []int) {
return file_app_router_command_command_proto_rawDescGZIP(), []int{0}
}
func (x *RoutingContext) GetInboundTag() string {
if x != nil {
return x.InboundTag
}
return ""
}
func (x *RoutingContext) GetNetwork() net.Network {
if x != nil {
return x.Network
}
return net.Network_Unknown
}
func (x *RoutingContext) GetSourceIPs() [][]byte {
if x != nil {
return x.SourceIPs
}
return nil
}
func (x *RoutingContext) GetTargetIPs() [][]byte {
if x != nil {
return x.TargetIPs
}
return nil
}
func (x *RoutingContext) GetSourcePort() uint32 {
if x != nil {
return x.SourcePort
}
return 0
}
func (x *RoutingContext) GetTargetPort() uint32 {
if x != nil {
return x.TargetPort
}
return 0
}
func (x *RoutingContext) GetTargetDomain() string {
if x != nil {
return x.TargetDomain
}
return ""
}
func (x *RoutingContext) GetProtocol() string {
if x != nil {
return x.Protocol
}
return ""
}
func (x *RoutingContext) GetUser() string {
if x != nil {
return x.User
}
return ""
}
func (x *RoutingContext) GetAttributes() map[string]string {
if x != nil {
return x.Attributes
}
return nil
}
func (x *RoutingContext) GetOutboundGroupTags() []string {
if x != nil {
return x.OutboundGroupTags
}
return nil
}
func (x *RoutingContext) GetOutboundTag() string {
if x != nil {
return x.OutboundTag
}
return ""
}
// SubscribeRoutingStatsRequest subscribes to routing statistics channel if
// opened by xray-core.
// * FieldSelectors selects a subset of fields in routing statistics to return.
// Valid selectors:
// - inbound: Selects connection's inbound tag.
// - network: Selects connection's network.
// - ip: Equivalent as "ip_source" and "ip_target", selects both source and
// target IP.
// - port: Equivalent as "port_source" and "port_target", selects both source
// and target port.
// - domain: Selects target domain.
// - protocol: Select connection's protocol.
// - user: Select connection's inbound user email.
// - attributes: Select connection's additional attributes.
// - outbound: Equivalent as "outbound" and "outbound_group", select both
// outbound tag and outbound group tags.
// * If FieldSelectors is left empty, all fields will be returned.
type SubscribeRoutingStatsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
FieldSelectors []string `protobuf:"bytes,1,rep,name=FieldSelectors,proto3" json:"FieldSelectors,omitempty"`
}
func (x *SubscribeRoutingStatsRequest) Reset() {
*x = SubscribeRoutingStatsRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_app_router_command_command_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SubscribeRoutingStatsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SubscribeRoutingStatsRequest) ProtoMessage() {}
func (x *SubscribeRoutingStatsRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_router_command_command_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SubscribeRoutingStatsRequest.ProtoReflect.Descriptor instead.
func (*SubscribeRoutingStatsRequest) Descriptor() ([]byte, []int) {
return file_app_router_command_command_proto_rawDescGZIP(), []int{1}
}
func (x *SubscribeRoutingStatsRequest) GetFieldSelectors() []string {
if x != nil {
return x.FieldSelectors
}
return nil
}
// TestRouteRequest manually tests a routing result according to the routing
// context message.
// * RoutingContext is the routing message without outbound information.
// * FieldSelectors selects the fields to return in the routing result. All
// fields are returned if left empty.
// * PublishResult broadcasts the routing result to routing statistics channel
// if set true.
type TestRouteRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
RoutingContext *RoutingContext `protobuf:"bytes,1,opt,name=RoutingContext,proto3" json:"RoutingContext,omitempty"`
FieldSelectors []string `protobuf:"bytes,2,rep,name=FieldSelectors,proto3" json:"FieldSelectors,omitempty"`
PublishResult bool `protobuf:"varint,3,opt,name=PublishResult,proto3" json:"PublishResult,omitempty"`
}
func (x *TestRouteRequest) Reset() {
*x = TestRouteRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_app_router_command_command_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *TestRouteRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TestRouteRequest) ProtoMessage() {}
func (x *TestRouteRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_router_command_command_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TestRouteRequest.ProtoReflect.Descriptor instead.
func (*TestRouteRequest) Descriptor() ([]byte, []int) {
return file_app_router_command_command_proto_rawDescGZIP(), []int{2}
}
func (x *TestRouteRequest) GetRoutingContext() *RoutingContext {
if x != nil {
return x.RoutingContext
}
return nil
}
func (x *TestRouteRequest) GetFieldSelectors() []string {
if x != nil {
return x.FieldSelectors
}
return nil
}
func (x *TestRouteRequest) GetPublishResult() bool {
if x != nil {
return x.PublishResult
}
return false
}
type Config struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *Config) Reset() {
*x = Config{}
if protoimpl.UnsafeEnabled {
mi := &file_app_router_command_command_proto_msgTypes[3]
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_router_command_command_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && 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_router_command_command_proto_rawDescGZIP(), []int{3}
}
var File_app_router_command_command_proto protoreflect.FileDescriptor
var file_app_router_command_command_proto_rawDesc = []byte{
0x0a, 0x20, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6d,
0x6d, 0x61, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x12, 0x17, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75,
0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x1a, 0x18, 0x63, 0x6f, 0x6d,
0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74, 0x2f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9c, 0x04, 0x0a, 0x0e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e,
0x67, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x49, 0x6e, 0x62, 0x6f,
0x75, 0x6e, 0x64, 0x54, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x49, 0x6e,
0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x61, 0x67, 0x12, 0x32, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77,
0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x78, 0x72, 0x61, 0x79,
0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77,
0x6f, 0x72, 0x6b, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x1c, 0x0a, 0x09,
0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x50, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52,
0x09, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x50, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x54, 0x61,
0x72, 0x67, 0x65, 0x74, 0x49, 0x50, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x09, 0x54,
0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x50, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x53, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x53, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x54, 0x61, 0x72, 0x67,
0x65, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x54, 0x61,
0x72, 0x67, 0x65, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x54, 0x61, 0x72, 0x67,
0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c,
0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08,
0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72,
0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x57, 0x0a, 0x0a,
0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x37, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74,
0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x69,
0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62,
0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x41, 0x74, 0x74, 0x72, 0x69,
0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x11, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e,
0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x54, 0x61, 0x67, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09,
0x52, 0x11, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x54,
0x61, 0x67, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54,
0x61, 0x67, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75,
0x6e, 0x64, 0x54, 0x61, 0x67, 0x1a, 0x3d, 0x0a, 0x0f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75,
0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x3a, 0x02, 0x38, 0x01, 0x22, 0x46, 0x0a, 0x1c, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62,
0x65, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c,
0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x46, 0x69,
0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x22, 0xb1, 0x01, 0x0a,
0x10, 0x54, 0x65, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x4f, 0x0a, 0x0e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74,
0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79,
0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d,
0x61, 0x6e, 0x64, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x65,
0x78, 0x74, 0x52, 0x0e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x65,
0x78, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63,
0x74, 0x6f, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x46, 0x69, 0x65, 0x6c,
0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x50, 0x75,
0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
0x08, 0x52, 0x0d, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74,
0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x32, 0xf0, 0x01, 0x0a, 0x0e, 0x52,
0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7b, 0x0a,
0x15, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e,
0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x35, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70,
0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e,
0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e,
0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e,
0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x43,
0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x61, 0x0a, 0x09, 0x54, 0x65,
0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x29, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61,
0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
0x64, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f,
0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x52, 0x6f, 0x75,
0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x00, 0x42, 0x6a, 0x0a,
0x1b, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f,
0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x2f,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f,
0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70,
0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa,
0x02, 0x17, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65,
0x72, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
}
var (
file_app_router_command_command_proto_rawDescOnce sync.Once
file_app_router_command_command_proto_rawDescData = file_app_router_command_command_proto_rawDesc
)
func file_app_router_command_command_proto_rawDescGZIP() []byte {
file_app_router_command_command_proto_rawDescOnce.Do(func() {
file_app_router_command_command_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_router_command_command_proto_rawDescData)
})
return file_app_router_command_command_proto_rawDescData
}
var file_app_router_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_app_router_command_command_proto_goTypes = []interface{}{
(*RoutingContext)(nil), // 0: xray.app.router.command.RoutingContext
(*SubscribeRoutingStatsRequest)(nil), // 1: xray.app.router.command.SubscribeRoutingStatsRequest
(*TestRouteRequest)(nil), // 2: xray.app.router.command.TestRouteRequest
(*Config)(nil), // 3: xray.app.router.command.Config
nil, // 4: xray.app.router.command.RoutingContext.AttributesEntry
(net.Network)(0), // 5: xray.common.net.Network
}
var file_app_router_command_command_proto_depIdxs = []int32{
5, // 0: xray.app.router.command.RoutingContext.Network:type_name -> xray.common.net.Network
4, // 1: xray.app.router.command.RoutingContext.Attributes:type_name -> xray.app.router.command.RoutingContext.AttributesEntry
0, // 2: xray.app.router.command.TestRouteRequest.RoutingContext:type_name -> xray.app.router.command.RoutingContext
1, // 3: xray.app.router.command.RoutingService.SubscribeRoutingStats:input_type -> xray.app.router.command.SubscribeRoutingStatsRequest
2, // 4: xray.app.router.command.RoutingService.TestRoute:input_type -> xray.app.router.command.TestRouteRequest
0, // 5: xray.app.router.command.RoutingService.SubscribeRoutingStats:output_type -> xray.app.router.command.RoutingContext
0, // 6: xray.app.router.command.RoutingService.TestRoute:output_type -> xray.app.router.command.RoutingContext
5, // [5:7] is the sub-list for method output_type
3, // [3:5] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_app_router_command_command_proto_init() }
func file_app_router_command_command_proto_init() {
if File_app_router_command_command_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_app_router_command_command_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RoutingContext); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_router_command_command_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SubscribeRoutingStatsRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_router_command_command_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*TestRouteRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_router_command_command_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_router_command_command_proto_rawDesc,
NumEnums: 0,
NumMessages: 5,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_app_router_command_command_proto_goTypes,
DependencyIndexes: file_app_router_command_command_proto_depIdxs,
MessageInfos: file_app_router_command_command_proto_msgTypes,
}.Build()
File_app_router_command_command_proto = out.File
file_app_router_command_command_proto_rawDesc = nil
file_app_router_command_command_proto_goTypes = nil
file_app_router_command_command_proto_depIdxs = nil
}

View file

@ -0,0 +1,69 @@
syntax = "proto3";
package xray.app.router.command;
option csharp_namespace = "Xray.App.Router.Command";
option go_package = "github.com/xtls/xray-core/v1/app/router/command";
option java_package = "com.xray.app.router.command";
option java_multiple_files = true;
import "common/net/network.proto";
// RoutingContext is the context with information relative to routing process.
// It conforms to the structure of xray.features.routing.Context and
// xray.features.routing.Route.
message RoutingContext {
string InboundTag = 1;
xray.common.net.Network Network = 2;
repeated bytes SourceIPs = 3;
repeated bytes TargetIPs = 4;
uint32 SourcePort = 5;
uint32 TargetPort = 6;
string TargetDomain = 7;
string Protocol = 8;
string User = 9;
map<string, string> Attributes = 10;
repeated string OutboundGroupTags = 11;
string OutboundTag = 12;
}
// SubscribeRoutingStatsRequest subscribes to routing statistics channel if
// opened by xray-core.
// * FieldSelectors selects a subset of fields in routing statistics to return.
// Valid selectors:
// - inbound: Selects connection's inbound tag.
// - network: Selects connection's network.
// - ip: Equivalent as "ip_source" and "ip_target", selects both source and
// target IP.
// - port: Equivalent as "port_source" and "port_target", selects both source
// and target port.
// - domain: Selects target domain.
// - protocol: Select connection's protocol.
// - user: Select connection's inbound user email.
// - attributes: Select connection's additional attributes.
// - outbound: Equivalent as "outbound" and "outbound_group", select both
// outbound tag and outbound group tags.
// * If FieldSelectors is left empty, all fields will be returned.
message SubscribeRoutingStatsRequest {
repeated string FieldSelectors = 1;
}
// TestRouteRequest manually tests a routing result according to the routing
// context message.
// * RoutingContext is the routing message without outbound information.
// * FieldSelectors selects the fields to return in the routing result. All
// fields are returned if left empty.
// * PublishResult broadcasts the routing result to routing statistics channel
// if set true.
message TestRouteRequest {
RoutingContext RoutingContext = 1;
repeated string FieldSelectors = 2;
bool PublishResult = 3;
}
service RoutingService {
rpc SubscribeRoutingStats(SubscribeRoutingStatsRequest)
returns (stream RoutingContext) {}
rpc TestRoute(TestRouteRequest) returns (RoutingContext) {}
}
message Config {}

View file

@ -0,0 +1,161 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package command
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion7
// RoutingServiceClient is the client API for RoutingService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type RoutingServiceClient interface {
SubscribeRoutingStats(ctx context.Context, in *SubscribeRoutingStatsRequest, opts ...grpc.CallOption) (RoutingService_SubscribeRoutingStatsClient, error)
TestRoute(ctx context.Context, in *TestRouteRequest, opts ...grpc.CallOption) (*RoutingContext, error)
}
type routingServiceClient struct {
cc grpc.ClientConnInterface
}
func NewRoutingServiceClient(cc grpc.ClientConnInterface) RoutingServiceClient {
return &routingServiceClient{cc}
}
func (c *routingServiceClient) SubscribeRoutingStats(ctx context.Context, in *SubscribeRoutingStatsRequest, opts ...grpc.CallOption) (RoutingService_SubscribeRoutingStatsClient, error) {
stream, err := c.cc.NewStream(ctx, &_RoutingService_serviceDesc.Streams[0], "/xray.app.router.command.RoutingService/SubscribeRoutingStats", opts...)
if err != nil {
return nil, err
}
x := &routingServiceSubscribeRoutingStatsClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type RoutingService_SubscribeRoutingStatsClient interface {
Recv() (*RoutingContext, error)
grpc.ClientStream
}
type routingServiceSubscribeRoutingStatsClient struct {
grpc.ClientStream
}
func (x *routingServiceSubscribeRoutingStatsClient) Recv() (*RoutingContext, error) {
m := new(RoutingContext)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *routingServiceClient) TestRoute(ctx context.Context, in *TestRouteRequest, opts ...grpc.CallOption) (*RoutingContext, error) {
out := new(RoutingContext)
err := c.cc.Invoke(ctx, "/xray.app.router.command.RoutingService/TestRoute", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// RoutingServiceServer is the server API for RoutingService service.
// All implementations must embed UnimplementedRoutingServiceServer
// for forward compatibility
type RoutingServiceServer interface {
SubscribeRoutingStats(*SubscribeRoutingStatsRequest, RoutingService_SubscribeRoutingStatsServer) error
TestRoute(context.Context, *TestRouteRequest) (*RoutingContext, error)
mustEmbedUnimplementedRoutingServiceServer()
}
// UnimplementedRoutingServiceServer must be embedded to have forward compatible implementations.
type UnimplementedRoutingServiceServer struct {
}
func (UnimplementedRoutingServiceServer) SubscribeRoutingStats(*SubscribeRoutingStatsRequest, RoutingService_SubscribeRoutingStatsServer) error {
return status.Errorf(codes.Unimplemented, "method SubscribeRoutingStats not implemented")
}
func (UnimplementedRoutingServiceServer) TestRoute(context.Context, *TestRouteRequest) (*RoutingContext, error) {
return nil, status.Errorf(codes.Unimplemented, "method TestRoute not implemented")
}
func (UnimplementedRoutingServiceServer) mustEmbedUnimplementedRoutingServiceServer() {}
// UnsafeRoutingServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to RoutingServiceServer will
// result in compilation errors.
type UnsafeRoutingServiceServer interface {
mustEmbedUnimplementedRoutingServiceServer()
}
func RegisterRoutingServiceServer(s grpc.ServiceRegistrar, srv RoutingServiceServer) {
s.RegisterService(&_RoutingService_serviceDesc, srv)
}
func _RoutingService_SubscribeRoutingStats_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(SubscribeRoutingStatsRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(RoutingServiceServer).SubscribeRoutingStats(m, &routingServiceSubscribeRoutingStatsServer{stream})
}
type RoutingService_SubscribeRoutingStatsServer interface {
Send(*RoutingContext) error
grpc.ServerStream
}
type routingServiceSubscribeRoutingStatsServer struct {
grpc.ServerStream
}
func (x *routingServiceSubscribeRoutingStatsServer) Send(m *RoutingContext) error {
return x.ServerStream.SendMsg(m)
}
func _RoutingService_TestRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TestRouteRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoutingServiceServer).TestRoute(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/xray.app.router.command.RoutingService/TestRoute",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoutingServiceServer).TestRoute(ctx, req.(*TestRouteRequest))
}
return interceptor(ctx, in, info, handler)
}
var _RoutingService_serviceDesc = grpc.ServiceDesc{
ServiceName: "xray.app.router.command.RoutingService",
HandlerType: (*RoutingServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "TestRoute",
Handler: _RoutingService_TestRoute_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "SubscribeRoutingStats",
Handler: _RoutingService_SubscribeRoutingStats_Handler,
ServerStreams: true,
},
},
Metadata: "app/router/command/command.proto",
}

View file

@ -0,0 +1,361 @@
package command_test
import (
"context"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/xtls/xray-core/v1/app/router"
. "github.com/xtls/xray-core/v1/app/router/command"
"github.com/xtls/xray-core/v1/app/stats"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/features/routing"
"github.com/xtls/xray-core/v1/testing/mocks"
"google.golang.org/grpc"
"google.golang.org/grpc/test/bufconn"
)
func TestServiceSubscribeRoutingStats(t *testing.T) {
c := stats.NewChannel(&stats.ChannelConfig{
SubscriberLimit: 1,
BufferSize: 0,
Blocking: true,
})
common.Must(c.Start())
defer c.Close()
lis := bufconn.Listen(1024 * 1024)
bufDialer := func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}
testCases := []*RoutingContext{
{InboundTag: "in", OutboundTag: "out"},
{TargetIPs: [][]byte{{1, 2, 3, 4}}, TargetPort: 8080, OutboundTag: "out"},
{TargetDomain: "example.com", TargetPort: 443, OutboundTag: "out"},
{SourcePort: 9999, TargetPort: 9999, OutboundTag: "out"},
{Network: net.Network_UDP, OutboundGroupTags: []string{"outergroup", "innergroup"}, OutboundTag: "out"},
{Protocol: "bittorrent", OutboundTag: "blocked"},
{User: "example@example.com", OutboundTag: "out"},
{SourceIPs: [][]byte{{127, 0, 0, 1}}, Attributes: map[string]string{"attr": "value"}, OutboundTag: "out"},
}
errCh := make(chan error)
nextPub := make(chan struct{})
// Server goroutine
go func() {
server := grpc.NewServer()
RegisterRoutingServiceServer(server, NewRoutingServer(nil, c))
errCh <- server.Serve(lis)
}()
// Publisher goroutine
go func() {
publishTestCases := func() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
for { // Wait until there's one subscriber in routing stats channel
if len(c.Subscribers()) > 0 {
break
}
if ctx.Err() != nil {
return ctx.Err()
}
}
for _, tc := range testCases {
c.Publish(context.Background(), AsRoutingRoute(tc))
time.Sleep(time.Millisecond)
}
return nil
}
if err := publishTestCases(); err != nil {
errCh <- err
}
// Wait for next round of publishing
<-nextPub
if err := publishTestCases(); err != nil {
errCh <- err
}
}()
// Client goroutine
go func() {
defer lis.Close()
conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure())
if err != nil {
errCh <- err
return
}
defer conn.Close()
client := NewRoutingServiceClient(conn)
// Test retrieving all fields
testRetrievingAllFields := func() error {
streamCtx, streamClose := context.WithCancel(context.Background())
// Test the unsubscription of stream works well
defer func() {
streamClose()
timeOutCtx, timeout := context.WithTimeout(context.Background(), time.Second)
defer timeout()
for { // Wait until there's no subscriber in routing stats channel
if len(c.Subscribers()) == 0 {
break
}
if timeOutCtx.Err() != nil {
t.Error("unexpected subscribers not decreased in channel", timeOutCtx.Err())
}
}
}()
stream, err := client.SubscribeRoutingStats(streamCtx, &SubscribeRoutingStatsRequest{})
if err != nil {
return err
}
for _, tc := range testCases {
msg, err := stream.Recv()
if err != nil {
return err
}
if r := cmp.Diff(msg, tc, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
t.Error(r)
}
}
// Test that double subscription will fail
errStream, err := client.SubscribeRoutingStats(context.Background(), &SubscribeRoutingStatsRequest{
FieldSelectors: []string{"ip", "port", "domain", "outbound"},
})
if err != nil {
return err
}
if _, err := errStream.Recv(); err == nil {
t.Error("unexpected successful subscription")
}
return nil
}
// Test retrieving only a subset of fields
testRetrievingSubsetOfFields := func() error {
streamCtx, streamClose := context.WithCancel(context.Background())
defer streamClose()
stream, err := client.SubscribeRoutingStats(streamCtx, &SubscribeRoutingStatsRequest{
FieldSelectors: []string{"ip", "port", "domain", "outbound"},
})
if err != nil {
return err
}
// Send nextPub signal to start next round of publishing
close(nextPub)
for _, tc := range testCases {
msg, err := stream.Recv()
if err != nil {
return err
}
stat := &RoutingContext{ // Only a subset of stats is retrieved
SourceIPs: tc.SourceIPs,
TargetIPs: tc.TargetIPs,
SourcePort: tc.SourcePort,
TargetPort: tc.TargetPort,
TargetDomain: tc.TargetDomain,
OutboundGroupTags: tc.OutboundGroupTags,
OutboundTag: tc.OutboundTag,
}
if r := cmp.Diff(msg, stat, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
t.Error(r)
}
}
return nil
}
if err := testRetrievingAllFields(); err != nil {
errCh <- err
}
if err := testRetrievingSubsetOfFields(); err != nil {
errCh <- err
}
errCh <- nil // Client passed all tests successfully
}()
// Wait for goroutines to complete
select {
case <-time.After(2 * time.Second):
t.Fatal("Test timeout after 2s")
case err := <-errCh:
if err != nil {
t.Fatal(err)
}
}
}
func TestSerivceTestRoute(t *testing.T) {
c := stats.NewChannel(&stats.ChannelConfig{
SubscriberLimit: 1,
BufferSize: 16,
Blocking: true,
})
common.Must(c.Start())
defer c.Close()
r := new(router.Router)
mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
common.Must(r.Init(&router.Config{
Rule: []*router.RoutingRule{
{
InboundTag: []string{"in"},
TargetTag: &router.RoutingRule_Tag{Tag: "out"},
},
{
Protocol: []string{"bittorrent"},
TargetTag: &router.RoutingRule_Tag{Tag: "blocked"},
},
{
PortList: &net.PortList{Range: []*net.PortRange{{From: 8080, To: 8080}}},
TargetTag: &router.RoutingRule_Tag{Tag: "out"},
},
{
SourcePortList: &net.PortList{Range: []*net.PortRange{{From: 9999, To: 9999}}},
TargetTag: &router.RoutingRule_Tag{Tag: "out"},
},
{
Domain: []*router.Domain{{Type: router.Domain_Domain, Value: "com"}},
TargetTag: &router.RoutingRule_Tag{Tag: "out"},
},
{
SourceGeoip: []*router.GeoIP{{CountryCode: "private", Cidr: []*router.CIDR{{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}},
TargetTag: &router.RoutingRule_Tag{Tag: "out"},
},
{
UserEmail: []string{"example@example.com"},
TargetTag: &router.RoutingRule_Tag{Tag: "out"},
},
{
Networks: []net.Network{net.Network_UDP, net.Network_TCP},
TargetTag: &router.RoutingRule_Tag{Tag: "out"},
},
},
}, mocks.NewDNSClient(mockCtl), mocks.NewOutboundManager(mockCtl)))
lis := bufconn.Listen(1024 * 1024)
bufDialer := func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}
errCh := make(chan error)
// Server goroutine
go func() {
server := grpc.NewServer()
RegisterRoutingServiceServer(server, NewRoutingServer(r, c))
errCh <- server.Serve(lis)
}()
// Client goroutine
go func() {
defer lis.Close()
conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure())
if err != nil {
errCh <- err
}
defer conn.Close()
client := NewRoutingServiceClient(conn)
testCases := []*RoutingContext{
{InboundTag: "in", OutboundTag: "out"},
{TargetIPs: [][]byte{{1, 2, 3, 4}}, TargetPort: 8080, OutboundTag: "out"},
{TargetDomain: "example.com", TargetPort: 443, OutboundTag: "out"},
{SourcePort: 9999, TargetPort: 9999, OutboundTag: "out"},
{Network: net.Network_UDP, Protocol: "bittorrent", OutboundTag: "blocked"},
{User: "example@example.com", OutboundTag: "out"},
{SourceIPs: [][]byte{{127, 0, 0, 1}}, Attributes: map[string]string{"attr": "value"}, OutboundTag: "out"},
}
// Test simple TestRoute
testSimple := func() error {
for _, tc := range testCases {
route, err := client.TestRoute(context.Background(), &TestRouteRequest{RoutingContext: tc})
if err != nil {
return err
}
if r := cmp.Diff(route, tc, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
t.Error(r)
}
}
return nil
}
// Test TestRoute with special options
testOptions := func() error {
sub, err := c.Subscribe()
if err != nil {
return err
}
for _, tc := range testCases {
route, err := client.TestRoute(context.Background(), &TestRouteRequest{
RoutingContext: tc,
FieldSelectors: []string{"ip", "port", "domain", "outbound"},
PublishResult: true,
})
if err != nil {
return err
}
stat := &RoutingContext{ // Only a subset of stats is retrieved
SourceIPs: tc.SourceIPs,
TargetIPs: tc.TargetIPs,
SourcePort: tc.SourcePort,
TargetPort: tc.TargetPort,
TargetDomain: tc.TargetDomain,
OutboundGroupTags: tc.OutboundGroupTags,
OutboundTag: tc.OutboundTag,
}
if r := cmp.Diff(route, stat, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
t.Error(r)
}
select { // Check that routing result has been published to statistics channel
case msg, received := <-sub:
if route, ok := msg.(routing.Route); received && ok {
if r := cmp.Diff(AsProtobufMessage(nil)(route), tc, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
t.Error(r)
}
} else {
t.Error("unexpected failure in receiving published routing result for testcase", tc)
}
case <-time.After(100 * time.Millisecond):
t.Error("unexpected failure in receiving published routing result", tc)
}
}
return nil
}
if err := testSimple(); err != nil {
errCh <- err
}
if err := testOptions(); err != nil {
errCh <- err
}
errCh <- nil // Client passed all tests successfully
}()
// Wait for goroutines to complete
select {
case <-time.After(2 * time.Second):
t.Fatal("Test timeout after 2s")
case err := <-errCh:
if err != nil {
t.Fatal(err)
}
}
}

View file

@ -0,0 +1,94 @@
package command
import (
"strings"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/features/routing"
)
// routingContext is an wrapper of protobuf RoutingContext as implementation of routing.Context and routing.Route.
type routingContext struct {
*RoutingContext
}
func (c routingContext) GetSourceIPs() []net.IP {
return mapBytesToIPs(c.RoutingContext.GetSourceIPs())
}
func (c routingContext) GetSourcePort() net.Port {
return net.Port(c.RoutingContext.GetSourcePort())
}
func (c routingContext) GetTargetIPs() []net.IP {
return mapBytesToIPs(c.RoutingContext.GetTargetIPs())
}
func (c routingContext) GetTargetPort() net.Port {
return net.Port(c.RoutingContext.GetTargetPort())
}
// AsRoutingContext converts a protobuf RoutingContext into an implementation of routing.Context.
func AsRoutingContext(r *RoutingContext) routing.Context {
return routingContext{r}
}
// AsRoutingRoute converts a protobuf RoutingContext into an implementation of routing.Route.
func AsRoutingRoute(r *RoutingContext) routing.Route {
return routingContext{r}
}
var fieldMap = map[string]func(*RoutingContext, routing.Route){
"inbound": func(s *RoutingContext, r routing.Route) { s.InboundTag = r.GetInboundTag() },
"network": func(s *RoutingContext, r routing.Route) { s.Network = r.GetNetwork() },
"ip_source": func(s *RoutingContext, r routing.Route) { s.SourceIPs = mapIPsToBytes(r.GetSourceIPs()) },
"ip_target": func(s *RoutingContext, r routing.Route) { s.TargetIPs = mapIPsToBytes(r.GetTargetIPs()) },
"port_source": func(s *RoutingContext, r routing.Route) { s.SourcePort = uint32(r.GetSourcePort()) },
"port_target": func(s *RoutingContext, r routing.Route) { s.TargetPort = uint32(r.GetTargetPort()) },
"domain": func(s *RoutingContext, r routing.Route) { s.TargetDomain = r.GetTargetDomain() },
"protocol": func(s *RoutingContext, r routing.Route) { s.Protocol = r.GetProtocol() },
"user": func(s *RoutingContext, r routing.Route) { s.User = r.GetUser() },
"attributes": func(s *RoutingContext, r routing.Route) { s.Attributes = r.GetAttributes() },
"outbound_group": func(s *RoutingContext, r routing.Route) { s.OutboundGroupTags = r.GetOutboundGroupTags() },
"outbound": func(s *RoutingContext, r routing.Route) { s.OutboundTag = r.GetOutboundTag() },
}
// AsProtobufMessage takes selectors of fields and returns a function to convert routing.Route to protobuf RoutingContext.
func AsProtobufMessage(fieldSelectors []string) func(routing.Route) *RoutingContext {
initializers := []func(*RoutingContext, routing.Route){}
for field, init := range fieldMap {
if len(fieldSelectors) == 0 { // If selectors not set, retrieve all fields
initializers = append(initializers, init)
continue
}
for _, selector := range fieldSelectors {
if strings.HasPrefix(field, selector) {
initializers = append(initializers, init)
break
}
}
}
return func(ctx routing.Route) *RoutingContext {
message := new(RoutingContext)
for _, init := range initializers {
init(message, ctx)
}
return message
}
}
func mapBytesToIPs(bytes [][]byte) []net.IP {
var ips []net.IP
for _, rawIP := range bytes {
ips = append(ips, net.IP(rawIP))
}
return ips
}
func mapIPsToBytes(ips []net.IP) [][]byte {
var bytes [][]byte
for _, ip := range ips {
bytes = append(bytes, []byte(ip))
}
return bytes
}

View file

@ -0,0 +1,9 @@
package command
import "github.com/xtls/xray-core/v1/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

319
app/router/condition.go Normal file
View file

@ -0,0 +1,319 @@
// +build !confonly
package router
import (
"strings"
"go.starlark.net/starlark"
"go.starlark.net/syntax"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/strmatcher"
"github.com/xtls/xray-core/v1/features/routing"
)
type Condition interface {
Apply(ctx routing.Context) bool
}
type ConditionChan []Condition
func NewConditionChan() *ConditionChan {
var condChan ConditionChan = make([]Condition, 0, 8)
return &condChan
}
func (v *ConditionChan) Add(cond Condition) *ConditionChan {
*v = append(*v, cond)
return v
}
// Apply applies all conditions registered in this chan.
func (v *ConditionChan) Apply(ctx routing.Context) bool {
for _, cond := range *v {
if !cond.Apply(ctx) {
return false
}
}
return true
}
func (v *ConditionChan) Len() int {
return len(*v)
}
var matcherTypeMap = map[Domain_Type]strmatcher.Type{
Domain_Plain: strmatcher.Substr,
Domain_Regex: strmatcher.Regex,
Domain_Domain: strmatcher.Domain,
Domain_Full: strmatcher.Full,
}
func domainToMatcher(domain *Domain) (strmatcher.Matcher, error) {
matcherType, f := matcherTypeMap[domain.Type]
if !f {
return nil, newError("unsupported domain type", domain.Type)
}
matcher, err := matcherType.New(domain.Value)
if err != nil {
return nil, newError("failed to create domain matcher").Base(err)
}
return matcher, nil
}
type DomainMatcher struct {
matchers strmatcher.IndexMatcher
}
func NewDomainMatcher(domains []*Domain) (*DomainMatcher, error) {
g := new(strmatcher.MatcherGroup)
for _, d := range domains {
m, err := domainToMatcher(d)
if err != nil {
return nil, err
}
g.Add(m)
}
return &DomainMatcher{
matchers: g,
}, nil
}
func (m *DomainMatcher) ApplyDomain(domain string) bool {
return len(m.matchers.Match(domain)) > 0
}
// Apply implements Condition.
func (m *DomainMatcher) Apply(ctx routing.Context) bool {
domain := ctx.GetTargetDomain()
if len(domain) == 0 {
return false
}
return m.ApplyDomain(domain)
}
type MultiGeoIPMatcher struct {
matchers []*GeoIPMatcher
onSource bool
}
func NewMultiGeoIPMatcher(geoips []*GeoIP, onSource bool) (*MultiGeoIPMatcher, error) {
var matchers []*GeoIPMatcher
for _, geoip := range geoips {
matcher, err := globalGeoIPContainer.Add(geoip)
if err != nil {
return nil, err
}
matchers = append(matchers, matcher)
}
matcher := &MultiGeoIPMatcher{
matchers: matchers,
onSource: onSource,
}
return matcher, nil
}
// Apply implements Condition.
func (m *MultiGeoIPMatcher) Apply(ctx routing.Context) bool {
var ips []net.IP
if m.onSource {
ips = ctx.GetSourceIPs()
} else {
ips = ctx.GetTargetIPs()
}
for _, ip := range ips {
for _, matcher := range m.matchers {
if matcher.Match(ip) {
return true
}
}
}
return false
}
type PortMatcher struct {
port net.MemoryPortList
onSource bool
}
// NewPortMatcher create a new port matcher that can match source or destination port
func NewPortMatcher(list *net.PortList, onSource bool) *PortMatcher {
return &PortMatcher{
port: net.PortListFromProto(list),
onSource: onSource,
}
}
// Apply implements Condition.
func (v *PortMatcher) Apply(ctx routing.Context) bool {
if v.onSource {
return v.port.Contains(ctx.GetSourcePort())
} else {
return v.port.Contains(ctx.GetTargetPort())
}
}
type NetworkMatcher struct {
list [8]bool
}
func NewNetworkMatcher(network []net.Network) NetworkMatcher {
var matcher NetworkMatcher
for _, n := range network {
matcher.list[int(n)] = true
}
return matcher
}
// Apply implements Condition.
func (v NetworkMatcher) Apply(ctx routing.Context) bool {
return v.list[int(ctx.GetNetwork())]
}
type UserMatcher struct {
user []string
}
func NewUserMatcher(users []string) *UserMatcher {
usersCopy := make([]string, 0, len(users))
for _, user := range users {
if len(user) > 0 {
usersCopy = append(usersCopy, user)
}
}
return &UserMatcher{
user: usersCopy,
}
}
// Apply implements Condition.
func (v *UserMatcher) Apply(ctx routing.Context) bool {
user := ctx.GetUser()
if len(user) == 0 {
return false
}
for _, u := range v.user {
if u == user {
return true
}
}
return false
}
type InboundTagMatcher struct {
tags []string
}
func NewInboundTagMatcher(tags []string) *InboundTagMatcher {
tagsCopy := make([]string, 0, len(tags))
for _, tag := range tags {
if len(tag) > 0 {
tagsCopy = append(tagsCopy, tag)
}
}
return &InboundTagMatcher{
tags: tagsCopy,
}
}
// Apply implements Condition.
func (v *InboundTagMatcher) Apply(ctx routing.Context) bool {
tag := ctx.GetInboundTag()
if len(tag) == 0 {
return false
}
for _, t := range v.tags {
if t == tag {
return true
}
}
return false
}
type ProtocolMatcher struct {
protocols []string
}
func NewProtocolMatcher(protocols []string) *ProtocolMatcher {
pCopy := make([]string, 0, len(protocols))
for _, p := range protocols {
if len(p) > 0 {
pCopy = append(pCopy, p)
}
}
return &ProtocolMatcher{
protocols: pCopy,
}
}
// Apply implements Condition.
func (m *ProtocolMatcher) Apply(ctx routing.Context) bool {
protocol := ctx.GetProtocol()
if len(protocol) == 0 {
return false
}
for _, p := range m.protocols {
if strings.HasPrefix(protocol, p) {
return true
}
}
return false
}
type AttributeMatcher struct {
program *starlark.Program
}
func NewAttributeMatcher(code string) (*AttributeMatcher, error) {
starFile, err := syntax.Parse("attr.star", "satisfied=("+code+")", 0)
if err != nil {
return nil, newError("attr rule").Base(err)
}
p, err := starlark.FileProgram(starFile, func(name string) bool {
return name == "attrs"
})
if err != nil {
return nil, err
}
return &AttributeMatcher{
program: p,
}, nil
}
// Match implements attributes matching.
func (m *AttributeMatcher) Match(attrs map[string]string) bool {
attrsDict := new(starlark.Dict)
for key, value := range attrs {
attrsDict.SetKey(starlark.String(key), starlark.String(value))
}
predefined := make(starlark.StringDict)
predefined["attrs"] = attrsDict
thread := &starlark.Thread{
Name: "matcher",
}
results, err := m.program.Init(thread, predefined)
if err != nil {
newError("attr matcher").Base(err).WriteToLog()
}
satisfied := results["satisfied"]
return satisfied != nil && bool(satisfied.Truth())
}
// Apply implements Condition.
func (m *AttributeMatcher) Apply(ctx routing.Context) bool {
attributes := ctx.GetAttributes()
if attributes == nil {
return false
}
return m.Match(attributes)
}

View file

@ -0,0 +1,193 @@
// +build !confonly
package router
import (
"encoding/binary"
"sort"
"github.com/xtls/xray-core/v1/common/net"
)
type ipv6 struct {
a uint64
b uint64
}
type GeoIPMatcher struct {
countryCode string
ip4 []uint32
prefix4 []uint8
ip6 []ipv6
prefix6 []uint8
}
func normalize4(ip uint32, prefix uint8) uint32 {
return (ip >> (32 - prefix)) << (32 - prefix)
}
func normalize6(ip ipv6, prefix uint8) ipv6 {
if prefix <= 64 {
ip.a = (ip.a >> (64 - prefix)) << (64 - prefix)
ip.b = 0
} else {
ip.b = (ip.b >> (128 - prefix)) << (128 - prefix)
}
return ip
}
func (m *GeoIPMatcher) Init(cidrs []*CIDR) error {
ip4Count := 0
ip6Count := 0
for _, cidr := range cidrs {
ip := cidr.Ip
switch len(ip) {
case 4:
ip4Count++
case 16:
ip6Count++
default:
return newError("unexpect ip length: ", len(ip))
}
}
cidrList := CIDRList(cidrs)
sort.Sort(&cidrList)
m.ip4 = make([]uint32, 0, ip4Count)
m.prefix4 = make([]uint8, 0, ip4Count)
m.ip6 = make([]ipv6, 0, ip6Count)
m.prefix6 = make([]uint8, 0, ip6Count)
for _, cidr := range cidrs {
ip := cidr.Ip
prefix := uint8(cidr.Prefix)
switch len(ip) {
case 4:
m.ip4 = append(m.ip4, normalize4(binary.BigEndian.Uint32(ip), prefix))
m.prefix4 = append(m.prefix4, prefix)
case 16:
ip6 := ipv6{
a: binary.BigEndian.Uint64(ip[0:8]),
b: binary.BigEndian.Uint64(ip[8:16]),
}
ip6 = normalize6(ip6, prefix)
m.ip6 = append(m.ip6, ip6)
m.prefix6 = append(m.prefix6, prefix)
}
}
return nil
}
func (m *GeoIPMatcher) match4(ip uint32) bool {
if len(m.ip4) == 0 {
return false
}
if ip < m.ip4[0] {
return false
}
size := uint32(len(m.ip4))
l := uint32(0)
r := size
for l < r {
x := ((l + r) >> 1)
if ip < m.ip4[x] {
r = x
continue
}
nip := normalize4(ip, m.prefix4[x])
if nip == m.ip4[x] {
return true
}
l = x + 1
}
return l > 0 && normalize4(ip, m.prefix4[l-1]) == m.ip4[l-1]
}
func less6(a ipv6, b ipv6) bool {
return a.a < b.a || (a.a == b.a && a.b < b.b)
}
func (m *GeoIPMatcher) match6(ip ipv6) bool {
if len(m.ip6) == 0 {
return false
}
if less6(ip, m.ip6[0]) {
return false
}
size := uint32(len(m.ip6))
l := uint32(0)
r := size
for l < r {
x := (l + r) / 2
if less6(ip, m.ip6[x]) {
r = x
continue
}
if normalize6(ip, m.prefix6[x]) == m.ip6[x] {
return true
}
l = x + 1
}
return l > 0 && normalize6(ip, m.prefix6[l-1]) == m.ip6[l-1]
}
// Match returns true if the given ip is included by the GeoIP.
func (m *GeoIPMatcher) Match(ip net.IP) bool {
switch len(ip) {
case 4:
return m.match4(binary.BigEndian.Uint32(ip))
case 16:
return m.match6(ipv6{
a: binary.BigEndian.Uint64(ip[0:8]),
b: binary.BigEndian.Uint64(ip[8:16]),
})
default:
return false
}
}
// GeoIPMatcherContainer is a container for GeoIPMatchers. It keeps unique copies of GeoIPMatcher by country code.
type GeoIPMatcherContainer struct {
matchers []*GeoIPMatcher
}
// Add adds a new GeoIP set into the container.
// If the country code of GeoIP is not empty, GeoIPMatcherContainer will try to find an existing one, instead of adding a new one.
func (c *GeoIPMatcherContainer) Add(geoip *GeoIP) (*GeoIPMatcher, error) {
if len(geoip.CountryCode) > 0 {
for _, m := range c.matchers {
if m.countryCode == geoip.CountryCode {
return m, nil
}
}
}
m := &GeoIPMatcher{
countryCode: geoip.CountryCode,
}
if err := m.Init(geoip.Cidr); err != nil {
return nil, err
}
if len(geoip.CountryCode) > 0 {
c.matchers = append(c.matchers, m)
}
return m, nil
}
var (
globalGeoIPContainer GeoIPMatcherContainer
)

View file

@ -0,0 +1,195 @@
package router_test
import (
"os"
"path/filepath"
"testing"
"github.com/golang/protobuf/proto"
"github.com/xtls/xray-core/v1/app/router"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/platform"
"github.com/xtls/xray-core/v1/common/platform/filesystem"
)
func init() {
wd, err := os.Getwd()
common.Must(err)
if _, err := os.Stat(platform.GetAssetLocation("geoip.dat")); err != nil && os.IsNotExist(err) {
common.Must(filesystem.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(wd, "..", "..", "release", "config", "geoip.dat")))
}
if _, err := os.Stat(platform.GetAssetLocation("geosite.dat")); err != nil && os.IsNotExist(err) {
common.Must(filesystem.CopyFile(platform.GetAssetLocation("geosite.dat"), filepath.Join(wd, "..", "..", "release", "config", "geosite.dat")))
}
}
func TestGeoIPMatcherContainer(t *testing.T) {
container := &router.GeoIPMatcherContainer{}
m1, err := container.Add(&router.GeoIP{
CountryCode: "CN",
})
common.Must(err)
m2, err := container.Add(&router.GeoIP{
CountryCode: "US",
})
common.Must(err)
m3, err := container.Add(&router.GeoIP{
CountryCode: "CN",
})
common.Must(err)
if m1 != m3 {
t.Error("expect same matcher for same geoip, but not")
}
if m1 == m2 {
t.Error("expect different matcher for different geoip, but actually same")
}
}
func TestGeoIPMatcher(t *testing.T) {
cidrList := router.CIDRList{
{Ip: []byte{0, 0, 0, 0}, Prefix: 8},
{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
{Ip: []byte{100, 64, 0, 0}, Prefix: 10},
{Ip: []byte{127, 0, 0, 0}, Prefix: 8},
{Ip: []byte{169, 254, 0, 0}, Prefix: 16},
{Ip: []byte{172, 16, 0, 0}, Prefix: 12},
{Ip: []byte{192, 0, 0, 0}, Prefix: 24},
{Ip: []byte{192, 0, 2, 0}, Prefix: 24},
{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
{Ip: []byte{192, 18, 0, 0}, Prefix: 15},
{Ip: []byte{198, 51, 100, 0}, Prefix: 24},
{Ip: []byte{203, 0, 113, 0}, Prefix: 24},
{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
{Ip: []byte{91, 108, 4, 0}, Prefix: 16},
}
matcher := &router.GeoIPMatcher{}
common.Must(matcher.Init(cidrList))
testCases := []struct {
Input string
Output bool
}{
{
Input: "192.168.1.1",
Output: true,
},
{
Input: "192.0.0.0",
Output: true,
},
{
Input: "192.0.1.0",
Output: false,
}, {
Input: "0.1.0.0",
Output: true,
},
{
Input: "1.0.0.1",
Output: false,
},
{
Input: "8.8.8.7",
Output: false,
},
{
Input: "8.8.8.8",
Output: true,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
{
Input: "91.108.255.254",
Output: true,
},
}
for _, testCase := range testCases {
ip := net.ParseAddress(testCase.Input).IP()
actual := matcher.Match(ip)
if actual != testCase.Output {
t.Error("expect input", testCase.Input, "to be", testCase.Output, ", but actually", actual)
}
}
}
func TestGeoIPMatcher4CN(t *testing.T) {
ips, err := loadGeoIP("CN")
common.Must(err)
matcher := &router.GeoIPMatcher{}
common.Must(matcher.Init(ips))
if matcher.Match([]byte{8, 8, 8, 8}) {
t.Error("expect CN geoip doesn't contain 8.8.8.8, but actually does")
}
}
func TestGeoIPMatcher6US(t *testing.T) {
ips, err := loadGeoIP("US")
common.Must(err)
matcher := &router.GeoIPMatcher{}
common.Must(matcher.Init(ips))
if !matcher.Match(net.ParseAddress("2001:4860:4860::8888").IP()) {
t.Error("expect US geoip contain 2001:4860:4860::8888, but actually not")
}
}
func loadGeoIP(country string) ([]*router.CIDR, error) {
geoipBytes, err := filesystem.ReadAsset("geoip.dat")
if err != nil {
return nil, err
}
var geoipList router.GeoIPList
if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil {
return nil, err
}
for _, geoip := range geoipList.Entry {
if geoip.CountryCode == country {
return geoip.Cidr, nil
}
}
panic("country not found: " + country)
}
func BenchmarkGeoIPMatcher4CN(b *testing.B) {
ips, err := loadGeoIP("CN")
common.Must(err)
matcher := &router.GeoIPMatcher{}
common.Must(matcher.Init(ips))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = matcher.Match([]byte{8, 8, 8, 8})
}
}
func BenchmarkGeoIPMatcher6US(b *testing.B) {
ips, err := loadGeoIP("US")
common.Must(err)
matcher := &router.GeoIPMatcher{}
common.Must(matcher.Init(ips))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = matcher.Match(net.ParseAddress("2001:4860:4860::8888").IP())
}
}

View file

@ -0,0 +1,446 @@
package router_test
import (
"os"
"path/filepath"
"strconv"
"testing"
"github.com/golang/protobuf/proto"
. "github.com/xtls/xray-core/v1/app/router"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/errors"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/platform"
"github.com/xtls/xray-core/v1/common/platform/filesystem"
"github.com/xtls/xray-core/v1/common/protocol"
"github.com/xtls/xray-core/v1/common/protocol/http"
"github.com/xtls/xray-core/v1/common/session"
"github.com/xtls/xray-core/v1/features/routing"
routing_session "github.com/xtls/xray-core/v1/features/routing/session"
)
func init() {
wd, err := os.Getwd()
common.Must(err)
if _, err := os.Stat(platform.GetAssetLocation("geoip.dat")); err != nil && os.IsNotExist(err) {
common.Must(filesystem.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(wd, "..", "..", "release", "config", "geoip.dat")))
}
if _, err := os.Stat(platform.GetAssetLocation("geosite.dat")); err != nil && os.IsNotExist(err) {
common.Must(filesystem.CopyFile(platform.GetAssetLocation("geosite.dat"), filepath.Join(wd, "..", "..", "release", "config", "geosite.dat")))
}
}
func withBackground() routing.Context {
return &routing_session.Context{}
}
func withOutbound(outbound *session.Outbound) routing.Context {
return &routing_session.Context{Outbound: outbound}
}
func withInbound(inbound *session.Inbound) routing.Context {
return &routing_session.Context{Inbound: inbound}
}
func withContent(content *session.Content) routing.Context {
return &routing_session.Context{Content: content}
}
func TestRoutingRule(t *testing.T) {
type ruleTest struct {
input routing.Context
output bool
}
cases := []struct {
rule *RoutingRule
test []ruleTest
}{
{
rule: &RoutingRule{
Domain: []*Domain{
{
Value: "example.com",
Type: Domain_Plain,
},
{
Value: "google.com",
Type: Domain_Domain,
},
{
Value: "^facebook\\.com$",
Type: Domain_Regex,
},
},
},
test: []ruleTest{
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("example.com"), 80)}),
output: true,
},
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("www.example.com.www"), 80)}),
output: true,
},
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("example.co"), 80)}),
output: false,
},
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("www.google.com"), 80)}),
output: true,
},
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("facebook.com"), 80)}),
output: true,
},
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("www.facebook.com"), 80)}),
output: false,
},
{
input: withBackground(),
output: false,
},
},
},
{
rule: &RoutingRule{
Cidr: []*CIDR{
{
Ip: []byte{8, 8, 8, 8},
Prefix: 32,
},
{
Ip: []byte{8, 8, 8, 8},
Prefix: 32,
},
{
Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(),
Prefix: 128,
},
},
},
test: []ruleTest{
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.8.8"), 80)}),
output: true,
},
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.4.4"), 80)}),
output: false,
},
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), 80)}),
output: true,
},
{
input: withBackground(),
output: false,
},
},
},
{
rule: &RoutingRule{
Geoip: []*GeoIP{
{
Cidr: []*CIDR{
{
Ip: []byte{8, 8, 8, 8},
Prefix: 32,
},
{
Ip: []byte{8, 8, 8, 8},
Prefix: 32,
},
{
Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(),
Prefix: 128,
},
},
},
},
},
test: []ruleTest{
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.8.8"), 80)}),
output: true,
},
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.4.4"), 80)}),
output: false,
},
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), 80)}),
output: true,
},
{
input: withBackground(),
output: false,
},
},
},
{
rule: &RoutingRule{
SourceCidr: []*CIDR{
{
Ip: []byte{192, 168, 0, 0},
Prefix: 16,
},
},
},
test: []ruleTest{
{
input: withInbound(&session.Inbound{Source: net.TCPDestination(net.ParseAddress("192.168.0.1"), 80)}),
output: true,
},
{
input: withInbound(&session.Inbound{Source: net.TCPDestination(net.ParseAddress("10.0.0.1"), 80)}),
output: false,
},
},
},
{
rule: &RoutingRule{
UserEmail: []string{
"admin@example.com",
},
},
test: []ruleTest{
{
input: withInbound(&session.Inbound{User: &protocol.MemoryUser{Email: "admin@example.com"}}),
output: true,
},
{
input: withInbound(&session.Inbound{User: &protocol.MemoryUser{Email: "love@example.com"}}),
output: false,
},
{
input: withBackground(),
output: false,
},
},
},
{
rule: &RoutingRule{
Protocol: []string{"http"},
},
test: []ruleTest{
{
input: withContent(&session.Content{Protocol: (&http.SniffHeader{}).Protocol()}),
output: true,
},
},
},
{
rule: &RoutingRule{
InboundTag: []string{"test", "test1"},
},
test: []ruleTest{
{
input: withInbound(&session.Inbound{Tag: "test"}),
output: true,
},
{
input: withInbound(&session.Inbound{Tag: "test2"}),
output: false,
},
},
},
{
rule: &RoutingRule{
PortList: &net.PortList{
Range: []*net.PortRange{
{From: 443, To: 443},
{From: 1000, To: 1100},
},
},
},
test: []ruleTest{
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 443)}),
output: true,
},
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 1100)}),
output: true,
},
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 1005)}),
output: true,
},
{
input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 53)}),
output: false,
},
},
},
{
rule: &RoutingRule{
SourcePortList: &net.PortList{
Range: []*net.PortRange{
{From: 123, To: 123},
{From: 9993, To: 9999},
},
},
},
test: []ruleTest{
{
input: withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 123)}),
output: true,
},
{
input: withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 9999)}),
output: true,
},
{
input: withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 9994)}),
output: true,
},
{
input: withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 53)}),
output: false,
},
},
},
{
rule: &RoutingRule{
Protocol: []string{"http"},
Attributes: "attrs[':path'].startswith('/test')",
},
test: []ruleTest{
{
input: withContent(&session.Content{Protocol: "http/1.1", Attributes: map[string]string{":path": "/test/1"}}),
output: true,
},
},
},
}
for _, test := range cases {
cond, err := test.rule.BuildCondition()
common.Must(err)
for _, subtest := range test.test {
actual := cond.Apply(subtest.input)
if actual != subtest.output {
t.Error("test case failed: ", subtest.input, " expected ", subtest.output, " but got ", actual)
}
}
}
}
func loadGeoSite(country string) ([]*Domain, error) {
geositeBytes, err := filesystem.ReadAsset("geosite.dat")
if err != nil {
return nil, err
}
var geositeList GeoSiteList
if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil {
return nil, err
}
for _, site := range geositeList.Entry {
if site.CountryCode == country {
return site.Domain, nil
}
}
return nil, errors.New("country not found: " + country)
}
func TestChinaSites(t *testing.T) {
domains, err := loadGeoSite("CN")
common.Must(err)
matcher, err := NewDomainMatcher(domains)
common.Must(err)
type TestCase struct {
Domain string
Output bool
}
testCases := []TestCase{
{
Domain: "163.com",
Output: true,
},
{
Domain: "163.com",
Output: true,
},
{
Domain: "164.com",
Output: false,
},
{
Domain: "164.com",
Output: false,
},
}
for i := 0; i < 1024; i++ {
testCases = append(testCases, TestCase{Domain: strconv.Itoa(i) + ".not-exists.com", Output: false})
}
for _, testCase := range testCases {
r := matcher.ApplyDomain(testCase.Domain)
if r != testCase.Output {
t.Error("expected output ", testCase.Output, " for domain ", testCase.Domain, " but got ", r)
}
}
}
func BenchmarkMultiGeoIPMatcher(b *testing.B) {
var geoips []*GeoIP
{
ips, err := loadGeoIP("CN")
common.Must(err)
geoips = append(geoips, &GeoIP{
CountryCode: "CN",
Cidr: ips,
})
}
{
ips, err := loadGeoIP("JP")
common.Must(err)
geoips = append(geoips, &GeoIP{
CountryCode: "JP",
Cidr: ips,
})
}
{
ips, err := loadGeoIP("CA")
common.Must(err)
geoips = append(geoips, &GeoIP{
CountryCode: "CA",
Cidr: ips,
})
}
{
ips, err := loadGeoIP("US")
common.Must(err)
geoips = append(geoips, &GeoIP{
CountryCode: "US",
Cidr: ips,
})
}
matcher, err := NewMultiGeoIPMatcher(geoips, false)
common.Must(err)
ctx := withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.8.8"), 80)})
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = matcher.Apply(ctx)
}
}

156
app/router/config.go Normal file
View file

@ -0,0 +1,156 @@
// +build !confonly
package router
import (
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/features/outbound"
"github.com/xtls/xray-core/v1/features/routing"
)
// CIDRList is an alias of []*CIDR to provide sort.Interface.
type CIDRList []*CIDR
// Len implements sort.Interface.
func (l *CIDRList) Len() int {
return len(*l)
}
// Less implements sort.Interface.
func (l *CIDRList) Less(i int, j int) bool {
ci := (*l)[i]
cj := (*l)[j]
if len(ci.Ip) < len(cj.Ip) {
return true
}
if len(ci.Ip) > len(cj.Ip) {
return false
}
for k := 0; k < len(ci.Ip); k++ {
if ci.Ip[k] < cj.Ip[k] {
return true
}
if ci.Ip[k] > cj.Ip[k] {
return false
}
}
return ci.Prefix < cj.Prefix
}
// Swap implements sort.Interface.
func (l *CIDRList) Swap(i int, j int) {
(*l)[i], (*l)[j] = (*l)[j], (*l)[i]
}
type Rule struct {
Tag string
Balancer *Balancer
Condition Condition
}
func (r *Rule) GetTag() (string, error) {
if r.Balancer != nil {
return r.Balancer.PickOutbound()
}
return r.Tag, nil
}
// Apply checks rule matching of current routing context.
func (r *Rule) Apply(ctx routing.Context) bool {
return r.Condition.Apply(ctx)
}
func (rr *RoutingRule) BuildCondition() (Condition, error) {
conds := NewConditionChan()
if len(rr.Domain) > 0 {
matcher, err := NewDomainMatcher(rr.Domain)
if err != nil {
return nil, newError("failed to build domain condition").Base(err)
}
conds.Add(matcher)
}
if len(rr.UserEmail) > 0 {
conds.Add(NewUserMatcher(rr.UserEmail))
}
if len(rr.InboundTag) > 0 {
conds.Add(NewInboundTagMatcher(rr.InboundTag))
}
if rr.PortList != nil {
conds.Add(NewPortMatcher(rr.PortList, false))
} else if rr.PortRange != nil {
conds.Add(NewPortMatcher(&net.PortList{Range: []*net.PortRange{rr.PortRange}}, false))
}
if rr.SourcePortList != nil {
conds.Add(NewPortMatcher(rr.SourcePortList, true))
}
if len(rr.Networks) > 0 {
conds.Add(NewNetworkMatcher(rr.Networks))
} else if rr.NetworkList != nil {
conds.Add(NewNetworkMatcher(rr.NetworkList.Network))
}
if len(rr.Geoip) > 0 {
cond, err := NewMultiGeoIPMatcher(rr.Geoip, false)
if err != nil {
return nil, err
}
conds.Add(cond)
} else if len(rr.Cidr) > 0 {
cond, err := NewMultiGeoIPMatcher([]*GeoIP{{Cidr: rr.Cidr}}, false)
if err != nil {
return nil, err
}
conds.Add(cond)
}
if len(rr.SourceGeoip) > 0 {
cond, err := NewMultiGeoIPMatcher(rr.SourceGeoip, true)
if err != nil {
return nil, err
}
conds.Add(cond)
} else if len(rr.SourceCidr) > 0 {
cond, err := NewMultiGeoIPMatcher([]*GeoIP{{Cidr: rr.SourceCidr}}, true)
if err != nil {
return nil, err
}
conds.Add(cond)
}
if len(rr.Protocol) > 0 {
conds.Add(NewProtocolMatcher(rr.Protocol))
}
if len(rr.Attributes) > 0 {
cond, err := NewAttributeMatcher(rr.Attributes)
if err != nil {
return nil, err
}
conds.Add(cond)
}
if conds.Len() == 0 {
return nil, newError("this rule has no effective fields").AtWarning()
}
return conds, nil
}
func (br *BalancingRule) Build(ohm outbound.Manager) (*Balancer, error) {
return &Balancer{
selectors: br.OutboundSelector,
strategy: &RandomStrategy{},
ohm: ohm,
}, nil
}

1242
app/router/config.pb.go Normal file

File diff suppressed because it is too large Load diff

146
app/router/config.proto Normal file
View file

@ -0,0 +1,146 @@
syntax = "proto3";
package xray.app.router;
option csharp_namespace = "Xray.App.Router";
option go_package = "github.com/xtls/xray-core/v1/app/router";
option java_package = "com.xray.app.router";
option java_multiple_files = true;
import "common/net/port.proto";
import "common/net/network.proto";
// Domain for routing decision.
message Domain {
// Type of domain value.
enum Type {
// The value is used as is.
Plain = 0;
// The value is used as a regular expression.
Regex = 1;
// The value is a root domain.
Domain = 2;
// The value is a domain.
Full = 3;
}
// Domain matching type.
Type type = 1;
// Domain value.
string value = 2;
message Attribute {
string key = 1;
oneof typed_value {
bool bool_value = 2;
int64 int_value = 3;
}
}
// Attributes of this domain. May be used for filtering.
repeated Attribute attribute = 3;
}
// IP for routing decision, in CIDR form.
message CIDR {
// IP address, should be either 4 or 16 bytes.
bytes ip = 1;
// Number of leading ones in the network mask.
uint32 prefix = 2;
}
message GeoIP {
string country_code = 1;
repeated CIDR cidr = 2;
}
message GeoIPList {
repeated GeoIP entry = 1;
}
message GeoSite {
string country_code = 1;
repeated Domain domain = 2;
}
message GeoSiteList {
repeated GeoSite entry = 1;
}
message RoutingRule {
oneof target_tag {
// Tag of outbound that this rule is pointing to.
string tag = 1;
// Tag of routing balancer.
string balancing_tag = 12;
}
// List of domains for target domain matching.
repeated Domain domain = 2;
// List of CIDRs for target IP address matching.
// Deprecated. Use geoip below.
repeated CIDR cidr = 3 [deprecated = true];
// List of GeoIPs for target IP address matching. If this entry exists, the
// cidr above will have no effect. GeoIP fields with the same country code are
// supposed to contain exactly same content. They will be merged during
// runtime. For customized GeoIPs, please leave country code empty.
repeated GeoIP geoip = 10;
// A range of port [from, to]. If the destination port is in this range, this
// rule takes effect. Deprecated. Use port_list.
xray.common.net.PortRange port_range = 4 [deprecated = true];
// List of ports.
xray.common.net.PortList port_list = 14;
// List of networks. Deprecated. Use networks.
xray.common.net.NetworkList network_list = 5 [deprecated = true];
// List of networks for matching.
repeated xray.common.net.Network networks = 13;
// List of CIDRs for source IP address matching.
repeated CIDR source_cidr = 6 [deprecated = true];
// List of GeoIPs for source IP address matching. If this entry exists, the
// source_cidr above will have no effect.
repeated GeoIP source_geoip = 11;
// List of ports for source port matching.
xray.common.net.PortList source_port_list = 16;
repeated string user_email = 7;
repeated string inbound_tag = 8;
repeated string protocol = 9;
string attributes = 15;
}
message BalancingRule {
string tag = 1;
repeated string outbound_selector = 2;
}
message Config {
enum DomainStrategy {
// Use domain as is.
AsIs = 0;
// Always resolve IP for domains.
UseIp = 1;
// Resolve to IP if the domain doesn't match any rules.
IpIfNonMatch = 2;
// Resolve to IP if any rule requires IP matching.
IpOnDemand = 3;
}
DomainStrategy domain_strategy = 1;
repeated RoutingRule rule = 2;
repeated BalancingRule balancing_rule = 3;
}

View file

@ -0,0 +1,9 @@
package router
import "github.com/xtls/xray-core/v1/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

146
app/router/router.go Normal file
View file

@ -0,0 +1,146 @@
// +build !confonly
package router
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
import (
"context"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/core"
"github.com/xtls/xray-core/v1/features/dns"
"github.com/xtls/xray-core/v1/features/outbound"
"github.com/xtls/xray-core/v1/features/routing"
routing_dns "github.com/xtls/xray-core/v1/features/routing/dns"
)
// Router is an implementation of routing.Router.
type Router struct {
domainStrategy Config_DomainStrategy
rules []*Rule
balancers map[string]*Balancer
dns dns.Client
}
// Route is an implementation of routing.Route.
type Route struct {
routing.Context
outboundGroupTags []string
outboundTag string
}
// Init initializes the Router.
func (r *Router) Init(config *Config, d dns.Client, ohm outbound.Manager) error {
r.domainStrategy = config.DomainStrategy
r.dns = d
r.balancers = make(map[string]*Balancer, len(config.BalancingRule))
for _, rule := range config.BalancingRule {
balancer, err := rule.Build(ohm)
if err != nil {
return err
}
r.balancers[rule.Tag] = balancer
}
r.rules = make([]*Rule, 0, len(config.Rule))
for _, rule := range config.Rule {
cond, err := rule.BuildCondition()
if err != nil {
return err
}
rr := &Rule{
Condition: cond,
Tag: rule.GetTag(),
}
btag := rule.GetBalancingTag()
if len(btag) > 0 {
brule, found := r.balancers[btag]
if !found {
return newError("balancer ", btag, " not found")
}
rr.Balancer = brule
}
r.rules = append(r.rules, rr)
}
return nil
}
// PickRoute implements routing.Router.
func (r *Router) PickRoute(ctx routing.Context) (routing.Route, error) {
rule, ctx, err := r.pickRouteInternal(ctx)
if err != nil {
return nil, err
}
tag, err := rule.GetTag()
if err != nil {
return nil, err
}
return &Route{Context: ctx, outboundTag: tag}, nil
}
func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context, error) {
if r.domainStrategy == Config_IpOnDemand {
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
}
for _, rule := range r.rules {
if rule.Apply(ctx) {
return rule, ctx, nil
}
}
if r.domainStrategy != Config_IpIfNonMatch || len(ctx.GetTargetDomain()) == 0 {
return nil, ctx, common.ErrNoClue
}
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
// Try applying rules again if we have IPs.
for _, rule := range r.rules {
if rule.Apply(ctx) {
return rule, ctx, nil
}
}
return nil, ctx, common.ErrNoClue
}
// Start implements common.Runnable.
func (*Router) Start() error {
return nil
}
// Close implements common.Closable.
func (*Router) Close() error {
return nil
}
// Type implement common.HasType.
func (*Router) Type() interface{} {
return routing.RouterType()
}
// GetOutboundGroupTags implements routing.Route.
func (r *Route) GetOutboundGroupTags() []string {
return r.outboundGroupTags
}
// GetOutboundTag implements routing.Route.
func (r *Route) GetOutboundTag() string {
return r.outboundTag
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
r := new(Router)
if err := core.RequireFeatures(ctx, func(d dns.Client, ohm outbound.Manager) error {
return r.Init(config.(*Config), d, ohm)
}); err != nil {
return nil, err
}
return r, nil
}))
}

198
app/router/router_test.go Normal file
View file

@ -0,0 +1,198 @@
package router_test
import (
"context"
"testing"
"github.com/golang/mock/gomock"
. "github.com/xtls/xray-core/v1/app/router"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/net"
"github.com/xtls/xray-core/v1/common/session"
"github.com/xtls/xray-core/v1/features/outbound"
routing_session "github.com/xtls/xray-core/v1/features/routing/session"
"github.com/xtls/xray-core/v1/testing/mocks"
)
type mockOutboundManager struct {
outbound.Manager
outbound.HandlerSelector
}
func TestSimpleRouter(t *testing.T) {
config := &Config{
Rule: []*RoutingRule{
{
TargetTag: &RoutingRule_Tag{
Tag: "test",
},
Networks: []net.Network{net.Network_TCP},
},
},
}
mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
mockDNS := mocks.NewDNSClient(mockCtl)
mockOhm := mocks.NewOutboundManager(mockCtl)
mockHs := mocks.NewOutboundHandlerSelector(mockCtl)
r := new(Router)
common.Must(r.Init(config, mockDNS, &mockOutboundManager{
Manager: mockOhm,
HandlerSelector: mockHs,
}))
ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("example.com"), 80)})
route, err := r.PickRoute(routing_session.AsRoutingContext(ctx))
common.Must(err)
if tag := route.GetOutboundTag(); tag != "test" {
t.Error("expect tag 'test', bug actually ", tag)
}
}
func TestSimpleBalancer(t *testing.T) {
config := &Config{
Rule: []*RoutingRule{
{
TargetTag: &RoutingRule_BalancingTag{
BalancingTag: "balance",
},
Networks: []net.Network{net.Network_TCP},
},
},
BalancingRule: []*BalancingRule{
{
Tag: "balance",
OutboundSelector: []string{"test-"},
},
},
}
mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
mockDNS := mocks.NewDNSClient(mockCtl)
mockOhm := mocks.NewOutboundManager(mockCtl)
mockHs := mocks.NewOutboundHandlerSelector(mockCtl)
mockHs.EXPECT().Select(gomock.Eq([]string{"test-"})).Return([]string{"test"})
r := new(Router)
common.Must(r.Init(config, mockDNS, &mockOutboundManager{
Manager: mockOhm,
HandlerSelector: mockHs,
}))
ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("example.com"), 80)})
route, err := r.PickRoute(routing_session.AsRoutingContext(ctx))
common.Must(err)
if tag := route.GetOutboundTag(); tag != "test" {
t.Error("expect tag 'test', bug actually ", tag)
}
}
func TestIPOnDemand(t *testing.T) {
config := &Config{
DomainStrategy: Config_IpOnDemand,
Rule: []*RoutingRule{
{
TargetTag: &RoutingRule_Tag{
Tag: "test",
},
Cidr: []*CIDR{
{
Ip: []byte{192, 168, 0, 0},
Prefix: 16,
},
},
},
},
}
mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
mockDNS := mocks.NewDNSClient(mockCtl)
mockDNS.EXPECT().LookupIP(gomock.Eq("example.com")).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
r := new(Router)
common.Must(r.Init(config, mockDNS, nil))
ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("example.com"), 80)})
route, err := r.PickRoute(routing_session.AsRoutingContext(ctx))
common.Must(err)
if tag := route.GetOutboundTag(); tag != "test" {
t.Error("expect tag 'test', bug actually ", tag)
}
}
func TestIPIfNonMatchDomain(t *testing.T) {
config := &Config{
DomainStrategy: Config_IpIfNonMatch,
Rule: []*RoutingRule{
{
TargetTag: &RoutingRule_Tag{
Tag: "test",
},
Cidr: []*CIDR{
{
Ip: []byte{192, 168, 0, 0},
Prefix: 16,
},
},
},
},
}
mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
mockDNS := mocks.NewDNSClient(mockCtl)
mockDNS.EXPECT().LookupIP(gomock.Eq("example.com")).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
r := new(Router)
common.Must(r.Init(config, mockDNS, nil))
ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("example.com"), 80)})
route, err := r.PickRoute(routing_session.AsRoutingContext(ctx))
common.Must(err)
if tag := route.GetOutboundTag(); tag != "test" {
t.Error("expect tag 'test', bug actually ", tag)
}
}
func TestIPIfNonMatchIP(t *testing.T) {
config := &Config{
DomainStrategy: Config_IpIfNonMatch,
Rule: []*RoutingRule{
{
TargetTag: &RoutingRule_Tag{
Tag: "test",
},
Cidr: []*CIDR{
{
Ip: []byte{127, 0, 0, 0},
Prefix: 8,
},
},
},
},
}
mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
mockDNS := mocks.NewDNSClient(mockCtl)
r := new(Router)
common.Must(r.Init(config, mockDNS, nil))
ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 80)})
route, err := r.PickRoute(routing_session.AsRoutingContext(ctx))
common.Must(err)
if tag := route.GetOutboundTag(); tag != "test" {
t.Error("expect tag 'test', bug actually ", tag)
}
}

174
app/stats/channel.go Normal file
View file

@ -0,0 +1,174 @@
// +build !confonly
package stats
import (
"context"
"sync"
"github.com/xtls/xray-core/v1/common"
)
// Channel is an implementation of stats.Channel.
type Channel struct {
channel chan channelMessage
subscribers []chan interface{}
// Synchronization components
access sync.RWMutex
closed chan struct{}
// Channel options
blocking bool // Set blocking state if channel buffer reaches limit
bufferSize int // Set to 0 as no buffering
subsLimit int // Set to 0 as no subscriber limit
}
// NewChannel creates an instance of Statistics Channel.
func NewChannel(config *ChannelConfig) *Channel {
return &Channel{
channel: make(chan channelMessage, config.BufferSize),
subsLimit: int(config.SubscriberLimit),
bufferSize: int(config.BufferSize),
blocking: config.Blocking,
}
}
// Subscribers implements stats.Channel.
func (c *Channel) Subscribers() []chan interface{} {
c.access.RLock()
defer c.access.RUnlock()
return c.subscribers
}
// Subscribe implements stats.Channel.
func (c *Channel) Subscribe() (chan interface{}, error) {
c.access.Lock()
defer c.access.Unlock()
if c.subsLimit > 0 && len(c.subscribers) >= c.subsLimit {
return nil, newError("Number of subscribers has reached limit")
}
subscriber := make(chan interface{}, c.bufferSize)
c.subscribers = append(c.subscribers, subscriber)
return subscriber, nil
}
// Unsubscribe implements stats.Channel.
func (c *Channel) Unsubscribe(subscriber chan interface{}) error {
c.access.Lock()
defer c.access.Unlock()
for i, s := range c.subscribers {
if s == subscriber {
// Copy to new memory block to prevent modifying original data
subscribers := make([]chan interface{}, len(c.subscribers)-1)
copy(subscribers[:i], c.subscribers[:i])
copy(subscribers[i:], c.subscribers[i+1:])
c.subscribers = subscribers
}
}
return nil
}
// Publish implements stats.Channel.
func (c *Channel) Publish(ctx context.Context, msg interface{}) {
select { // Early exit if channel closed
case <-c.closed:
return
default:
pub := channelMessage{context: ctx, message: msg}
if c.blocking {
pub.publish(c.channel)
} else {
pub.publishNonBlocking(c.channel)
}
}
}
// Running returns whether the channel is running.
func (c *Channel) Running() bool {
select {
case <-c.closed: // Channel closed
default: // Channel running or not initialized
if c.closed != nil { // Channel initialized
return true
}
}
return false
}
// Start implements common.Runnable.
func (c *Channel) Start() error {
c.access.Lock()
defer c.access.Unlock()
if !c.Running() {
c.closed = make(chan struct{}) // Reset close signal
go func() {
for {
select {
case pub := <-c.channel: // Published message received
for _, sub := range c.Subscribers() { // Concurrency-safe subscribers retrievement
if c.blocking {
pub.broadcast(sub)
} else {
pub.broadcastNonBlocking(sub)
}
}
case <-c.closed: // Channel closed
for _, sub := range c.Subscribers() { // Remove all subscribers
common.Must(c.Unsubscribe(sub))
close(sub)
}
return
}
}
}()
}
return nil
}
// Close implements common.Closable.
func (c *Channel) Close() error {
c.access.Lock()
defer c.access.Unlock()
if c.Running() {
close(c.closed) // Send closed signal
}
return nil
}
// channelMessage is the published message with guaranteed delivery.
// message is discarded only when the context is early cancelled.
type channelMessage struct {
context context.Context
message interface{}
}
func (c channelMessage) publish(publisher chan channelMessage) {
select {
case publisher <- c:
case <-c.context.Done():
}
}
func (c channelMessage) publishNonBlocking(publisher chan channelMessage) {
select {
case publisher <- c:
default: // Create another goroutine to keep sending message
go c.publish(publisher)
}
}
func (c channelMessage) broadcast(subscriber chan interface{}) {
select {
case subscriber <- c.message:
case <-c.context.Done():
}
}
func (c channelMessage) broadcastNonBlocking(subscriber chan interface{}) {
select {
case subscriber <- c.message:
default: // Create another goroutine to keep sending message
go c.broadcast(subscriber)
}
}

405
app/stats/channel_test.go Normal file
View file

@ -0,0 +1,405 @@
package stats_test
import (
"context"
"fmt"
"testing"
"time"
. "github.com/xtls/xray-core/v1/app/stats"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/features/stats"
)
func TestStatsChannel(t *testing.T) {
// At most 2 subscribers could be registered
c := NewChannel(&ChannelConfig{SubscriberLimit: 2, Blocking: true})
a, err := stats.SubscribeRunnableChannel(c)
common.Must(err)
if !c.Running() {
t.Fatal("unexpected failure in running channel after first subscription")
}
b, err := c.Subscribe()
common.Must(err)
// Test that third subscriber is forbidden
_, err = c.Subscribe()
if err == nil {
t.Fatal("unexpected successful subscription")
}
t.Log("expected error: ", err)
stopCh := make(chan struct{})
errCh := make(chan string)
go func() {
c.Publish(context.Background(), 1)
c.Publish(context.Background(), 2)
c.Publish(context.Background(), "3")
c.Publish(context.Background(), []int{4})
stopCh <- struct{}{}
}()
go func() {
if v, ok := (<-a).(int); !ok || v != 1 {
errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1)
}
if v, ok := (<-a).(int); !ok || v != 2 {
errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 2)
}
if v, ok := (<-a).(string); !ok || v != "3" {
errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", "3")
}
if v, ok := (<-a).([]int); !ok || v[0] != 4 {
errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", []int{4})
}
stopCh <- struct{}{}
}()
go func() {
if v, ok := (<-b).(int); !ok || v != 1 {
errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1)
}
if v, ok := (<-b).(int); !ok || v != 2 {
errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 2)
}
if v, ok := (<-b).(string); !ok || v != "3" {
errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", "3")
}
if v, ok := (<-b).([]int); !ok || v[0] != 4 {
errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", []int{4})
}
stopCh <- struct{}{}
}()
timeout := time.After(2 * time.Second)
for i := 0; i < 3; i++ {
select {
case <-timeout:
t.Fatal("Test timeout after 2s")
case e := <-errCh:
t.Fatal(e)
case <-stopCh:
}
}
// Test the unsubscription of channel
common.Must(c.Unsubscribe(b))
// Test the last subscriber will close channel with `UnsubscribeClosableChannel`
common.Must(stats.UnsubscribeClosableChannel(c, a))
if c.Running() {
t.Fatal("unexpected running channel after unsubscribing the last subscriber")
}
}
func TestStatsChannelUnsubcribe(t *testing.T) {
c := NewChannel(&ChannelConfig{Blocking: true})
common.Must(c.Start())
defer c.Close()
a, err := c.Subscribe()
common.Must(err)
defer c.Unsubscribe(a)
b, err := c.Subscribe()
common.Must(err)
pauseCh := make(chan struct{})
stopCh := make(chan struct{})
errCh := make(chan string)
{
var aSet, bSet bool
for _, s := range c.Subscribers() {
if s == a {
aSet = true
}
if s == b {
bSet = true
}
}
if !(aSet && bSet) {
t.Fatal("unexpected subscribers: ", c.Subscribers())
}
}
go func() { // Blocking publish
c.Publish(context.Background(), 1)
<-pauseCh // Wait for `b` goroutine to resume sending message
c.Publish(context.Background(), 2)
}()
go func() {
if v, ok := (<-a).(int); !ok || v != 1 {
errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1)
}
if v, ok := (<-a).(int); !ok || v != 2 {
errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 2)
}
}()
go func() {
if v, ok := (<-b).(int); !ok || v != 1 {
errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1)
}
// Unsubscribe `b` while publishing is paused
c.Unsubscribe(b)
{ // Test `b` is not in subscribers
var aSet, bSet bool
for _, s := range c.Subscribers() {
if s == a {
aSet = true
}
if s == b {
bSet = true
}
}
if !(aSet && !bSet) {
errCh <- fmt.Sprint("unexpected subscribers: ", c.Subscribers())
}
}
// Resume publishing progress
close(pauseCh)
// Test `b` is neither closed nor able to receive any data
select {
case v, ok := <-b:
if ok {
errCh <- fmt.Sprint("unexpected data received: ", v)
} else {
errCh <- fmt.Sprint("unexpected closed channel: ", b)
}
default:
}
close(stopCh)
}()
select {
case <-time.After(2 * time.Second):
t.Fatal("Test timeout after 2s")
case e := <-errCh:
t.Fatal(e)
case <-stopCh:
}
}
func TestStatsChannelBlocking(t *testing.T) {
// Do not use buffer so as to create blocking scenario
c := NewChannel(&ChannelConfig{BufferSize: 0, Blocking: true})
common.Must(c.Start())
defer c.Close()
a, err := c.Subscribe()
common.Must(err)
defer c.Unsubscribe(a)
pauseCh := make(chan struct{})
stopCh := make(chan struct{})
errCh := make(chan string)
ctx, cancel := context.WithCancel(context.Background())
// Test blocking channel publishing
go func() {
// Dummy messsage with no subscriber receiving, will block broadcasting goroutine
c.Publish(context.Background(), nil)
<-pauseCh
// Publishing should be blocked here, for last message was not cleared and buffer was full
c.Publish(context.Background(), nil)
pauseCh <- struct{}{}
// Publishing should still be blocked here
c.Publish(ctx, nil)
// Check publishing is done because context is canceled
select {
case <-ctx.Done():
if ctx.Err() != context.Canceled {
errCh <- fmt.Sprint("unexpected error: ", ctx.Err())
}
default:
errCh <- "unexpected non-blocked publishing"
}
close(stopCh)
}()
go func() {
pauseCh <- struct{}{}
select {
case <-pauseCh:
errCh <- "unexpected non-blocked publishing"
case <-time.After(100 * time.Millisecond):
}
// Receive first published message
<-a
select {
case <-pauseCh:
case <-time.After(100 * time.Millisecond):
errCh <- "unexpected blocking publishing"
}
// Manually cancel the context to end publishing
cancel()
}()
select {
case <-time.After(2 * time.Second):
t.Fatal("Test timeout after 2s")
case e := <-errCh:
t.Fatal(e)
case <-stopCh:
}
}
func TestStatsChannelNonBlocking(t *testing.T) {
// Do not use buffer so as to create blocking scenario
c := NewChannel(&ChannelConfig{BufferSize: 0, Blocking: false})
common.Must(c.Start())
defer c.Close()
a, err := c.Subscribe()
common.Must(err)
defer c.Unsubscribe(a)
pauseCh := make(chan struct{})
stopCh := make(chan struct{})
errCh := make(chan string)
ctx, cancel := context.WithCancel(context.Background())
// Test blocking channel publishing
go func() {
c.Publish(context.Background(), nil)
c.Publish(context.Background(), nil)
pauseCh <- struct{}{}
<-pauseCh
c.Publish(ctx, nil)
c.Publish(ctx, nil)
// Check publishing is done because context is canceled
select {
case <-ctx.Done():
if ctx.Err() != context.Canceled {
errCh <- fmt.Sprint("unexpected error: ", ctx.Err())
}
case <-time.After(100 * time.Millisecond):
errCh <- "unexpected non-cancelled publishing"
}
}()
go func() {
// Check publishing won't block even if there is no subscriber receiving message
select {
case <-pauseCh:
case <-time.After(100 * time.Millisecond):
errCh <- "unexpected blocking publishing"
}
// Receive first and second published message
<-a
<-a
pauseCh <- struct{}{}
// Manually cancel the context to end publishing
cancel()
// Check third and forth published message is cancelled and cannot receive
<-time.After(100 * time.Millisecond)
select {
case <-a:
errCh <- "unexpected non-cancelled publishing"
default:
}
select {
case <-a:
errCh <- "unexpected non-cancelled publishing"
default:
}
close(stopCh)
}()
select {
case <-time.After(2 * time.Second):
t.Fatal("Test timeout after 2s")
case e := <-errCh:
t.Fatal(e)
case <-stopCh:
}
}
func TestStatsChannelConcurrency(t *testing.T) {
// Do not use buffer so as to create blocking scenario
c := NewChannel(&ChannelConfig{BufferSize: 0, Blocking: true})
common.Must(c.Start())
defer c.Close()
a, err := c.Subscribe()
common.Must(err)
defer c.Unsubscribe(a)
b, err := c.Subscribe()
common.Must(err)
defer c.Unsubscribe(b)
stopCh := make(chan struct{})
errCh := make(chan string)
go func() { // Blocking publish
c.Publish(context.Background(), 1)
c.Publish(context.Background(), 2)
}()
go func() {
if v, ok := (<-a).(int); !ok || v != 1 {
errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1)
}
if v, ok := (<-a).(int); !ok || v != 2 {
errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 2)
}
}()
go func() {
// Block `b` for a time so as to ensure source channel is trying to send message to `b`.
<-time.After(25 * time.Millisecond)
// This causes concurrency scenario: unsubscribe `b` while trying to send message to it
c.Unsubscribe(b)
// Test `b` is not closed and can still receive data 1:
// Because unsubscribe won't affect the ongoing process of sending message.
select {
case v, ok := <-b:
if v1, ok1 := v.(int); !(ok && ok1 && v1 == 1) {
errCh <- fmt.Sprint("unexpected failure in receiving data: ", 1)
}
default:
errCh <- fmt.Sprint("unexpected block from receiving data: ", 1)
}
// Test `b` is not closed but cannot receive data 2:
// Because in a new round of messaging, `b` has been unsubscribed.
select {
case v, ok := <-b:
if ok {
errCh <- fmt.Sprint("unexpected receiving: ", v)
} else {
errCh <- "unexpected closing of channel"
}
default:
}
close(stopCh)
}()
select {
case <-time.After(2 * time.Second):
t.Fatal("Test timeout after 2s")
case e := <-errCh:
t.Fatal(e)
case <-stopCh:
}
}

View file

@ -0,0 +1,127 @@
// +build !confonly
package command
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
import (
"context"
"runtime"
"time"
grpc "google.golang.org/grpc"
"github.com/xtls/xray-core/v1/app/stats"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/strmatcher"
"github.com/xtls/xray-core/v1/core"
feature_stats "github.com/xtls/xray-core/v1/features/stats"
)
// statsServer is an implementation of StatsService.
type statsServer struct {
stats feature_stats.Manager
startTime time.Time
}
func NewStatsServer(manager feature_stats.Manager) StatsServiceServer {
return &statsServer{
stats: manager,
startTime: time.Now(),
}
}
func (s *statsServer) GetStats(ctx context.Context, request *GetStatsRequest) (*GetStatsResponse, error) {
c := s.stats.GetCounter(request.Name)
if c == nil {
return nil, newError(request.Name, " not found.")
}
var value int64
if request.Reset_ {
value = c.Set(0)
} else {
value = c.Value()
}
return &GetStatsResponse{
Stat: &Stat{
Name: request.Name,
Value: value,
},
}, nil
}
func (s *statsServer) QueryStats(ctx context.Context, request *QueryStatsRequest) (*QueryStatsResponse, error) {
matcher, err := strmatcher.Substr.New(request.Pattern)
if err != nil {
return nil, err
}
response := &QueryStatsResponse{}
manager, ok := s.stats.(*stats.Manager)
if !ok {
return nil, newError("QueryStats only works its own stats.Manager.")
}
manager.VisitCounters(func(name string, c feature_stats.Counter) bool {
if matcher.Match(name) {
var value int64
if request.Reset_ {
value = c.Set(0)
} else {
value = c.Value()
}
response.Stat = append(response.Stat, &Stat{
Name: name,
Value: value,
})
}
return true
})
return response, nil
}
func (s *statsServer) GetSysStats(ctx context.Context, request *SysStatsRequest) (*SysStatsResponse, error) {
var rtm runtime.MemStats
runtime.ReadMemStats(&rtm)
uptime := time.Since(s.startTime)
response := &SysStatsResponse{
Uptime: uint32(uptime.Seconds()),
NumGoroutine: uint32(runtime.NumGoroutine()),
Alloc: rtm.Alloc,
TotalAlloc: rtm.TotalAlloc,
Sys: rtm.Sys,
Mallocs: rtm.Mallocs,
Frees: rtm.Frees,
LiveObjects: rtm.Mallocs - rtm.Frees,
NumGC: rtm.NumGC,
PauseTotalNs: rtm.PauseTotalNs,
}
return response, nil
}
func (s *statsServer) mustEmbedUnimplementedStatsServiceServer() {}
type service struct {
statsManager feature_stats.Manager
}
func (s *service) Register(server *grpc.Server) {
RegisterStatsServiceServer(server, NewStatsServer(s.statsManager))
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) {
s := new(service)
core.RequireFeatures(ctx, func(sm feature_stats.Manager) {
s.statsManager = sm
})
return s, nil
}))
}

View file

@ -0,0 +1,720 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.14.0
// source: app/stats/command/command.proto
package command
import (
proto "github.com/golang/protobuf/proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
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)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type GetStatsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Name of the stat counter.
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// Whether or not to reset the counter to fetching its value.
Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"`
}
func (x *GetStatsRequest) Reset() {
*x = GetStatsRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_app_stats_command_command_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetStatsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetStatsRequest) ProtoMessage() {}
func (x *GetStatsRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetStatsRequest.ProtoReflect.Descriptor instead.
func (*GetStatsRequest) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{0}
}
func (x *GetStatsRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *GetStatsRequest) GetReset_() bool {
if x != nil {
return x.Reset_
}
return false
}
type Stat struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Value int64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"`
}
func (x *Stat) Reset() {
*x = Stat{}
if protoimpl.UnsafeEnabled {
mi := &file_app_stats_command_command_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Stat) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Stat) ProtoMessage() {}
func (x *Stat) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Stat.ProtoReflect.Descriptor instead.
func (*Stat) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{1}
}
func (x *Stat) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Stat) GetValue() int64 {
if x != nil {
return x.Value
}
return 0
}
type GetStatsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Stat *Stat `protobuf:"bytes,1,opt,name=stat,proto3" json:"stat,omitempty"`
}
func (x *GetStatsResponse) Reset() {
*x = GetStatsResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_app_stats_command_command_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetStatsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetStatsResponse) ProtoMessage() {}
func (x *GetStatsResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetStatsResponse.ProtoReflect.Descriptor instead.
func (*GetStatsResponse) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{2}
}
func (x *GetStatsResponse) GetStat() *Stat {
if x != nil {
return x.Stat
}
return nil
}
type QueryStatsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Pattern string `protobuf:"bytes,1,opt,name=pattern,proto3" json:"pattern,omitempty"`
Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"`
}
func (x *QueryStatsRequest) Reset() {
*x = QueryStatsRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_app_stats_command_command_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *QueryStatsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*QueryStatsRequest) ProtoMessage() {}
func (x *QueryStatsRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use QueryStatsRequest.ProtoReflect.Descriptor instead.
func (*QueryStatsRequest) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{3}
}
func (x *QueryStatsRequest) GetPattern() string {
if x != nil {
return x.Pattern
}
return ""
}
func (x *QueryStatsRequest) GetReset_() bool {
if x != nil {
return x.Reset_
}
return false
}
type QueryStatsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Stat []*Stat `protobuf:"bytes,1,rep,name=stat,proto3" json:"stat,omitempty"`
}
func (x *QueryStatsResponse) Reset() {
*x = QueryStatsResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_app_stats_command_command_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *QueryStatsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*QueryStatsResponse) ProtoMessage() {}
func (x *QueryStatsResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use QueryStatsResponse.ProtoReflect.Descriptor instead.
func (*QueryStatsResponse) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{4}
}
func (x *QueryStatsResponse) GetStat() []*Stat {
if x != nil {
return x.Stat
}
return nil
}
type SysStatsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *SysStatsRequest) Reset() {
*x = SysStatsRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_app_stats_command_command_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SysStatsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SysStatsRequest) ProtoMessage() {}
func (x *SysStatsRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SysStatsRequest.ProtoReflect.Descriptor instead.
func (*SysStatsRequest) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{5}
}
type SysStatsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
NumGoroutine uint32 `protobuf:"varint,1,opt,name=NumGoroutine,proto3" json:"NumGoroutine,omitempty"`
NumGC uint32 `protobuf:"varint,2,opt,name=NumGC,proto3" json:"NumGC,omitempty"`
Alloc uint64 `protobuf:"varint,3,opt,name=Alloc,proto3" json:"Alloc,omitempty"`
TotalAlloc uint64 `protobuf:"varint,4,opt,name=TotalAlloc,proto3" json:"TotalAlloc,omitempty"`
Sys uint64 `protobuf:"varint,5,opt,name=Sys,proto3" json:"Sys,omitempty"`
Mallocs uint64 `protobuf:"varint,6,opt,name=Mallocs,proto3" json:"Mallocs,omitempty"`
Frees uint64 `protobuf:"varint,7,opt,name=Frees,proto3" json:"Frees,omitempty"`
LiveObjects uint64 `protobuf:"varint,8,opt,name=LiveObjects,proto3" json:"LiveObjects,omitempty"`
PauseTotalNs uint64 `protobuf:"varint,9,opt,name=PauseTotalNs,proto3" json:"PauseTotalNs,omitempty"`
Uptime uint32 `protobuf:"varint,10,opt,name=Uptime,proto3" json:"Uptime,omitempty"`
}
func (x *SysStatsResponse) Reset() {
*x = SysStatsResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_app_stats_command_command_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SysStatsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SysStatsResponse) ProtoMessage() {}
func (x *SysStatsResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SysStatsResponse.ProtoReflect.Descriptor instead.
func (*SysStatsResponse) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{6}
}
func (x *SysStatsResponse) GetNumGoroutine() uint32 {
if x != nil {
return x.NumGoroutine
}
return 0
}
func (x *SysStatsResponse) GetNumGC() uint32 {
if x != nil {
return x.NumGC
}
return 0
}
func (x *SysStatsResponse) GetAlloc() uint64 {
if x != nil {
return x.Alloc
}
return 0
}
func (x *SysStatsResponse) GetTotalAlloc() uint64 {
if x != nil {
return x.TotalAlloc
}
return 0
}
func (x *SysStatsResponse) GetSys() uint64 {
if x != nil {
return x.Sys
}
return 0
}
func (x *SysStatsResponse) GetMallocs() uint64 {
if x != nil {
return x.Mallocs
}
return 0
}
func (x *SysStatsResponse) GetFrees() uint64 {
if x != nil {
return x.Frees
}
return 0
}
func (x *SysStatsResponse) GetLiveObjects() uint64 {
if x != nil {
return x.LiveObjects
}
return 0
}
func (x *SysStatsResponse) GetPauseTotalNs() uint64 {
if x != nil {
return x.PauseTotalNs
}
return 0
}
func (x *SysStatsResponse) GetUptime() uint32 {
if x != nil {
return x.Uptime
}
return 0
}
type Config struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *Config) Reset() {
*x = Config{}
if protoimpl.UnsafeEnabled {
mi := &file_app_stats_command_command_proto_msgTypes[7]
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_stats_command_command_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && 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_stats_command_command_proto_rawDescGZIP(), []int{7}
}
var File_app_stats_command_command_proto protoreflect.FileDescriptor
var file_app_stats_command_command_proto_rawDesc = []byte{
0x0a, 0x1f, 0x61, 0x70, 0x70, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2f, 0x63, 0x6f, 0x6d, 0x6d,
0x61, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x12, 0x16, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74,
0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x3b, 0x0a, 0x0f, 0x47, 0x65, 0x74,
0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04,
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
0x05, 0x72, 0x65, 0x73, 0x65, 0x74, 0x22, 0x30, 0x0a, 0x04, 0x53, 0x74, 0x61, 0x74, 0x12, 0x12,
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x44, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53,
0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x04,
0x73, 0x74, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61,
0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d,
0x61, 0x6e, 0x64, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, 0x74, 0x22, 0x43,
0x0a, 0x11, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x12, 0x14, 0x0a,
0x05, 0x72, 0x65, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x72, 0x65,
0x73, 0x65, 0x74, 0x22, 0x46, 0x0a, 0x12, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74,
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x73, 0x74, 0x61,
0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61,
0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
0x2e, 0x53, 0x74, 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, 0x74, 0x22, 0x11, 0x0a, 0x0f, 0x53,
0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa2,
0x02, 0x0a, 0x10, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4e, 0x75, 0x6d, 0x47, 0x6f, 0x72, 0x6f, 0x75, 0x74,
0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x4e, 0x75, 0x6d, 0x47, 0x6f,
0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x75, 0x6d, 0x47, 0x43,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x4e, 0x75, 0x6d, 0x47, 0x43, 0x12, 0x14, 0x0a,
0x05, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x41, 0x6c,
0x6c, 0x6f, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x6f,
0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c,
0x6c, 0x6f, 0x63, 0x12, 0x10, 0x0a, 0x03, 0x53, 0x79, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04,
0x52, 0x03, 0x53, 0x79, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x73,
0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x4d, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x73, 0x12,
0x14, 0x0a, 0x05, 0x46, 0x72, 0x65, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05,
0x46, 0x72, 0x65, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x4c, 0x69, 0x76, 0x65, 0x4f, 0x62, 0x6a,
0x65, 0x63, 0x74, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x4c, 0x69, 0x76, 0x65,
0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x50, 0x61, 0x75, 0x73, 0x65,
0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x50,
0x61, 0x75, 0x73, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x55,
0x70, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x55, 0x70, 0x74,
0x69, 0x6d, 0x65, 0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x32, 0xba, 0x02,
0x0a, 0x0c, 0x53, 0x74, 0x61, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5f,
0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61,
0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d,
0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73,
0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74,
0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
0x65, 0x0a, 0x0a, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x29, 0x2e,
0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63,
0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74,
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e,
0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
0x64, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x62, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x53, 0x79, 0x73,
0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x53,
0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28,
0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e,
0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x67, 0x0a, 0x1a, 0x63, 0x6f,
0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73,
0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79,
0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x73, 0x74, 0x61,
0x74, 0x73, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x16, 0x58, 0x72, 0x61,
0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x43, 0x6f, 0x6d, 0x6d,
0x61, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_app_stats_command_command_proto_rawDescOnce sync.Once
file_app_stats_command_command_proto_rawDescData = file_app_stats_command_command_proto_rawDesc
)
func file_app_stats_command_command_proto_rawDescGZIP() []byte {
file_app_stats_command_command_proto_rawDescOnce.Do(func() {
file_app_stats_command_command_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_stats_command_command_proto_rawDescData)
})
return file_app_stats_command_command_proto_rawDescData
}
var file_app_stats_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_app_stats_command_command_proto_goTypes = []interface{}{
(*GetStatsRequest)(nil), // 0: xray.app.stats.command.GetStatsRequest
(*Stat)(nil), // 1: xray.app.stats.command.Stat
(*GetStatsResponse)(nil), // 2: xray.app.stats.command.GetStatsResponse
(*QueryStatsRequest)(nil), // 3: xray.app.stats.command.QueryStatsRequest
(*QueryStatsResponse)(nil), // 4: xray.app.stats.command.QueryStatsResponse
(*SysStatsRequest)(nil), // 5: xray.app.stats.command.SysStatsRequest
(*SysStatsResponse)(nil), // 6: xray.app.stats.command.SysStatsResponse
(*Config)(nil), // 7: xray.app.stats.command.Config
}
var file_app_stats_command_command_proto_depIdxs = []int32{
1, // 0: xray.app.stats.command.GetStatsResponse.stat:type_name -> xray.app.stats.command.Stat
1, // 1: xray.app.stats.command.QueryStatsResponse.stat:type_name -> xray.app.stats.command.Stat
0, // 2: xray.app.stats.command.StatsService.GetStats:input_type -> xray.app.stats.command.GetStatsRequest
3, // 3: xray.app.stats.command.StatsService.QueryStats:input_type -> xray.app.stats.command.QueryStatsRequest
5, // 4: xray.app.stats.command.StatsService.GetSysStats:input_type -> xray.app.stats.command.SysStatsRequest
2, // 5: xray.app.stats.command.StatsService.GetStats:output_type -> xray.app.stats.command.GetStatsResponse
4, // 6: xray.app.stats.command.StatsService.QueryStats:output_type -> xray.app.stats.command.QueryStatsResponse
6, // 7: xray.app.stats.command.StatsService.GetSysStats:output_type -> xray.app.stats.command.SysStatsResponse
5, // [5:8] is the sub-list for method output_type
2, // [2:5] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_app_stats_command_command_proto_init() }
func file_app_stats_command_command_proto_init() {
if File_app_stats_command_command_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_app_stats_command_command_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetStatsRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_stats_command_command_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Stat); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_stats_command_command_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetStatsResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_stats_command_command_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*QueryStatsRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_stats_command_command_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*QueryStatsResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_stats_command_command_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SysStatsRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_stats_command_command_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SysStatsResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_app_stats_command_command_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_stats_command_command_proto_rawDesc,
NumEnums: 0,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_app_stats_command_command_proto_goTypes,
DependencyIndexes: file_app_stats_command_command_proto_depIdxs,
MessageInfos: file_app_stats_command_command_proto_msgTypes,
}.Build()
File_app_stats_command_command_proto = out.File
file_app_stats_command_command_proto_rawDesc = nil
file_app_stats_command_command_proto_goTypes = nil
file_app_stats_command_command_proto_depIdxs = nil
}

View file

@ -0,0 +1,55 @@
syntax = "proto3";
package xray.app.stats.command;
option csharp_namespace = "Xray.App.Stats.Command";
option go_package = "github.com/xtls/xray-core/v1/app/stats/command";
option java_package = "com.xray.app.stats.command";
option java_multiple_files = true;
message GetStatsRequest {
// Name of the stat counter.
string name = 1;
// Whether or not to reset the counter to fetching its value.
bool reset = 2;
}
message Stat {
string name = 1;
int64 value = 2;
}
message GetStatsResponse {
Stat stat = 1;
}
message QueryStatsRequest {
string pattern = 1;
bool reset = 2;
}
message QueryStatsResponse {
repeated Stat stat = 1;
}
message SysStatsRequest {}
message SysStatsResponse {
uint32 NumGoroutine = 1;
uint32 NumGC = 2;
uint64 Alloc = 3;
uint64 TotalAlloc = 4;
uint64 Sys = 5;
uint64 Mallocs = 6;
uint64 Frees = 7;
uint64 LiveObjects = 8;
uint64 PauseTotalNs = 9;
uint32 Uptime = 10;
}
service StatsService {
rpc GetStats(GetStatsRequest) returns (GetStatsResponse) {}
rpc QueryStats(QueryStatsRequest) returns (QueryStatsResponse) {}
rpc GetSysStats(SysStatsRequest) returns (SysStatsResponse) {}
}
message Config {}

View file

@ -0,0 +1,169 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package command
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion7
// StatsServiceClient is the client API for StatsService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type StatsServiceClient interface {
GetStats(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error)
QueryStats(ctx context.Context, in *QueryStatsRequest, opts ...grpc.CallOption) (*QueryStatsResponse, error)
GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error)
}
type statsServiceClient struct {
cc grpc.ClientConnInterface
}
func NewStatsServiceClient(cc grpc.ClientConnInterface) StatsServiceClient {
return &statsServiceClient{cc}
}
func (c *statsServiceClient) GetStats(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error) {
out := new(GetStatsResponse)
err := c.cc.Invoke(ctx, "/xray.app.stats.command.StatsService/GetStats", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *statsServiceClient) QueryStats(ctx context.Context, in *QueryStatsRequest, opts ...grpc.CallOption) (*QueryStatsResponse, error) {
out := new(QueryStatsResponse)
err := c.cc.Invoke(ctx, "/xray.app.stats.command.StatsService/QueryStats", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *statsServiceClient) GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error) {
out := new(SysStatsResponse)
err := c.cc.Invoke(ctx, "/xray.app.stats.command.StatsService/GetSysStats", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// StatsServiceServer is the server API for StatsService service.
// All implementations must embed UnimplementedStatsServiceServer
// for forward compatibility
type StatsServiceServer interface {
GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error)
QueryStats(context.Context, *QueryStatsRequest) (*QueryStatsResponse, error)
GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error)
mustEmbedUnimplementedStatsServiceServer()
}
// UnimplementedStatsServiceServer must be embedded to have forward compatible implementations.
type UnimplementedStatsServiceServer struct {
}
func (UnimplementedStatsServiceServer) GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetStats not implemented")
}
func (UnimplementedStatsServiceServer) QueryStats(context.Context, *QueryStatsRequest) (*QueryStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method QueryStats not implemented")
}
func (UnimplementedStatsServiceServer) GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetSysStats not implemented")
}
func (UnimplementedStatsServiceServer) mustEmbedUnimplementedStatsServiceServer() {}
// UnsafeStatsServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to StatsServiceServer will
// result in compilation errors.
type UnsafeStatsServiceServer interface {
mustEmbedUnimplementedStatsServiceServer()
}
func RegisterStatsServiceServer(s grpc.ServiceRegistrar, srv StatsServiceServer) {
s.RegisterService(&_StatsService_serviceDesc, srv)
}
func _StatsService_GetStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetStatsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StatsServiceServer).GetStats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/xray.app.stats.command.StatsService/GetStats",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StatsServiceServer).GetStats(ctx, req.(*GetStatsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StatsService_QueryStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(QueryStatsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StatsServiceServer).QueryStats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/xray.app.stats.command.StatsService/QueryStats",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StatsServiceServer).QueryStats(ctx, req.(*QueryStatsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StatsService_GetSysStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SysStatsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StatsServiceServer).GetSysStats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/xray.app.stats.command.StatsService/GetSysStats",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StatsServiceServer).GetSysStats(ctx, req.(*SysStatsRequest))
}
return interceptor(ctx, in, info, handler)
}
var _StatsService_serviceDesc = grpc.ServiceDesc{
ServiceName: "xray.app.stats.command.StatsService",
HandlerType: (*StatsServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetStats",
Handler: _StatsService_GetStats_Handler,
},
{
MethodName: "QueryStats",
Handler: _StatsService_QueryStats_Handler,
},
{
MethodName: "GetSysStats",
Handler: _StatsService_GetSysStats_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "app/stats/command/command.proto",
}

View file

@ -0,0 +1,92 @@
package command_test
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/xtls/xray-core/v1/app/stats"
. "github.com/xtls/xray-core/v1/app/stats/command"
"github.com/xtls/xray-core/v1/common"
)
func TestGetStats(t *testing.T) {
m, err := stats.NewManager(context.Background(), &stats.Config{})
common.Must(err)
sc, err := m.RegisterCounter("test_counter")
common.Must(err)
sc.Set(1)
s := NewStatsServer(m)
testCases := []struct {
name string
reset bool
value int64
err bool
}{
{
name: "counterNotExist",
err: true,
},
{
name: "test_counter",
reset: true,
value: 1,
},
{
name: "test_counter",
value: 0,
},
}
for _, tc := range testCases {
resp, err := s.GetStats(context.Background(), &GetStatsRequest{
Name: tc.name,
Reset_: tc.reset,
})
if tc.err {
if err == nil {
t.Error("nil error: ", tc.name)
}
} else {
common.Must(err)
if r := cmp.Diff(resp.Stat, &Stat{Name: tc.name, Value: tc.value}, cmpopts.IgnoreUnexported(Stat{})); r != "" {
t.Error(r)
}
}
}
}
func TestQueryStats(t *testing.T) {
m, err := stats.NewManager(context.Background(), &stats.Config{})
common.Must(err)
sc1, err := m.RegisterCounter("test_counter")
common.Must(err)
sc1.Set(1)
sc2, err := m.RegisterCounter("test_counter_2")
common.Must(err)
sc2.Set(2)
sc3, err := m.RegisterCounter("test_counter_3")
common.Must(err)
sc3.Set(3)
s := NewStatsServer(m)
resp, err := s.QueryStats(context.Background(), &QueryStatsRequest{
Pattern: "counter_",
})
common.Must(err)
if r := cmp.Diff(resp.Stat, []*Stat{
{Name: "test_counter_2", Value: 2},
{Name: "test_counter_3", Value: 3},
}, cmpopts.SortSlices(func(s1, s2 *Stat) bool { return s1.Name < s2.Name }),
cmpopts.IgnoreUnexported(Stat{})); r != "" {
t.Error(r)
}
}

View file

@ -0,0 +1,9 @@
package command
import "github.com/xtls/xray-core/v1/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

Some files were not shown because too many files have changed in this diff Show more